/** * reach — may the CONNECTED virtual user touch this document at all? * * The one predicate every path to `ng` consults, so the boundary is decided in a * single place instead of being re-argued at each call site. * * ── The boundary ────────────────────────────────────────────────────────── * A virtual user must simulate the boundary of the future single-user wallet: * every access function is confined to the user currently connected * (`setCurrentUser`), and no cross-user access is permitted. Otherwise the * consumer is coded against a reach that will never exist — the same failure mode * as an ACL where the real model is key possession, one level down. * * Two ways a document is legitimately reachable, and no others: * * 1. **You hold its cap.** Either because you created it (the store refiles the * cap) or because someone delivered it to you. This is the whole of the * access model, so it is the whole of the predicate. * 2. **It is declared INFRASTRUCTURE.** A short, explicitly-registered list — * never inferred from the shape of a NURI, because an inferred exemption is * a hole. See {@link declareInfrastructure}. * * ── What may be exempt, and why so little ───────────────────────────────── * > The only reads/writes not confined to a virtual user are those that make * > multi-user operation possible at all. Nothing common — only the indexing * > mechanisms that make the virtual users work. * * The test an exemption must pass: *does removing it stop the virtual users from * functioning, or does it merely stop users from seeing each other's content?* * Only the first qualifies. The shim passes (remove it and no user is resolvable * at all); a shared index of user content does not (remove it and every user still * works — you simply have to be given links). * * Depositing into another user's inbox is NOT handled here: it is a write to a * document you do not hold, and it is legitimate — the only channel by which a * link crosses from one user to another, hence the bootstrap of the whole * reachability graph. It is allowed at the inbox surface, which is where the * asymmetry (deposit yes, read no) is expressed. * * At migration this module disappears: the boundary becomes the wallet itself. */ import { getCaps } from "../shared-wallet/bootstrap"; import { targetOf } from "../model/nuri"; import type { Nuri } from "../model/types"; /** * NURIs of the polyfill's own scaffolding, registered as they are resolved. * * Explicit registration rather than pattern-matching: the store-root and the * doc-shim are exempt because they ARE the index of virtual users, not because * they look a certain way. A NURI is in here because some code path put it here, * knowing what it was. */ const infrastructure = new Set(); /** * Register `nuri` as scaffolding that the boundary does not apply to. Called by * the store-registry as it resolves the store-root pointer and the doc-shim — * the only two documents that qualify, because without them no virtual user can * be resolved at all. * * Deliberately NOT exported from the package: nothing outside the library may * widen the exemption list. */ export function declareInfrastructure(nuri: Nuri): void { infrastructure.add(nuri); } /** Is `nuri` registered scaffolding? */ export function isInfrastructure(nuri: Nuri): boolean { return infrastructure.has(nuri); } /** Forget every declared exemption (tests / a fresh wallet). */ export function resetInfrastructure(): void { infrastructure.clear(); } /** * Do we POSSESS the cap of `nuri`? Not "does this string carry one" — a caller may * legitimately be holding the bare form and possess the cap elsewhere, which is the * normal case: NURIs travel bare through content and indexes, while the cap sits in * what the user holds. Possession is what decides; the shape of the reference the * caller happens to have in hand decides nothing. * * `targetOf` first, so a cap-bearing reference and its bare form answer alike. * * Inert until the first cap exists (`caps.isEnforcing()`), so a consumer that never * touches caps keeps working. Once ANY cap has been issued the boundary applies to * every user, including one holding nothing: that is the isolation. */ export function mayReach(nuri: Nuri): boolean { const caps = getCaps(); if (!caps.isEnforcing()) return true; const target = targetOf(nuri); return isInfrastructure(target) || caps.capFor(target) !== undefined; } /** * **Rule 1 — authorization**, at the PASSAGE POINTS (`docs.*`, `subscribe`). * * Nothing reaches `ng` unless the connected user possesses the document's cap. This * is the guard: it fires on a request that should never have been made, and its job * is to make sure the attempt fails rather than succeeds quietly. * * Deliberately duplicated with rule 2 below — see {@link mustNotAttempt}. Two rules, * two places, one criterion: a lapse in either is caught by the other. */ export function assertMayReach(nuri: Nuri, op: string): void { if (mayReach(nuri)) return; throw new Error( `[ng-eventually] ${op}: refused — the connected user does not hold this document's ` + "cap. Naming a document does not grant access to it: a cap is looked up in what " + `you hold, or it was delivered to you. ${JSON.stringify(nuri)}`, ); } /** * **Rule 2 — do not even attempt**, at the CALLERS (`read-model`, `open-repo`, * `subscribe`'s callers…). * * A reader that does not hold a document's cap must not issue the operation at all. * Not attempting and being refused are different things: the first is a caller that * knows what it holds, the second is one that hoped and got caught. Only the first * is the model — upstream you cannot even address a repo you have no cap for. * * Practically it also stops the library from asking the broker for documents it has * no business asking about, which is work, noise, and a leak of intent. */ export function mustNotAttempt(nuri: Nuri): boolean { return !mayReach(nuri); }