36c0148750
L'entrée publique `/polyfill` portait le magasin d'injection (le `ng` injecté, les dépendances du registre, QUI est connecté, le singleton `CapRegistry`). Tout module interne l'importait donc pour atteindre la config — ce qui faisait de l'entrée une dépendance du code qu'elle publie, avec les cycles `polyfill` <-> `connect` et `polyfill` <-> `inbox`. Le magasin rejoint `shared-wallet/` : rien n'est injecté en amont, l'app importe le SDK et « qui suis-je » est la session — il n'y a pas de relais d'utilisateur courant parce qu'un wallet a exactement un user. Ce module est la forme de cette absence, il s'évapore en entier à la migration. L'entrée ne fait plus que ré-exporter. Plus aucun module interne n'importe `polyfill`.
134 lines
6.1 KiB
TypeScript
134 lines
6.1 KiB
TypeScript
/**
|
|
* 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<Nuri>();
|
|
|
|
/**
|
|
* 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);
|
|
}
|