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:
@@ -50,9 +50,32 @@
|
|||||||
* writer; they are left as-is and belong to P1b.
|
* 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";
|
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). */
|
/** The map key of the anonymous holder (no identity established yet). */
|
||||||
const ANONYMOUS = "";
|
const ANONYMOUS = "";
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
import { mustNotAttempt } from "./reach";
|
import { mustNotAttempt } from "./reach";
|
||||||
import { getConfig, getStoreRegistryDeps } from "../polyfill";
|
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 { logStage, shortNuri } from "../shared-wallet/access-log";
|
||||||
import type { Nuri } from "../model/types";
|
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,
|
* The unguarded core. Exported for ONE importer — `shared-wallet/physical.ts` — and
|
||||||
* doc-shim). The machinery's counterpart to {@link ensureRepoOpen}: resolving
|
* for nobody else; neither entry point re-exports it. The `Unguarded` suffix is the
|
||||||
* WHICH documents a virtual user owns cannot itself be confined to that user.
|
* warning, and the single importer is what keeps it honest.
|
||||||
* See `physical.ts` for why this is a separate function and not an exemption.
|
|
||||||
*
|
|
||||||
* Never exported from the package.
|
|
||||||
*/
|
*/
|
||||||
export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise<void> {
|
export async function openRepoUnguarded(nuri: Nuri): Promise<void> {
|
||||||
if (!nuri) return;
|
|
||||||
return openRepoUnguarded(nuri);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openRepoUnguarded(nuri: Nuri): Promise<void> {
|
|
||||||
// Drop the registry if the session changed (in-page re-login → fresh verifier).
|
// Drop the registry if the session changed (in-page re-login → fresh verifier).
|
||||||
await syncSession();
|
await syncSession();
|
||||||
if (opened.has(nuri)) return;
|
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
|
// Unguarded on purpose: the caller already decided. `ensureRepoOpen` applied
|
||||||
// rule 2 above; `ensurePhysicalRepoOpen` is the machinery's door and is not
|
// rule 2 above; `ensurePhysicalRepoOpen` is the machinery's door and is not
|
||||||
// subject to the boundary at all (see physical.ts).
|
// 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();
|
if (type === "State") onState();
|
||||||
});
|
});
|
||||||
held.set(nuri, unsub);
|
held.set(nuri, unsub);
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ import type { Nuri, ReadCap } from "./types";
|
|||||||
const SCHEME = "did:ng:";
|
const SCHEME = "did:ng:";
|
||||||
/** The segment that turns a naming NURI into a reading one — upstream's ReadCap
|
/** 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. */
|
* 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
|
* 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 };
|
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 { sparqlUpdate, sparqlQuery } from "../surface/docs";
|
||||||
import { physicalCreate, physicalQuery, physicalUpdate } from "./physical";
|
import { physicalCreate, physicalQuery, physicalUpdate } from "./physical";
|
||||||
import { getCaps, getCurrentUser, getStoreRegistryDeps } from "../polyfill";
|
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 { 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 { mustNotAttempt } from "../emulated-verifier/reach";
|
||||||
import { accessLogPrefix, logStage, shortNuri } from "./access-log";
|
import { accessLogPrefix, logStage, shortNuri } from "./access-log";
|
||||||
import type { Nuri, ReadCap, Scope } from "../model/types";
|
import type { Nuri, ReadCap, Scope } from "../model/types";
|
||||||
|
|||||||
@@ -40,6 +40,9 @@
|
|||||||
|
|
||||||
import { getConfig } from "../polyfill";
|
import { getConfig } from "../polyfill";
|
||||||
import { logAccess } from "./access-log";
|
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 { isNuri } from "../model/nuri";
|
||||||
import type { Nuri } from "../model/types";
|
import type { Nuri } from "../model/types";
|
||||||
|
|
||||||
@@ -101,3 +104,34 @@ export async function physicalUpdate(
|
|||||||
logAccess("WRITE", anchor, label, " (physical)");
|
logAccess("WRITE", anchor, label, " (physical)");
|
||||||
return ng.sparql_update(sessionId, query, anchor);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,17 +112,11 @@ export function subscribeDoc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe as the PHYSICAL user — the shim's own documents. The machinery's
|
* The unguarded core. Exported for ONE importer — `shared-wallet/physical.ts`, which
|
||||||
* counterpart to {@link subscribeDoc}; never exported from the package.
|
* 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(
|
export function subscribeDocUnguarded(
|
||||||
nuri: Nuri,
|
|
||||||
onChange: (r: DocChange, type: DocChangeType) => void,
|
|
||||||
): Unsubscribe {
|
|
||||||
return subscribeDocUnguarded(nuri, onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeDocUnguarded(
|
|
||||||
nuri: Nuri,
|
nuri: Nuri,
|
||||||
onChange: (r: DocChange, type: DocChangeType) => void,
|
onChange: (r: DocChange, type: DocChangeType) => void,
|
||||||
): Unsubscribe {
|
): Unsubscribe {
|
||||||
|
|||||||
Reference in New Issue
Block a user