feat: un document en store public sert son ReadCap, une référence nue suffit
Le modèle amont est explicite dans `PublicRepoLinkV0` : le lien ne porte AUCUN `read_cap`, et son commentaire dit pourquoi — *"The latest ReadCap of the branch will be downloaded from the outerOverlay, if the peer brokers listed below allow it […] the public site are served differently by brokers"* (engine/net/src/types.rs:5098). La clé n'est pas remise par un émetteur : elle est donnée par le réseau à qui la demande, parce que le broker a épinglé l'overlay externe (`expose_outer`). La bibliothèque refusait jusqu'ici la forme sans cap quel que soit le store. Sûr dans le bon sens, mais une application ne pouvait pas exprimer « fais circuler, la référence suffit » — le seul acte que le modèle rend gratuit — et son unique contournement était de distribuer la clé, ce qui détruit la confidentialité composable. `emulated-verifier/public-store.ts` émule le mécanisme SANS toucher à la garde. La possession reste l'unique critère : un document public est lisible non par exception mais parce que son cap est *obtenable*. Chaque porte de lecture demande d'abord (`readUnion`, `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`), puis le chemin ordinaire s'applique. Lire n'est pas écrire. Ce que le store sert est un droit de LECTURE : `learnFromPublicStore` le classe à part et `assertMayWrite` refuse l'écriture dessus. Sans cela une référence nue achetait une écriture, ce qu'aucun store amont n'accorde. Autres conséquences : - `recordInPublicStore` (marquer + frapper) devient `markInPublicStore` (marquer). Frapper un second cap à côté de celui qu'on vient de télécharger donnerait deux clés différentes le jour où la constante devient un secret. - `hasCap` quitte la porte polyfill : il se lisait « ai-je le droit de lire ceci ? » et un document public y répondait `false` jusqu'à ce qu'on demande son cap. Aucun appelant hors des tests. - Les tests cross-user ne font plus traverser de cap par une variable JS : Bob n'obtient que la référence nue, comme une vraie application. Écarts documentés plutôt que masqués : le pari sur un modèle DÉCLARÉ (`expose_outer` est câblé à `false` côté client et `ExtTopicSyncReq` est `unimplemented!()`), la découverte limitée à ce qu'on sait déjà nommer, `useShape` qui n'a pas d'await à dépenser, et l'absence de `locator`. 179 tests unitaires, e2e 42/42 contre le broker en ligne.
This commit is contained in:
+16
-15
@@ -336,7 +336,7 @@ declare function doc_subscribe(repo_o: string, session_id: any, callback: Functi
|
||||
|
||||
## 9. Inbox — deposits, and cap delivery
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `inbox`; `shareCap` also re-exported from `/polyfill`)
|
||||
### Today — `@ng-eventually/client` (namespace `inbox`; `share` also re-exported from `/polyfill`)
|
||||
|
||||
```ts
|
||||
// inbox.ts:48,58
|
||||
@@ -355,7 +355,7 @@ export async function post(targetInbox: Nuri, opts: PostOptions): Promise<void>;
|
||||
// inbox.ts:215
|
||||
export async function postToDocument(doc: Nuri, opts: PostOptions): Promise<void>;
|
||||
// inbox.ts:282
|
||||
export async function shareCap(cap: ReadCap, toInbox: Nuri): Promise<void>;
|
||||
export async function share(doc: NuriLike, toUser: string): Promise<void>;
|
||||
// inbox.ts:339
|
||||
export async function read(targetInbox: Nuri): Promise<Deposit[]>;
|
||||
// inbox.ts:419
|
||||
@@ -384,7 +384,7 @@ export function watch(
|
||||
Consequences per function:
|
||||
|
||||
- `post` / `postToDocument` — the sender-side act exists in the model (the broker routes `InboxPost` natively, `engine/net/src/server_broker.rs`); its JS surface does not. **The future SDK's name and signature are unknown** — `docs/nextgraph-current-state.md:187` records that nothing is announced. `postToDocument`'s resolution step (find the document's inbox address) rides on a **deliberate divergence**: this lib PUBLISHES the address on the document (Header-branch emulation), whereas upstream an address is only ever TRANSMITTED (`ContactDetails` carries `ng:site_inbox`/`ng:protected_inbox`, `engine/verifier/src/inbox_processor.rs:778-830`; the verifier's `inboxes` table is session-local, rebuilt empty — `verifier.rs:520,2820`). Documented in `docs/briefs/2026-08-03-document-inbox-addressing.md`.
|
||||
- `shareCap` — a **gap upstream, not a disagreement**, verified at both ends: `ContactDetails.read_cap: Option<ReadCap>` exists (`engine/net/src/types.rs:4233`) but building a message with it is `unimplemented!()` (`types.rs:3786`), its only caller passes `with_readcap: false`, and the receiving arm never reads the field (`inbox_processor.rs:778-830`). `InboxMsgContent::Link` is a **unit variant carrying nothing** (`types.rs:4252`) — do not read it as the delivery channel. The recipient-side filing the lib emulates is real: `AddLink { read_cap }` on the User branch (`engine/repo/src/types.rs:1939-1948`). The consumer's *act* (share one document's cap to one inbox) is target-shaped; only the transport is emulated.
|
||||
- `share` — a **gap upstream, not a disagreement**, verified at both ends: `ContactDetails.read_cap: Option<ReadCap>` exists (`engine/net/src/types.rs:4233`) but building a message with it is `unimplemented!()` (`types.rs:3786`), its only caller passes `with_readcap: false`, and the receiving arm never reads the field (`inbox_processor.rs:778-830`). `InboxMsgContent::Link` is a **unit variant carrying nothing** (`types.rs:4252`) — do not read it as the delivery channel. The recipient-side filing the lib emulates is real: `AddLink { read_cap }` on the User branch (`engine/repo/src/types.rs:1939-1948`). The consumer's *act* (share one document's cap to one inbox) is target-shaped; only the transport is emulated.
|
||||
- `read` / `materialize` / `readSynced` / `processInbox` / `watch` — **stand-ins for the recipient's own verifier processing**, which has no consumer-facing JS surface upstream and may never have this list-of-deposits shape. A consumer should treat "my inbox gets processed when I connect, and applied caps just appear in what I hold" as the durable contract (that is what `connectedUser` automates, § 13); code that leans on enumerating raw deposits as a mailbox UI is coding against emulation detail it may have to unlearn. The consumer-payload case (`Deposit.payload` as app data) maps to `InboxMsgContent` variants upstream (`types.rs:4249-4260`), of which only `ContactDetails` and `SocialQuery` are more than unit variants today — arbitrary app payloads through the inbox are an **ASSUMPTION**, constrained by the model only in that messages are sealed, per-recipient, and applied by the recipient.
|
||||
- `watch`'s `_opts?: { intervalMs?: number }` is accepted and **ignored** (kept for signature compatibility with a removed polling watcher) — dead surface, see § 15.
|
||||
|
||||
@@ -405,11 +405,10 @@ export function hasReadCap(s: string): s is ReadCap;
|
||||
export type Nuri = `did:ng:${string}`;
|
||||
export type ReadCap = `did:ng:${string}:r:${string}`;
|
||||
|
||||
// @ng-eventually/client/polyfill — polyfill.ts:205
|
||||
export function capFor(nuri: Nuri): ReadCap | undefined;
|
||||
// polyfill.ts:193 — hands out the registry itself
|
||||
export function getCaps(): CapRegistry;
|
||||
// polyfill.ts:215 — tests / fresh wallet only
|
||||
// @ng-eventually/client/polyfill
|
||||
// tests / fresh wallet only — the registry itself is NOT published, and neither is any
|
||||
// "do I hold this?" predicate (`hasCap`, removed 2026-08-06: it read like "may I read
|
||||
// this?", and a document in a public store answers `false` until something asks for it).
|
||||
export function resetCaps(): void;
|
||||
|
||||
// @ng-eventually/client/polyfill — caps.ts:59 (class CapRegistry)
|
||||
@@ -417,8 +416,10 @@ constructor(holder?: () => PrincipalId | null);
|
||||
mint(nuri: Nuri): ReadCap;
|
||||
learn(cap: ReadCap): void;
|
||||
capFor(nuri: Nuri): ReadCap | undefined;
|
||||
publishRepoLink(nuri: Nuri): ReadCap;
|
||||
isPublished(nuri: Nuri): boolean;
|
||||
learnFromPublicStore(cap: ReadCap): void; // a cap the public store SERVED — read only
|
||||
isReadOnlyPublicCap(nuri: Nuri): boolean;
|
||||
markInPublicStore(nuri: Nuri): void;
|
||||
isInPublicStore(nuri: Nuri): boolean;
|
||||
open(nuri: Nuri, scope: Scope): ReadCap;
|
||||
isEnforcing(): boolean;
|
||||
onChange(listener: () => void): () => void;
|
||||
@@ -439,7 +440,7 @@ clear(): void;
|
||||
|
||||
`capFor(nuri)` asks the only question the model admits — "do I hold this document's key?" — and returning `undefined` is the whole possible answer. There is no "may principal P read D?" anywhere, and the future SDK cannot offer one without inventing an ACL the engine does not have. That absence is a **finding about the target's model**, not a missing feature: a consumer should never expect a cap-introspection API.
|
||||
|
||||
The `CapRegistry` class itself is machinery (the in-memory record of what the connected holder holds — upstream's local user storage). The consumer-facing surface is `capFor` + the acts (`shareCap`, creating a document, processing one's inbox); see § 15.
|
||||
The `CapRegistry` class itself is machinery (the in-memory record of what the connected holder holds — upstream's local user storage). It is not published at all: the consumer surface is the ACTS (creating a document, `inbox.share`, processing one's inbox — and, for a document in a public store, simply reading it), never a lookup; see § 15.
|
||||
|
||||
---
|
||||
|
||||
@@ -586,10 +587,10 @@ export type { NG } from "@ng-org/web";
|
||||
|
||||
Exported, but not SDK surface. Coding against these builds knowledge that migration deletes:
|
||||
|
||||
- **`docs.depositInto`** — the named boundary-crossing write `inbox.post` uses. It is exported only because `inbox.ts` lives in another module; a consumer must always go through `inbox.post` / `inbox.shareCap`. Upstream a deposit is a sealed message, not a SPARQL update — this function's very signature is emulation.
|
||||
- **`docs.depositInto`** — the named boundary-crossing write `inbox.post` uses. It is exported only because `inbox.ts` lives in another module; a consumer must always go through `inbox.post` / `inbox.share`. Upstream a deposit is a sealed message, not a SPARQL update — this function's very signature is emulation.
|
||||
- **`getConfig` / `getStoreRegistryDeps`** — tagged `@internal` in source, exported for the lib's own wrappers.
|
||||
- **`resetConfig` / `resetStoreRegistry` / `resetCaps` / `storeRegistry.resetRegistryCache`** — test/reset machinery. In particular `resetCaps` wipes EVERY holder's caps, which no product flow should ever do.
|
||||
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is `capFor` (possession lookup), `inbox.shareCap` (grant), and the acts that file caps implicitly (creating a document, processing one's inbox). `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until P1b — the guard they feed is bypassed by every internal writer.
|
||||
- **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is the acts that file caps: creating a document, `inbox.share` (grant), processing one's inbox, and reading a document a public store serves. `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until P1b — the guard they feed is bypassed by every internal writer.
|
||||
- ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `VirtualUserRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers.
|
||||
- ~~**`storeRegistry.addLink` / `readLinks`**~~ — **RESOLVED 2026-08-03**: no longer exported. Consumers receive caps by processing their inbox (automated at connection); calling these directly baked in a register the verifier owns upstream.
|
||||
- ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line.
|
||||
@@ -599,7 +600,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
### Places the current surface teaches something to unlearn
|
||||
|
||||
- ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `watchShape`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery".
|
||||
- **`shareCap` is importable from both entries** (`inbox.shareCap` on the SDK entry via `export * as inbox`, and a named re-export on `/polyfill`). The polyfill re-export exists "so the cap vocabulary stays on the polyfill side" — but the namespace export undoes that. Harmless functionally; blurs the same boundary.
|
||||
- **`share` is importable from both entries** (`inbox.share` on the SDK entry via `export * as inbox`, and a named re-export on `/polyfill`). The polyfill re-export exists "so the cap vocabulary stays on the polyfill side" — but the namespace export undoes that. Harmless functionally; blurs the same boundary.
|
||||
- **`inbox.read`/`materialize` as a mailbox** — enumerating raw deposits is emulation detail (§ 9); the durable contract is deposit-and-it-gets-applied. An app building UI on the deposit list should expect that surface to change shape entirely.
|
||||
- **`watchShape`'s "planned `useShape` upgrade"** — stated in the module header with no provenance in this repo or the clone (§ 5). The load-state *distinction* is safe; the claim that NextGraph plans this exact hook shape is an assumption and must not be cited as an announced API.
|
||||
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `watchShape`'s design already assumes).
|
||||
@@ -623,5 +624,5 @@ storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScop
|
||||
### `@ng-eventually/client/polyfill` — `src/polyfill.ts`
|
||||
|
||||
```text
|
||||
direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, hasCap, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, share
|
||||
direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, share
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user