refactor(layout): les portes non gardées en un seul module, mintCap chez le verifier

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.
This commit is contained in:
Sylvain Duchesne
2026-08-04 12:49:59 +02:00
parent 88914f50ae
commit cd096de2b0
6 changed files with 77 additions and 61 deletions
+24 -1
View File
@@ -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 = "";
@@ -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<void> {
}
/**
* 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<void> {
if (!nuri) return;
return openRepoUnguarded(nuri);
}
async function openRepoUnguarded(nuri: Nuri): Promise<void> {
export async function openRepoUnguarded(nuri: Nuri): Promise<void> {
// 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<void> {
// 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);
+5 -34
View File
@@ -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}`;
}
@@ -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";
@@ -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<void> {
if (!nuri) return;
return openRepoUnguarded(nuri);
}
+4 -10
View File
@@ -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 {