refactor(layout): ranger les modules par destin à la migration
Les 25 modules étaient à plat, nommés d'après ce qu'ils font mécaniquement (`store-registry`, `read-model`, `reach`, `caps`). Rien dans l'arborescence ne disait lesquels DEVIENDRONT le vrai SDK, lesquels tiennent lieu du travail que le verifier fera nativement, et lesquels n'existent que parce qu'un wallet est partagé — trois destins sans rapport. Quatre dossiers, les deux fichiers d'entrée restant à la racine pour que l'`exports` du paquet et le code du consommateur ne bougent pas : - `model/` — le modèle d'adressage de la cible, transcrit : vocabulaire pur, pas d'I/O. Survit comme connaissance. - `surface/` — ce que l'app touche, chaque symbole ayant un pendant cible documenté. Supprimé quand l'alias bascule ; le code de l'app est inchangé. - `emulated-verifier/` — les doublures de ce que le verifier fait nativement : possession, dépôt des caps, frontière, non-livraison, traitement des inbox, registres de branche, ouverture de repo. **C'est le dossier où diverger du modèle est possible.** Le préfixe `emulated-` porte le sens : tient lieu de, jamais est — cette bibliothèque ne réside dans aucune couche de la cible, elle les référence. - `shared-wallet/` — n'existe que parce qu'un wallet héberge toutes les identités. Aucun pendant, rien sur quoi s'aligner ; sa seule loi est de rester invisible depuis `surface/`. S'évapore, remplacé par rien. `store-registry-api.ts` devient `surface/placement.ts` : il faisait déjà à la main ce que la frontière de dossier fait structurellement — c'est la meilleure preuve interne du bien-fondé de ce rangement. Ce commit ne fait que déplacer et recâbler les imports (src, test, e2e). Les scissions des modules à cheval suivent. 157 tests unitaires, typecheck src/test/e2e vert.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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 "../polyfill";
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user