rspace-online/server/mi-access.ts

66 lines
2.3 KiB
TypeScript

/**
* MI Access Control — validates caller access to a space before
* serving MI data (knowledge index, memory, query-content).
*
* Reuses existing auth primitives from spaces.ts / community-store.
*/
import { resolveCallerRole } from "./spaces";
import type { SpaceRoleString } from "./spaces";
import { loadCommunity, getDocumentData } from "./community-store";
import type { EncryptIDClaims } from "./auth";
export interface MiAccessResult {
allowed: boolean;
role: SpaceRoleString;
reason?: string;
}
/**
* Check whether the caller (identified by claims) may access MI for
* the given space. Returns the effective role if allowed.
*/
export async function validateMiSpaceAccess(
space: string,
claims: EncryptIDClaims | null,
minRole: SpaceRoleString = "viewer",
): Promise<MiAccessResult> {
if (!space) {
return { allowed: false, role: "viewer", reason: "Space parameter required" };
}
await loadCommunity(space);
const data = getDocumentData(space);
if (!data) {
return { allowed: false, role: "viewer", reason: "Space not found" };
}
const visibility = data.meta?.visibility || "public";
// Private and permissioned spaces require authentication
if ((visibility === "private" || visibility === "permissioned") && !claims) {
return { allowed: false, role: "viewer", reason: "Authentication required for this space" };
}
// Resolve the caller's role
const resolved = await resolveCallerRole(space, claims);
const role: SpaceRoleString = resolved?.role || "viewer";
// For private spaces, non-members (viewer default) are denied
if (visibility === "private" && role === "viewer" && !resolved?.isOwner) {
// Check if the caller is actually a member
if (!claims) {
return { allowed: false, role: "viewer", reason: "Authentication required for this space" };
}
// resolveCallerRole returns viewer for non-members — check membership explicitly
const callerDid = (claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`;
const isMember = data.members?.[claims.sub] || data.members?.[callerDid];
const isOwner = data.meta?.ownerDID === claims.sub || data.meta?.ownerDID === callerDid;
if (!isMember && !isOwner) {
return { allowed: false, role: "viewer", reason: "You don't have access to this space" };
}
}
return { allowed: true, role };
}