import type { DID, SpaceAccessLevel, SpaceGrantLevel, SpaceMemberList, } from "@commonfabric/api"; import { debugStr } from "@commonfabric/data-model"; import { isDID, isWellFormedDID } from "@commonfabric/identity/did"; import { aclDocId, ANYONE_USER, isACL } from "@commonfabric/memory/acl"; import type { MemorySpace, URI } from "@commonfabric/memory/interface"; import { type Cell, unwrapCell } from "../cell.ts"; import { spaceReaderRole, type SpaceRole } from "../cfc/space-membership.ts"; import { getCellOrThrow, isCellResult } from "../query-result-proxy.ts"; import type { Runtime } from "../runtime.ts"; import { scopeRank } from "../scope.ts"; import type { IExtendedStorageTransaction } from "../storage/interface.ts"; import { topFrame } from "./frame-context.ts"; /** The level each role {@link spaceReaderRole} returns stands for. */ const LEVEL_OF_ROLE: Record = { owner: "OWNER", writer: "WRITE", reader: "READ", }; /** * Returns the current principal's own access to the space `target`'s value * lives in: its membership as the space's access list states it, * `acl[principal] ?? acl["*"]`. {@link spaceReaderRole} decides it, as it does * for the render membership lookup. The memory server also grants configured service DIDs * `OWNER`, which never arises here since the principal is never the service. * * `"none"` means the principal holds nothing there: the list grants them * nothing, or, on a client, the memory server has refused the principal the * space outright. `undefined` means the answer is not known yet, which is * what an access list that has not arrived, a space that has no access list, * a run with no principal, and a `target` passed as `undefined` all return. * * `target` is required, and names the space explicitly even when it is the * calling code's own. A `target` passed as `undefined` is one not known yet: * a computation's by-value input reads `undefined` while the value it names * cannot be read, which is when the principal may not belong to its space. * * Who the principal is depends on where the call runs. In a reactive * computation it is the principal demanding the value, and the call makes the * computation's value a per-user one, so two users never share an answer. In * a handler it is the event's actor. In a pattern body it throws, since a * pattern body builds one graph for every viewer. * * The level names no principal, and tells a member only what a member can * already read, since any member can read the whole access list. * {@link spaceAccessOf} returns another principal's level. * * @throws If called outside a handler or a reactive computation, with no * `target`, or with a `target` that is neither a cell nor `undefined`. */ export function spaceAccess( // Optional here, though the declared API requires it, so that the runtime // check below has a case to catch from untyped callers. ...args: [target?: unknown] ): SpaceAccessLevel | undefined { const { runtime, tx, target, principal, reactive } = ownCall( "spaceAccess", args, ); if (target === undefined) return undefined; return accessLevel( runtime, tx, spaceOfTarget(target, "spaceAccess(target)"), principal, reactive, true, ); } /** * Returns `principal`'s access to the space `target`'s value lives in, where * {@link spaceAccess} returns the current principal's own: that principal's * membership as the space's access list states it, read the same way. A * handler that must know whether the principal a label names still belongs * to the space asks this. * * The answer is the list's alone. Whether the memory server has refused this * runtime the space says nothing about another principal, so a refusal does * not enter into it, and the answer does not depend on who asks, so a * computation calling it keeps its read scope. `undefined` means the list has * not arrived, the space has no list, or `target` was passed as `undefined`. * * @throws If called outside a handler or a reactive computation, with no * `target`, with a `target` that is neither a cell nor `undefined`, or * with a `principal` that is not a well-formed DID. */ export function spaceAccessOf( // Optional here, though the declared API requires both, so that the runtime // checks below have cases to catch from untyped callers. ...args: [target?: unknown, principal?: unknown] ): SpaceAccessLevel | undefined { const frame = topFrame(); const kind = frame?.frameKind; if (kind !== "lift" && kind !== "handler") { throw new Error( "`spaceAccessOf(target, principal)` can only be called from a handler " + "or a reactive computation.", ); } const { runtime, tx } = frame!; if (runtime === undefined || tx === undefined) { throw new Error( "`spaceAccessOf(target, principal)` requires an executing runtime.", ); } if (args.length === 0) { throw new Error( "`spaceAccessOf()` requires a `target`: a cell in the space to ask about.", ); } const [target, principal] = args; if (!isWellFormedDID(principal)) { throw new Error( "`spaceAccessOf(target, principal)` takes a principal's DID; " + debugStr`got $quote${principal}.`, ); } if (target === undefined) return undefined; return accessLevel( runtime, tx, spaceOfTarget(target, "spaceAccessOf(target, principal)"), principal, kind === "lift", false, ); } /** * Returns the principals the access list of the space `target`'s value lives * in names, each with the level its entry grants, and the list's `*` entry * apart, as `anyone`: so that a pattern can list a space's members. The * memory server also grants configured service DIDs `OWNER` without an entry, * and those are not listed. * * Only a principal the list grants access gets it: the current principal, * found as {@link spaceAccess} finds it, through its own entry or the `*` * entry. Anyone else gets `undefined`, which is also what an access list that * has not arrived, a space that has no access list, a run with no principal, * and a `target` passed as `undefined` all return. The answer depends on who * asks, so a call in a computation makes its value a per-user one, as * {@link spaceAccess} does, and is run again when the list changes or, on a * client, when the memory server refuses or readmits this runtime. * * The list tells a member only what a member can already read, since any * member can read the whole access list. On a serving runtime, which reads * every space as its owner, the check on the current principal is what keeps * a non-member's demand from reading it. * * @throws If called outside a handler or a reactive computation, with no * `target`, or with a `target` that is neither a cell nor `undefined`. */ export function spaceMembers( // Optional here, though the declared API requires it, so that the runtime // check below has a case to catch from untyped callers. ...args: [target?: unknown] ): SpaceMemberList | undefined { const { runtime, tx, target, principal, reactive } = ownCall( "spaceMembers", args, ); if (target === undefined) return undefined; const space = spaceOfTarget(target, "spaceMembers(target)"); const level = accessLevel(runtime, tx, space, principal, reactive, true); if (level === undefined || level === "none") return undefined; // A level other than `none` comes from a well-formed list, so the check // below only narrows its type. const acl = aclOf(runtime, tx, space); if (!isACL(acl)) return undefined; const principals: Record = {}; let anyone: SpaceGrantLevel | undefined; for (const [user, entry] of Object.entries(acl)) { if (entry === undefined) continue; if (user === ANYONE_USER) anyone = entry; else if (isDID(user)) principals[user] = entry; } return anyone === undefined ? { principals } : { principals, anyone }; } /** * Returns the DID of the space `target`'s value lives in, after following any * links it holds, so that a pattern can name a space it holds a cell of: one a * handler just created with `PatternFactory.inSpace()`, say, whose DID an * invitation to it has to carry. * * `undefined` means the answer is not known yet. A `target` passed as * `undefined` is one not known yet, as it is for {@link spaceAccess}. And a run * that has named an `inSpace()` target its space has not resolved returns * `undefined` for every `target`: the runner resolves the name once the run * ends and runs it again, discarding what the first run wrote, and the child * that run created has no space yet. The run after it returns the child's * space. * * Unlike {@link spaceAccess}, the answer does not depend on who asks, so a call * in a computation leaves its read scope as it is. It can be called where * {@link spaceAccess} can. A pattern body builds its graph over references * that name no space yet, so a call there throws. * * @throws If called outside a handler or a reactive computation, with no * `target`, or with a `target` that is neither a cell nor `undefined`. */ export function spaceOf( // Optional here, though the declared API requires it, so that the runtime // check below has a case to catch from untyped callers. ...args: [target?: unknown] ): MemorySpace | undefined { const frame = topFrame(); const kind = frame?.frameKind; if (kind !== "lift" && kind !== "handler") { throw new Error( "`spaceOf(target)` can only be called from a handler or a reactive " + "computation; a pattern body's references name no space yet.", ); } if (args.length === 0) { throw new Error( "`spaceOf()` requires a `target`: a cell in the space to name.", ); } const [target] = args; if (target === undefined) return undefined; const cell = cellOfTarget(target, "spaceOf(target)"); if ((frame!.pendingSpaceNames?.size ?? 0) > 0) return undefined; return spaceOfTarget(cell, "spaceOf(target)"); } /** * Returns the space the value of the cell `target` lives in, after following * any links it holds. `call` names the call `target` was passed to, for the * error. * * @throws Error when `target` is not a cell. */ export function spaceOfTarget(target: unknown, call: string): MemorySpace { return cellOfTarget(target, call).resolveAsCell().getAsNormalizedFullLink() .space; } /** * Returns the cell `target` is, whether pattern code holds it as a cell, as a * `Reactive` proxy over a whole cell, or as the value a cell's query-result * proxy reads as. `call` names the call `target` was passed to, for the error. * * @throws Error when `target` is not a cell. */ export function cellOfTarget(target: unknown, call: string): Cell { const cell = unwrapCell(target); if (cell !== undefined) return cell; if (isCellResult(target)) return getCellOrThrow(target); throw new Error(`\`${call}\` takes a cell as its target.`); } /** * Helper for {@link spaceAccess} and {@link spaceMembers}, which checks that * the call to the function `name` was made where there is one principal to * ask about, and with a `target`, and returns what the call works from: the * executing runtime and transaction, `target`, the principal the calling code * runs for, and whether the call is in a reactive computation. In a * computation it narrows the read scope to `user` before anything else, since * the answer depends on who is asking. * * @throws If called outside a handler or a reactive computation, or with no * `target`. */ function ownCall(name: string, args: [target?: unknown]): { runtime: Runtime; tx: IExtendedStorageTransaction; target: unknown; principal: string | undefined; reactive: boolean; } { const frame = topFrame(); const kind = frame?.frameKind; if (kind !== "lift" && kind !== "handler") { throw new Error( `\`${name}(target)\` can only be called from a handler or a reactive ` + "computation, where there is one principal to ask about.", ); } const { runtime, tx } = frame!; if (runtime === undefined || tx === undefined) { throw new Error(`\`${name}(target)\` requires an executing runtime.`); } if (args.length === 0) { throw new Error( `\`${name}()\` requires a \`target\`: a cell in the space to ask about.`, ); } const [target] = args; if (kind === "lift") { // The answer depends on who is asking, so the computation's value is a // per-user one whatever the principal turns out to be. This goes first so // that no return in the caller skips it. if (scopeRank(tx.getNarrowestReadScope()) < scopeRank("user")) { tx.resetNarrowestReadScope("user"); } return { runtime, tx, target, principal: runtime.homeSpacePrincipalFor(tx), reactive: true, }; } return { runtime, tx, target, principal: runtime.actingPrincipalFor(tx), reactive: false, }; } /** * Helper for {@link spaceAccess} and its siblings, which returns * `principal`'s access to `space`, reading the space's access list through * `tx`. With `reactive`, a change to whether the memory server admits this * runtime to `space` runs the executing action again. `own` says whether * `principal` is the one the calling code runs for, whose session this * runtime's may be; the session says nothing about any other principal. */ function accessLevel( runtime: Runtime, tx: IExtendedStorageTransaction, space: MemorySpace, principal: string | undefined, reactive: boolean, own: boolean, ): SpaceAccessLevel | undefined { if (principal === undefined) return undefined; // A serving runtime reads every space as that space's owner, so what the // memory server thinks of its own session says nothing about the principal // whose level it returns. Only a client's session is that principal's. const sessionIsPrincipal = own && !runtime.servingPosture; const action = runtime.scheduler.executingAction; if (sessionIsPrincipal && reactive && action !== null) { runtime.spaceAccessWatch.rerunOnChange(space, action); } const acl = aclOf(runtime, tx, space); if (sessionIsPrincipal && isRefused(runtime, space)) return "none"; if (acl === undefined) return undefined; const role = spaceReaderRole(isACL(acl) ? acl : undefined, principal); return role === null ? "none" : LEVEL_OF_ROLE[role]; } /** * Helper for {@link accessLevel} and {@link spaceMembers}, which returns the * value of `space`'s access list, read through `tx`: `undefined` while it has * not arrived, and for a space that has none. */ function aclOf( runtime: Runtime, tx: IExtendedStorageTransaction, space: MemorySpace, ): unknown { return runtime.getCellFromLink( { space, id: aclDocId(space) as URI, path: [] }, undefined, tx, ).get(); } /** * Helper for {@link accessLevel}, which returns whether the memory server has * refused this runtime's session `space` for good. */ function isRefused(runtime: Runtime, space: MemorySpace): boolean { const storage = runtime.storageManager; return (storage.spaceAccessError?.(space) ?? storage.authorizationError?.(space)) !== undefined; }