From cd096de2b061ccf06202ae6888ee47a4c8ea89c8 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Tue, 4 Aug 2026 12:49:59 +0200 Subject: [PATCH] =?UTF-8?q?refactor(layout):=20les=20portes=20non=20gard?= =?UTF-8?q?=C3=A9es=20en=20un=20seul=20module,=20mintCap=20chez=20le=20ver?= =?UTF-8?q?ifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois extractions que le rangement par destin a rendues évidentes : - `subscribePhysicalDoc` et `ensurePhysicalRepoOpen` rejoignent `shared-wallet/physical.ts`, qui devient l'API non gardée de la machinerie en un seul endroit — ce que sa propre doctrine réclamait (« des fonctions séparées, jamais des exemptions »). Elles vivaient jusqu'ici à côté de leurs jumelles gardées, à un import près d'être atteintes par erreur. Les deux cœurs sont désormais exportés sous un nom `Unguarded`, pour cet unique importateur. - `mintCap` et sa valeur de remplacement quittent `model/nuri.ts` pour `emulated-verifier/caps.ts`. `model/` transcrit le vocabulaire d'adressage de la cible ; frapper une clé n'en fait pas partie — en amont le moteur frappe à la création du repo, et ensuite on cherche un cap dans ce qu'on détient ou on le reçoit. Sa présence dans le module modèle contredisait l'en-tête de ce module et logeait la seule valeur inventée de l'émulation dans le fichier qui se dit vocabulaire vérifié. Un cycle est apparu au passage (`open-repo` ↔ `physical`) : résolu en faisant appeler à `open-repo` le cœur non gardé plutôt que la porte de la machinerie. 157 tests unitaires, typecheck src/test/e2e vert. --- packages/client/src/emulated-verifier/caps.ts | 25 +++++++++++- .../client/src/emulated-verifier/open-repo.ts | 20 +++------- packages/client/src/model/nuri.ts | 39 +++---------------- .../src/shared-wallet/account-registry.ts | 6 ++- packages/client/src/shared-wallet/physical.ts | 34 ++++++++++++++++ packages/client/src/surface/subscribe.ts | 14 ++----- 6 files changed, 77 insertions(+), 61 deletions(-) diff --git a/packages/client/src/emulated-verifier/caps.ts b/packages/client/src/emulated-verifier/caps.ts index 2b6bf4c..2084078 100644 --- a/packages/client/src/emulated-verifier/caps.ts +++ b/packages/client/src/emulated-verifier/caps.ts @@ -50,9 +50,32 @@ * writer; they are left as-is and belong to P1b. */ -import { hasReadCap, mintCap, targetOf } from "../model/nuri"; +import { CAP_SEGMENT, hasReadCap, targetOf } from "../model/nuri"; import type { Nuri, PrincipalId, ReadCap, Scope } from "../model/types"; +/** + * The stand-in cap value, and the minting point — moved here from `model/nuri.ts` + * on 2026-08-03 because it did not belong to the model. + * + * `model/` transcribes the target's addressing vocabulary; minting is not part of + * that vocabulary. Upstream nothing on the surface turns a bare reference into a cap: + * the engine mints at repo creation and you afterwards look a cap up in what you hold, + * or you were given it. Keeping `mintCap` in the model module contradicted that module's + * own header, and put the emulation's one invented value in the file that claims to hold + * only verified target vocabulary. + * + * P1b replaces this single constant with a real key; migration deletes both. + */ +const STAND_IN_CAP = "OK"; + +/** + * Build the cap-bearing form of `nuri` — `{target}:r:OK`. Passing an already + * cap-bearing reference yields the same value. INTERNAL to the emulated verifier. + */ +export function mintCap(nuri: Nuri): ReadCap { + return `${targetOf(nuri)}${CAP_SEGMENT}${STAND_IN_CAP}`; +} + /** The map key of the anonymous holder (no identity established yet). */ const ANONYMOUS = ""; diff --git a/packages/client/src/emulated-verifier/open-repo.ts b/packages/client/src/emulated-verifier/open-repo.ts index f68c9ff..a0525ba 100644 --- a/packages/client/src/emulated-verifier/open-repo.ts +++ b/packages/client/src/emulated-verifier/open-repo.ts @@ -61,7 +61,7 @@ import { mustNotAttempt } from "./reach"; import { getConfig, getStoreRegistryDeps } from "../polyfill"; -import { subscribePhysicalDoc, type Unsubscribe } from "../surface/subscribe"; +import { subscribeDocUnguarded, type Unsubscribe } from "../surface/subscribe"; import { logStage, shortNuri } from "../shared-wallet/access-log"; import type { Nuri } from "../model/types"; @@ -174,19 +174,11 @@ export async function ensureRepoOpen(nuri: Nuri): Promise { } /** - * Open a repo as the PHYSICAL user — the shim's own documents (store-root, - * doc-shim). The machinery's counterpart to {@link ensureRepoOpen}: resolving - * WHICH documents a virtual user owns cannot itself be confined to that user. - * See `physical.ts` for why this is a separate function and not an exemption. - * - * Never exported from the package. + * The unguarded core. Exported for ONE importer — `shared-wallet/physical.ts` — and + * for nobody else; neither entry point re-exports it. The `Unguarded` suffix is the + * warning, and the single importer is what keeps it honest. */ -export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise { - if (!nuri) return; - return openRepoUnguarded(nuri); -} - -async function openRepoUnguarded(nuri: Nuri): Promise { +export async function openRepoUnguarded(nuri: Nuri): Promise { // Drop the registry if the session changed (in-page re-login → fresh verifier). await syncSession(); if (opened.has(nuri)) return; @@ -239,7 +231,7 @@ async function openRepoUnguarded(nuri: Nuri): Promise { // Unguarded on purpose: the caller already decided. `ensureRepoOpen` applied // rule 2 above; `ensurePhysicalRepoOpen` is the machinery's door and is not // subject to the boundary at all (see physical.ts). - const unsub = subscribePhysicalDoc(nuri, (_r, type) => { + const unsub = subscribeDocUnguarded(nuri, (_r, type) => { if (type === "State") onState(); }); held.set(nuri, unsub); diff --git a/packages/client/src/model/nuri.ts b/packages/client/src/model/nuri.ts index 54b32d8..d9ff6c0 100644 --- a/packages/client/src/model/nuri.ts +++ b/packages/client/src/model/nuri.ts @@ -38,7 +38,11 @@ import type { Nuri, ReadCap } from "./types"; const SCHEME = "did:ng:"; /** The segment that turns a naming NURI into a reading one — upstream's ReadCap * encoding (`readcap_nuri`), NOT the `:k:` used for objects/files/commits. */ -const CAP_SEGMENT = ":r:"; +/** + * The ReadCap discriminant. Exported because the emulated verifier mints with it + * (`emulated-verifier/caps.ts`); the model owns the grammar, minting is not part of it. + */ +export const CAP_SEGMENT = ":r:"; /** * Is this string a NextGraph reference at all? A **type guard**: it is the door @@ -83,36 +87,3 @@ export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap } { return hasReadCap(nuri) ? { target: targetOf(nuri), readCap: nuri } : { target: nuri }; } -/** - * The stand-in cap value. A CONSTANT, on purpose. - * - * Upstream this segment carries `base64url(serde_bare(ObjectRef))` — the block id - * and its key serialized together. Here it carries `OK`. - * - * The only question this library can answer today is **do I hold this document's - * cap, or not** — a boolean. An earlier version derived a per-document digest, - * which looked like a key and was not one: it invited the reader to believe - * something was protected, and it made "the key is reproducible" a subtlety to - * explain rather than a fact you can see. `OK` says what it is — a presence - * marker. The document a cap opens is already identified by the NURI it is - * attached to, so the value carries no information anyway. - * - * P1b replaces this single constant with a real key. Nothing else has to change: - * every path already reads a cap rather than recomputing one. - */ -const STAND_IN_CAP = "OK"; - -/** - * Build the cap-bearing form of `nuri` — `{target}:r:OK`. Passing an already - * cap-bearing reference yields the same value. - * - * This is INTERNAL: nothing on the library's surface turns a bare reference into a - * cap, because that is not how the model works — you look a cap up in what you - * hold, or you were given it (see `caps.ts`). - * - * No cast needed on the way out: the compiler derives `` `did:ng:…:r:…` `` from the - * template itself, which is exactly the {@link ReadCap} shape. - */ -export function mintCap(nuri: Nuri): ReadCap { - return `${targetOf(nuri)}${CAP_SEGMENT}${STAND_IN_CAP}`; -} diff --git a/packages/client/src/shared-wallet/account-registry.ts b/packages/client/src/shared-wallet/account-registry.ts index 38f61ce..a2ba031 100644 --- a/packages/client/src/shared-wallet/account-registry.ts +++ b/packages/client/src/shared-wallet/account-registry.ts @@ -64,9 +64,11 @@ import { sparqlUpdate, sparqlQuery } from "../surface/docs"; import { physicalCreate, physicalQuery, physicalUpdate } from "./physical"; import { getCaps, getCurrentUser, getStoreRegistryDeps } from "../polyfill"; -import { ensureRepoOpen, ensurePhysicalRepoOpen } from "../emulated-verifier/open-repo"; +import { ensureRepoOpen } from "../emulated-verifier/open-repo"; +import { ensurePhysicalRepoOpen, subscribePhysicalDoc } from "./physical"; import { escapeLiteral, escapeIri, assertNuri } from "../surface/sparql"; -import { hasReadCap, isNuri, mintCap } from "../model/nuri"; +import { hasReadCap, isNuri } from "../model/nuri"; +import { mintCap } from "../emulated-verifier/caps"; import { mustNotAttempt } from "../emulated-verifier/reach"; import { accessLogPrefix, logStage, shortNuri } from "./access-log"; import type { Nuri, ReadCap, Scope } from "../model/types"; diff --git a/packages/client/src/shared-wallet/physical.ts b/packages/client/src/shared-wallet/physical.ts index 40ed302..6baa9bc 100644 --- a/packages/client/src/shared-wallet/physical.ts +++ b/packages/client/src/shared-wallet/physical.ts @@ -40,6 +40,9 @@ import { getConfig } from "../polyfill"; import { logAccess } from "./access-log"; +import { subscribeDocUnguarded } from "../surface/subscribe"; +import { openRepoUnguarded } from "../emulated-verifier/open-repo"; +import type { DocChange, DocChangeType, Unsubscribe } from "../surface/subscribe"; import { isNuri } from "../model/nuri"; import type { Nuri } from "../model/types"; @@ -101,3 +104,34 @@ export async function physicalUpdate( logAccess("WRITE", anchor, label, " (physical)"); return ng.sparql_update(sessionId, query, anchor); } + +// --- the rest of the privileged door --------------------------------------- +// +// Moved here 2026-08-03 so that ONE module is the machinery's entire unguarded API, +// which is what this module's own doctrine asked for (see the header: separate +// functions, never exemptions). Before this they lived beside their guarded twins in +// `surface/subscribe.ts` and `emulated-verifier/open-repo.ts` — one import away from +// being reached by mistake. + +/** + * Subscribe as the PHYSICAL user — the shim's own documents. The machinery's + * counterpart to `subscribeDoc`; never exported from the package. + */ +export function subscribePhysicalDoc( + nuri: Nuri, + onChange: (r: DocChange, type: DocChangeType) => void, +): Unsubscribe { + return subscribeDocUnguarded(nuri, onChange); +} + +/** + * Open a repo as the PHYSICAL user — the shim's own documents (store-root, doc-shim). + * The machinery's counterpart to `ensureRepoOpen`: resolving WHICH documents a virtual + * user owns cannot itself be confined to that user. + * + * Never exported from the package. + */ +export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise { + if (!nuri) return; + return openRepoUnguarded(nuri); +} diff --git a/packages/client/src/surface/subscribe.ts b/packages/client/src/surface/subscribe.ts index deaaf51..26b2fb0 100644 --- a/packages/client/src/surface/subscribe.ts +++ b/packages/client/src/surface/subscribe.ts @@ -112,17 +112,11 @@ export function subscribeDoc( } /** - * Subscribe as the PHYSICAL user — the shim's own documents. The machinery's - * counterpart to {@link subscribeDoc}; never exported from the package. + * The unguarded core. Exported for ONE importer — `shared-wallet/physical.ts`, which + * owns the machinery's entire privileged door — and for nobody else. It is not + * re-exported by either entry point; the `Unguarded` suffix is the warning. */ -export function subscribePhysicalDoc( - nuri: Nuri, - onChange: (r: DocChange, type: DocChangeType) => void, -): Unsubscribe { - return subscribeDocUnguarded(nuri, onChange); -} - -function subscribeDocUnguarded( +export function subscribeDocUnguarded( nuri: Nuri, onChange: (r: DocChange, type: DocChangeType) => void, ): Unsubscribe {