docs: sept affirmations sur NextGraph requalifiées à la source

Lot F de la revue adverse. Chaque affirmation relue dans `nextgraph-rs` par symbole avant
d'être réécrite ; aucune ne s'est révélée exacte.

- `AddLink` / `RemoveLink` / `RepoLinkV0` étaient présentés comme **implémentés**. Leurs
  arms de vérificateur sont des `Ok(())`, là où celui d'`AddRepo` fait un vrai travail, et
  rien ne les construit. Le tableau dit désormais « déclaré, stubbé », et la conclusion qui
  en déduisait « le registre existe, seule la livraison manque » est corrigée : **les deux
  bouts** sont déclarés-et-stubbés.
- La table `inboxes` était dite « reconstruite vide à chaque session » — elle est
  repeuplée au chargement, et la clé privée d'inbox est persistée par repo. L'argument de
  sécurité qui s'appuyait dessus repose maintenant sur le bon motif : la table est **par
  vérificateur**, pas éphémère.
- La citation de « `doc_create` laisse `inbox: None` » pointait un constructeur réservé aux
  tests ; re-ciblée sur le chemin de production.
- `ExtObjectGet` était dit « le seul » primitif accessible à un non-membre et exigeant les
  clés : il y en a trois, et sa structure n'a aucun champ de clé.
- L'en-tête de `public-store.ts` était marqué **VERIFIED** alors qu'il repose sur un
  commentaire de doc, et la condition qu'il citait (« si les brokers pairs l'autorisent »)
  disparaissait de la conclusion. Requalifié en **pari**, condition rétablie.
- Deux sur-restrictions corrigées (ce qu'écrit le traitement d'un `ContactDetails`, et le
  prétendu « miroir 1:1 » de `NuriV0`, qui a dix champs).

**Sur la grammaire du ReadCap, une correction de MA correction.** J'avais écrit que la
forme `{target}:r:{cap}` était notre invention. Faux : le segment `r:` et son encodage sont
ceux d'amont, et l'auteur de NextGraph l'a énoncé. Ce qui est établi est plus étroit —
aucun parseur amont n'accepte aujourd'hui un NURI de repo qui le porte, et le segment est
produit comme valeur de champ. J'avais conclu d'une implémentation absente à ce que la
cible ferait, ce que la doctrine du projet interdit nommément. Seule « P1b remplace la
valeur, pas la forme » est corrigée, requalifiée en **pari**.

**Et la surface ne publie plus de type que personne n'utilise.** `export * from
"./model/types"` publiait huit types en bloc ; c'est une liste nommée de six. `ReadCap`
sort — aucune signature publiée ne le prend ni ne le rend, seules deux fonctions privées
de `inbox.ts` s'en servent — et `InboxScope` aussi. Un type n'est publié que si une
signature publiée l'utilise.

197 tests, 0 échec ; les trois typechecks propres ; `lint` sans erreur.
This commit is contained in:
Sylvain Duchesne
2026-08-10 12:09:20 +02:00
parent 0d9e2bbe97
commit f378c71739
13 changed files with 183 additions and 67 deletions
@@ -36,11 +36,12 @@ export interface EventuallyConfig {
export async function ensureIdentity(): Promise<PrincipalId>; // returns who you are
// ── addressing ───────────────────────────────────────────────────────────
// A type is published only when a published signature uses it. `ReadCap` and
// `InboxScope` were withdrawn on 2026-08-10: no published call takes or returns
// either. They still exist inside the library — they are simply not yours to hold.
export type Nuri = `did:ng:${string}`;
export type ReadCap = `did:ng:${string}:r:${string}`;
export type NuriLike = Nuri | string;
export type Scope = "public" | "protected" | "private";
export type InboxScope = "public" | "protected";
// ── placement: where an application's documents live ─────────────────────
export const storeRegistry: { // no identity parameter — the session is one user's
+41 -8
View File
@@ -399,7 +399,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`.
- `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 address→repo association lives in `inboxes: PubKey → RepoId`, a table of the **verifier** — one per user, `verifier.rs:105`). *(Corrected 2026-08-10: this said the table was "session-local, rebuilt empty". It is initialized empty (`verifier.rs:520,2820`) and then repopulated at every load — `Verifier::load` → `add_repo_without_saving` → `add_repo_`, `verifier.rs:534-566,2871,2887` — with the inbox private key persisted per repo, `user_storage/repo.rs:61,171,207,362`. The property that matters is that it is **per verifier**, not that it is ephemeral.)* Documented in `docs/briefs/2026-08-03-document-inbox-addressing.md`.
- `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.
- `readForDocument(doc)` — the owner's side of a document's inbox, named by the DOCUMENT. Same LEVEL-1 SHAPE ruling as `read`: it is the recipient's own processing, which has no consumer-facing JS surface upstream, and enumerating its deposits is emulation detail. It exists so an application never handles an inbox address.
@@ -415,12 +415,22 @@ Consequences per function:
### Today
```ts
// @ng-eventually/sdk — model/types.ts. The types are the whole published cap surface.
// @ng-eventually/sdk — model/types.ts. The published cap surface is now ONE type.
export type Nuri = `did:ng:${string}`;
export type ReadCap = `did:ng:${string}:r:${string}`;
export type NuriLike = Nuri | string;
// NOT published, each deliberately:
// ReadCap — `did:ng:${string}:r:${string}`. Unpublished 2026-08-10, when
// `export * from "./model/types"` became a named list. It remains the library's
// internal type for a cap-bearing reference, but NO published signature takes or
// returns one: within `surface/inbox.ts` only two private helpers use it
// (`capsSeenIn`, `capOfPayload`), plus the emulated registers. Publishing it named
// the one value the model says must never be handed over on request (§ 0 of
// `readcap-and-nuri-model.md`) — while leaving no published call able to produce
// one, since `linkTo` was removed and `mintCap` is unreachable (§ 11). A type whose
// only possible use by a consumer is a cast is worse than no type. See § 14.
// InboxScope — unpublished the same day, same rule: its only user is
// `account-registry.userInbox(id, scope)`, which is not published (§ 12).
// isNuri / hasReadCap — the type guards (`model/nuri.ts`). Unpublished since the
// permissive-in change: every entry takes `NuriLike` and validates at the door, so
// a consumer holding a plain string narrows nothing. Publishing a guard would
@@ -452,7 +462,8 @@ clear(): void;
**LEVEL-1 SHAPE.** There is no capability API at level 2 or 3 (no cap method in `index.d.ts`, none in the ORM), and there is **nothing to introspect upstream**: reading is key possession. The model, VERIFIED:
- A ReadCap is the serialized `ObjectRef``format!("r:{}", base64_url::encode(&ser))` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`). The lib's `ReadCap` template-literal grammar (`…:r:{cap}`) is upstream's, with the stand-in constant `OK` in place of the key material (P1b swaps the value, not the shape).
- A ReadCap is the serialized `ObjectRef``format!("r:{}", base64_url::encode(&ser))` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`). **The `r:` segment and its encoding are upstream's**, reported by NextGraph's developer and verified in that function: id and key are serialized together into ONE opaque segment, unlike the `:k:` object/file/commit forms where they are two. The lib's `ReadCap` template-literal type uses that segment, with the stand-in constant `OK` in place of the key material.
- **"P1b swaps the value, not the shape" is a BET, and this section stated it as a fact until 2026-08-10.** What the source establishes is narrower, in three readings: (a) `readcap_nuri()` is produced as a **field value**, never concatenated onto a NURI — every call site fills `AppTabBranchInfo.readcap: Option<String>` (`engine/net/src/app_protocol.rs:1334`; `engine/verifier/src/verifier.rs:278,320`; `rocksdb_user_storage.rs:162,172`); (b) **no upstream parser accepts a repo NURI carrying `:r:`**`NuriV0::new_from` (`app_protocol.rs:643-737`) tries `did:ng:i`, `RE_REPO_O`, `RE_FILE_READ_CAP`, `RE_REPO` and `RE_BRANCH`, and none of the regexes at `engine/net/src/types.rs:48-80` has an `r:` form; (c) the slot the type *declares* for a repo read cap is a **field**`NuriV0.access: Vec<NgAccessV0>` with `NgAccessV0::ReadCap(ReadCap)` (`app_protocol.rs:54-62,192`) — itself constructed nowhere today (only `NgAccessV0::Key`, `:622`). Per the design principle none of that says the target will *not* parse a cap-bearing repo NURI; it says nothing parses one yet, so "the shape survives, only the value changes" is an assumption and not a passthrough. If the cap turns out to belong in a field, P1b moves it there instead of swapping a substring — a change the surface absorbs, because the value is opaque and nothing published parses it (§ 11).
- Caps live in two durable registers by origin: created documents → `AddRepo { read_cap }` on the store's Store branch (`engine/repo/src/types.rs:1890-1899`, committed by `doc_create` via `send_add_repo_to_store`, `engine/verifier/src/request_processor.rs:698`); received caps → `AddLink { read_cap }` on the private store's User branch (`types.rs:1939-1948`).
- The one path that loads a repo from a cap is `pub(crate)``Verifier::load_repo_from_read_cap` (`engine/verifier/src/verifier.rs:2237`) — unexposed to JS.
@@ -469,7 +480,10 @@ The `CapRegistry` class itself is machinery (the in-memory record of what the co
```ts
// NOT published — internal, and each for a stated reason:
// surface/sparql.ts escapeLiteral, escapeIri, assertNuri
// model/nuri.ts isNuri, hasReadCap, targetOf, parseNuri, toNuri, mintCap
// model/nuri.ts isNuri, hasReadCap, targetOf, parseNuri, toNuri
// emulated-verifier/caps.ts mintCap (it lived in `model/nuri.ts` until the source
// layout was reorganised by migration fate; this list
// still said so until 2026-08-10)
```
Two decisions meet here, and both point the same way.
@@ -536,7 +550,7 @@ export async function openDocumentInbox(doc: NuriLike): Promise<Nuri>;
- **`listMyEntityDocs(id, scope)` → level 1/2, VERIFIED mechanism.** Upstream the listing is the store's `ldp:contains` graph (written at `request_processor.rs:706-708`), readable with an anchored `sparql_query` on the store; the caps come back by replaying the Store branch (`AddRepo::verify``load_repo_from_read_cap`). The function's shape (give me my per-scope doc NURIs) survives; its implementation becomes one native read.
- **`userStoreDoc(id, scope)` / `resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `index.d.ts:264-272`). The store IS the container; the per-scope index document disappears.
- **`userInbox(id)` → level 1, VERIFIED counterpart with a different granularity.** Upstream a user's inboxes are their public and protected STORE repos' inboxes — the only two `AddInboxCap` commits in the engine (`engine/verifier/src/site.rs:128,149`). An identity-level "my inbox" therefore maps to a store inbox; the resolution moves into the lib/SDK and the consumer's act (deposit to an address, process my own) is unchanged.
- **`openDocumentInbox(doc)` / `documentInboxAddress(doc)` → level 1, VERIFIED support, no exerciser.** Every `Repo` carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); `AddInboxCapV0` is keyed by `repo_id` with no is-store restriction (`engine/repo/src/types.rs:1973`; applied at `engine/verifier/src/verifier.rs:1920-1928`); but no code path creates one for a plain document (`doc_create` leaves `inbox: None`, `repo.rs:574`) and no level-2/3 API exposes any of it. So: the *capability* is engine-verified; the *functions* are invented surface; and the **address publication is a real, deliberate divergence** (upstream transmits addresses, never publishes them — § 9), with the ownership guard compensating our design, not mirroring an upstream rule.
- **`openDocumentInbox(doc)` / `documentInboxAddress(doc)` → level 1, VERIFIED support, no exerciser.** Every `Repo` carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); `AddInboxCapV0` is keyed by `repo_id` with no is-store restriction (`engine/repo/src/types.rs:1973`; applied at `engine/verifier/src/verifier.rs:1920-1928`); but no code path creates one for a plain document (`doc_create` `new_repo_default``Store::create_repo_default``create_repo_with_keys`, which builds the `Repo` with `inbox: None``engine/verifier/src/verifier.rs:3004`, `engine/repo/src/store.rs:264,284,691`) and no level-2/3 API exposes any of it. So: the *capability* is engine-verified; the *functions* are invented surface; and the **address publication is a real, deliberate divergence** (upstream transmits addresses, never publishes them — § 9), with the ownership guard compensating our design, not mirroring an upstream rule.
- **`addLink(cap)` / `readLinks()` → level 1, VERIFIED model, no JS surface.** The emulated `AddLink { read_cap }` register (`engine/repo/src/types.rs:1939-1948`*"so that a user can share with all its device a new Link they received"*, external repos only). Upstream this filing happens inside the verifier when it processes the inbox; the future SDK most likely never exposes these as calls, so consumers should not code against them (§ 15).
- **`resolveAccount` / `ensureAccount` / `VirtualUserRecord` / `RegistrySession` / `reservedAccount` / `resetRegistryCache` → NO COUNTERPART.** The shared-wallet shim (accounts directory, pointer → doc-shim indirection) has no image in the target — the target has no central directory of identities (`docs/migration-guide.md` § 3). The whole group disappears with the shim.
- **`isOwnInbox` / `myInboxes` → NO COUNTERPART as API.** Upstream the question "which inboxes may I read" is answered inside the verifier by the User branch's `AddInboxCap` records; nothing suggests a JS API for it. These exist for the emulated read guard and the connection drain.
@@ -583,7 +597,7 @@ declare function user_disconnect(user_id: string): Promise<void>;
## 14. Type re-exports
`@ng-eventually/sdk` re-exports, type-only (erased at build, `index.ts:48-50`):
`@ng-eventually/sdk` re-exports, type-only (erased at build, `src/index.ts`):
```ts
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
@@ -593,6 +607,25 @@ export type { NG } from "@ng-org/web";
**PASSTHROUGH (levels 2/3, VERIFIED)**`ShapeType`/`BaseType` at `@ng-org/shex-orm` `dist/types.d.ts:5,12` (installed 0.1.2-alpha.8); `NG` at `index.d.ts:136`. At migration these imports point at the same packages directly; nothing changes for the consumer.
### The library's own model types — published by NAME since 2026-08-10
The entry used to say `export * from "./model/types"`, a blanket re-export publishing eight types in one gesture. It now names them, under one rule:
> **A type is published only if a PUBLISHED SIGNATURE uses it.**
```ts
export type { Nuri, NuriLike, Scope, PrincipalId, NgLike, UseShapeLike } from "./model/types";
```
Each one's warrant: `Nuri` is what every reference-returning call returns and `NuriLike` what every entry accepts (§ 10, § 11); `Scope` types `storeRegistry.*` and `watchShape` (§ 12, § 5); `PrincipalId` is `ensureIdentity`'s return and a field of `Deposit`, `PostOptions` and `EventuallyConfig` (§ 2bis, § 9, § 1); `NgLike` and `UseShapeLike` type the two injected objects in `EventuallyConfig` (§ 1).
Two types the blanket export published are now internal, each because **nothing published names it**:
- **`ReadCap`** — no published signature takes or returns one. Its users are two private helpers of `surface/inbox.ts` (`capsSeenIn`, `capOfPayload`) and the emulated registers. Publishing it advertised a value a consumer has no published call to obtain, and deliberately so: `linkTo` was removed precisely for handing one out (§ 0 of `readcap-and-nuri-model.md`), and `mintCap` is unreachable from outside (§ 11). The only use a consumer could make of it is a cast — which is what the surface's permissive-in / precise-out design exists to make unnecessary.
- **`InboxScope`** — used only by `account-registry.userInbox(id, scope)`, unpublished since 2026-08-05 (§ 12). An application never handles an inbox address, so it never names an inbox scope.
Both remain **defined** in `model/types.ts` and are used throughout the library; only their publication changed. Nothing about the target motivates either removal — this is a statement about *this* surface, and the same test that pins the appendix pins it.
---
## 15. Machinery on the surface — what a consumer should NOT code against
@@ -628,7 +661,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
### `@ng-eventually/sdk` — `src/index.ts` (the only entry since 2026-08-07)
```text
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, RegistrySession, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, NG, NgLike, Nuri, NuriLike, PrincipalId, RegistrySession, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
docs: docCreate, sparqlQuery, sparqlUpdate
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
+6 -4
View File
@@ -70,11 +70,13 @@ The surface consequence: the *act* — obtain a link, circulate it — is the sa
## 5. Recommendation for the polyfill
The surface already exists: `linkTo(doc: NuriLike): ReadCap` (`packages/sdk/src/surface/placement.ts:65-77`) for the traveling value, `inbox.share(doc, toUser)` (`packages/sdk/src/surface/inbox.ts:285`) for directed delivery. **Keep `linkTo` — the act is the right one** — with four adjustments:
**SUPERSEDED on its first point, 2026-08-06 — `linkTo` was REMOVED, and the removal stands.** This section used to open: *"The surface already exists: `linkTo(doc: NuriLike): ReadCap` … `inbox.share(doc, toUser)` for directed delivery. **Keep `linkTo` — the act is the right one**"*. What that recommendation missed is the single thing § 0 of `docs/readcap-and-nuri-model.md` exists to hold: a call that returns a document's **key** where a caller asked for its **reference** converts *"whoever has the reference AND the key reads"* into *"whoever has the reference reads"* — for that document and for every document it mentions — so confidentiality can no longer be composed inside anything one circulates. The *act* (circulate a value that opens a document) may still be right; making it the answer to "give me the link to my document" was not. The reasoning is recorded where the function was, in the `No linkTo here` block of `packages/sdk/src/surface/placement.ts`.
1. **Label it LEVEL-1 SHAPE in `docs/api-contract.md`.** What supports it: the `NgLink` family and its stated sharing flow, the `PermaShare` permission, the exercised object-URL and profile-QR precedents, and the PO doctrine that circulation is the only distribution. The model's own stated flows are unusable without *some* produce-a-link affordance, which is as much confidence as an unbuilt feature allows. What cannot be promised: the SDK's name for it, sync vs async (upstream link-building needs overlay + peers from the session, so async is plausible — same adapter-sized delta class as `subscribeDoc`'s sync unsubscribe), and whether the value is a NURI string or a structured link. Therefore: **the returned value is opaque**; a consumer that stores it, transmits it, and hands it back unmodified learns nothing to unlearn; a consumer that parses it does.
2. **Fix the comment-vs-code mismatch in `linkTo`.** The docstring claims *"A protected document's key never comes out this way — it goes through `share`"*; the code returns any held cap, with no scope check. The **code** is the model-true side: `RepoLinkV0`-with-key IS the protected-document link, and sharing it out-of-band is the documented normal case (`:5059`). Align the comment: a protected link carries the key and is legitimate to circulate — with the §4 durability caveat, not a prohibition.
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/sdk/src/surface/inbox.ts:410`, `packages/sdk/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as `linkTo`: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
What an application does instead, today: it names a document with the bare reference it already holds — every published call returns one — and grants access with `inbox.share(doc, toUser)` (`packages/sdk/src/surface/inbox.ts`). The four points below are kept and re-read against that: 1 and 4 stand as written for **any** future link-producing surface; 2 is void with the function; 3 is unchanged and still open.
1. **Label any such call LEVEL-1 SHAPE in `docs/api-contract.md`.** What supports it: the `NgLink` family and its stated sharing flow, the `PermaShare` permission, the exercised object-URL and profile-QR precedents, and the PO doctrine that circulation is the only distribution. The model's own stated flows are unusable without *some* produce-a-link affordance, which is as much confidence as an unbuilt feature allows. What cannot be promised: the SDK's name for it, sync vs async (upstream link-building needs overlay + peers from the session, so async is plausible — same adapter-sized delta class as `subscribeDoc`'s sync unsubscribe), and whether the value is a NURI string or a structured link. Therefore: **the returned value is opaque**; a consumer that stores it, transmits it, and hands it back unmodified learns nothing to unlearn; a consumer that parses it does.
2. ~~**Fix the comment-vs-code mismatch in `linkTo`.**~~ **VOID — the function is gone.** The observation it rested on survives and is worth keeping: `RepoLinkV0`-with-key IS the protected-document link, and circulating it out-of-band is the documented normal case (`:5059`), so a protected link carrying its key is not in itself a violation — with the § 4 durability caveat. What made `linkTo` wrong was not that the value carried a key; it was that a caller got one **by asking for a reference**. Handing over a key must be its own act, which is what `inbox.share` is.
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/sdk/src/surface/inbox.ts:410`, `packages/sdk/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as point 1: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
4. **Do not add**: link options (expiry, audience, revoke-this-link), per-reader introspection for public documents, or any API that parses or inspects a link's insides — nothing upstream supports any of them, and each teaches a lever the model does not have.
## 6. The question for the NextGraph developer
+8 -8
View File
@@ -24,21 +24,21 @@ Builds the published `ng` Proxy (consumed once, `index.ts:61`): forwards every p
- **Defect — the `login` arm fabricates a member (see Findings F1).** `@ng-org/web` has no `login`: none among the exports of `index.d.ts` (re-verified), and no `fn login` in `sdk/js/lib-wasm/src/lib.rs`. The proxy nevertheless returns a function for `prop === "login"` (`ng-proxy.ts:16-22`), so `typeof ng.login === "function"` on the wrapper while the real SDK yields `undefined` — the one place the proxy adds a member, contradicting its own header and the surface contract's "adds no member and removes none" (§ 3). Calling it throws at runtime (`ng[prop]` is undefined). **ASSUMPTION with no provenance** — no target layer names a `login`.
- Disappears at migration (the whole module).
## 2. NURI internals — the unexported slice of `nuri.ts`
## 2. NURI internals — the unexported slice of `nuri.ts`, plus the minting point
```ts
// nuri.ts:73
// model/nuri.ts
export function targetOf(nuri: Nuri): Nuri;
// nuri.ts:82
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap };
// nuri.ts:116
// emulated-verifier/caps.ts — NOT `model/nuri.ts`; it moved when the source layout was
// reorganised by migration fate, and this block said `nuri.ts` until 2026-08-10.
export function mintCap(nuri: Nuri): ReadCap;
```
`targetOf` strips a `:r:` cap segment to the naming form; `parseNuri` is the parsed pair; `mintCap` builds the cap-bearing form with the stand-in value `OK`. Kept off the surface deliberately: nothing published turns a bare reference into a cap.
`targetOf` strips a `:r:` cap segment to the naming form; `parseNuri` is the parsed pair; `mintCap` builds the cap-bearing form with the stand-in value `OK` (`STAND_IN_CAP`). Kept off the surface deliberately: nothing published turns a bare reference into a cap.
- `targetOf` / `parseNuri`**LEVEL-1 SHAPE, model VERIFIED**: a 1:1 mirror of upstream's one-type-with-optional-access NURI. The ReadCap encoding they discriminate on is `r:{base64url(serde_bare(ObjectRef))}` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`), distinct from the `:k:` object/commit forms (`object_nuri`/`commit_nuri`, `types.rs:510-514`). No JS surface parses NURIs at level 2 or 3 — the real SDK takes plain strings — so these helpers never surface in signatures and survive only as internals.
- `mintCap`**NO COUNTERPART as an operation, and that is the point**: upstream a ReadCap is produced by the engine when a repo is created, never derived from a bare reference by a caller. `mintCap` exists solely because the emulation needs a cap VALUE at creation time and P1b has not yet supplied real key material; the constant `OK` pretends nothing (`nuri.ts:87-103`). It has exactly two call sites (`shared-wallet/account-registry.ts` `createEntityDoc`; `emulated-verifier/caps.ts` internals) — the minting points of the emulation. At P1b the constant becomes a real key; at migration the function is deleted (the engine mints).
- `targetOf` / `parseNuri`**LEVEL-1 SHAPE, model VERIFIED**: they transcribe upstream's one-type-with-optional-access NURI, on the **two** fields this library uses. Not a "1:1 mirror" of `NuriV0`, as this line claimed until 2026-08-10: that type has TEN fields — `identity, target, entire_store, objects, signature, branch, overlay, access, topic, locator` (`engine/net/src/app_protocol.rs:181-194`) — of which `parseNuri` carries `target` and the cap half of `access`. The other eight have no counterpart here (the missing `locator` is a stated gap, `docs/readcap-and-nuri-model.md` § 4sexies). The ReadCap encoding they discriminate on is `r:{base64url(serde_bare(ObjectRef))}` (`BlockRef::readcap_nuri`, `engine/repo/src/types.rs:518-521`), distinct from the `:k:` object/commit forms (`object_nuri`/`commit_nuri`, `types.rs:510-514`). No JS surface parses NURIs at level 2 or 3 — the real SDK takes plain strings — so these helpers never surface in signatures and survive only as internals.
- `mintCap`**NO COUNTERPART as an operation, and that is the point**: upstream a ReadCap is produced by the engine when a repo is created, never derived from a bare reference by a caller. `mintCap` exists solely because the emulation needs a cap VALUE at creation time and P1b has not yet supplied real key material; the constant `OK` pretends nothing (`STAND_IN_CAP`, `emulated-verifier/caps.ts`). It has exactly two call sites (`shared-wallet/account-registry.ts` `createEntityDoc`; `emulated-verifier/caps.ts` internals) — the minting points of the emulation. At P1b the constant becomes a real key; at migration the function is deleted (the engine mints).
## 3. The reach boundary — `emulated-verifier/reach.ts`
@@ -266,6 +266,6 @@ export function inspectOutbox(): void;
Fully internal modules: `shared-wallet/access-log.ts` (`AccessOp`, `setAccessLog`, `enabled`, `activeIdentity`, `accessLogPrefix`, `logStage`, `shortNuri`, `logAccess`); `emulated-verifier/machinery.ts` (`MACHINERY_NS`, `isMachinerySubject`); `surface/ng-proxy.ts` (`makeNg`); `emulated-verifier/open-repo.ts` (`SyncState`, `setOpenTimeoutForTests`, `resetOpenedRepos`, `getSyncState`, `ensureRepoOpen`, `ensurePhysicalRepoOpen`, `ensureReposOpen`); `shared-wallet/outbox-log.ts` (`inspectOutbox`); `shared-wallet/physical.ts` (`physicalCreate`, `physicalQuery`, `physicalUpdate`); `emulated-verifier/reach.ts` (`declareInfrastructure`, `isInfrastructure`, `resetInfrastructure`, `mayReach`, `assertMayReach`, `mustNotAttempt`); `emulated-verifier/read-filter.ts` (`filterReadable`, `makeReadFilteredView`).
Internal slices of partially-published modules: `nuri.ts` (`targetOf`, `parseNuri`, `mintCap`); `emulated-verifier/connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `shared-wallet/account-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
Internal slices of partially-published modules: `model/nuri.ts` (`targetOf`, `parseNuri`) and `emulated-verifier/caps.ts` (`mintCap`); `emulated-verifier/connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `shared-wallet/account-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/virtualUsers.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the entry point.
+5 -2
View File
@@ -23,8 +23,11 @@ this step swaps the *emulated* key for the real one, not the model:
(`readCap` on the store's Store branch, `link` on its User branch) become the real
`AddRepo` / `AddLink` commits. Remove the emulation; the wallet and the branches
already hold them.
- `nuri.ts`'s stand-in cap value — the constant `OK` — becomes the real
`r:{base64url(serde_bare(ObjectRef))}`. It is **one function** (`mintCap`), because
- the stand-in cap value — the constant `OK` (`STAND_IN_CAP`,
`emulated-verifier/caps.ts`) becomes the real
`r:{base64url(serde_bare(ObjectRef))}`. It is **one function** (`mintCap`, in that
same module since the source layout was reorganised — this said `nuri.ts` until
2026-08-10), because
every path now READS a stored cap instead of recomputing one. `hasReadCap` /
`targetOf` stay meaningful: the `r:` discriminant is upstream grammar, not ours.
- `inbox.share(doc, toUser)` becomes the native sealed delivery (whatever the SDK ends up naming it — see the note below
+11 -1
View File
@@ -123,10 +123,20 @@ offline"*; *"removing permissions … requires a SyncSignature"* (synchronous).
**Only two repos have an inbox today: a user's public and protected STORES.** Not
documents, and not the private store. `new_store_default` attaches one solely `if
!private` (`engine/verifier/src/verifier.rs:2994`), and `doc_create` goes through
`new_repo_default`, which leaves `inbox: None` (`engine/repo/src/repo.rs:574`). The only
`new_repo_default` (`verifier.rs:3004`, called at `request_processor.rs:689`) →
`Store::create_repo_default` (`engine/repo/src/store.rs:264`) →
`create_repo_with_keys` (`store.rs:284`), which builds the `Repo` with `inbox: None`
(`store.rs:691`). The only
`AddInboxCap` commits in the whole engine are the two in `engine/verifier/src/site.rs:128,149`
— one for the public store repo, one for the protected one.
*(Citation corrected 2026-08-10. This pointed at `engine/repo/src/repo.rs:574`, which is
inside `Repo::new_with_member` (`repo.rs:543`) — a constructor no production path
reaches: its callers are `Repo::new_with_perms`, gated `#[cfg(any(test, feature =
"testing"))]` (`repo.rs:186-192`), and `#[cfg(test)]` blocks in `branch.rs:387,490` and
`commit.rs:1659,1849,1919`. The claim itself was right; it was being proved by a test
fixture.)*
**But the engine SUPPORTS an inbox on any repo — "does not" and "cannot" are different
statements.** `inbox: Option<PrivKey>` is a field of EVERY `Repo`
(`engine/repo/src/repo.rs:126`), not of a store structure. `AddInboxCapV0` is keyed by
+15 -11
View File
@@ -161,7 +161,7 @@ Consistent with the rest of the model: no role and no list, only "do you hold th
- There is **no existence command at the SDK level**.
- The only probe (`BlocksExist`) is **internal to the crate**, requires `BlockId`s **and** an already **loaded** repo, and addresses the **inner** overlay — which is derived from the **read secret**.
- A cap-less reference carries a RepoId and the **outer** overlay: no `BlockId` to probe. And the outer is never registered anyway (`expose_outer` hard-coded to `false`, with no SDK parameter).
- The only primitive accessible to a non-member (`ExtObjectGet`) requires the ObjectIds **and their keys**.
- The primitive a non-member can reach (`ExtObjectGet`) requires the **ObjectIds**, which one only holds once one can already read. *(Corrected 2026-08-10 — this line used to say "the ObjectIds **and their keys**", and to call `ExtObjectGet` the **only** such primitive. Both are wrong at the source: `ExtObjectGetV0 { overlay, ids: Vec<ObjectId>, include_files }` has **no key field** at all (`engine/net/src/types.rs:4492-4501`), and `ExtRequestContentV0` has **three** variants — `WalletGetExport`, `ExtObjectGet`, `ExtTopicSyncReq` (`:4520-4526`), the last of which falls into `unimplemented!()` (`:4533`). The conclusion is unchanged and rests on addressing, not on keys: blocks come back **encrypted**, and naming them needs ObjectIds a non-holder does not have — the formulation `docs/nextgraph-current-state.md` § "The `Ext` protocol serves blocks with no control" already carried.)*
> **Addressing itself presupposes the cap.** Proving a document's existence without holding its key is not constructible today, and nothing indicates that it is planned.
@@ -258,16 +258,18 @@ A **wallet is only a keyring**. What we have been calling a "virtual user" is, u
### Giving access is a **Link** — one word, three places, all already named
**VERIFIED 2026-07-30.** The delivery message, the register and the record all exist upstream under the same word, which is what a shape being real looks like:
**VERIFIED 2026-07-30, state column corrected 2026-08-10.** The delivery message, the register and the record all exist upstream under the same word, which is what a shape being real looks like. What none of them is, is *implemented* — the table said so of three rows, and re-reading the source refuted it:
| Step | Upstream | State |
|---|---|---|
| The message deposited in the recipient's inbox | `InboxMsgContent::Link` (`engine/net/src/types.rs:4249-4261`) | **declared, payload-less** — a variant with no fields, i.e. specified and not implemented |
| Where the recipient files it on processing | `AddLink { read_cap }` on the **User branch** of the private store (`engine/repo/src/types.rs:1934-1950`) | implemented (verifier arm `commits/mod.rs:681`) |
| Withdrawing it | `RemoveLink`, ORset (`engine/repo/src/types.rs:1952`) | implemented |
| What circulates | `RepoLinkV0 { read_cap, … }` (`engine/net/src/types.rs:5062`) | implemented |
| Where the recipient files it on processing | `AddLink { read_cap }` on the **User branch** of the private store (`engine/repo/src/types.rs:1934-1950`) | **declared, stubbed** — the verifier arm is a no-op `Ok(())` (`engine/verifier/src/commits/mod.rs:681-693`), and nothing in the workspace constructs one |
| Withdrawing it | `RemoveLink`, ORset (`engine/repo/src/types.rs:1952`) | **declared, stubbed** — the same no-op `Ok(())` arm (`commits/mod.rs:695-707`) |
| What circulates | `RepoLinkV0 { read_cap, … }` (`engine/net/src/types.rs:5062-5078`) | **declared only** — zero constructors and zero consumers in the workspace (only the `RepoLink` wrapper and its two accessors, `:5082-5097`) |
So: **deposit a Link into the recipient's inbox; on connection the recipient processes the inbox and files it with `AddLink` on their User branch.** That is the whole gesture, and every piece of it has a name.
**What makes "stub" the right word rather than a quibble**: the neighbouring arm in the same file does real work. `CommitVerifier for AddRepo` calls `load_repo_from_read_cap` then `add_doc` (`commits/mod.rs:644-664`); `AddLink` and `RemoveLink`, twenty lines below, return `Ok(())` with `#[allow(unused_variables)]` on every parameter. Same trait, same file, opposite states — so "there is a verifier arm" cannot be read as "the register works".
So: **deposit a Link into the recipient's inbox; on connection the recipient processes the inbox and files it with `AddLink` on their User branch.** That is the whole gesture, and every piece of it has a **name** — which is not the same as having a behaviour. Per this document's own rule, none of that says what the target will do; it says the gesture is fully specified and none of it runs.
Two consequences worth stating, because both are easy to get wrong:
@@ -286,23 +288,25 @@ There IS a register, and it is a fourth commit type next to `AddRepo`:
pub struct AddLinkV0 { pub read_cap: ReadCap, /* … */ }
```
`engine/repo/src/types.rs:1934-1950`, with `RemoveLink` as its ORset counterpart (`:1952`) and a verifier arm at `engine/verifier/src/commits/mod.rs:681`. So:
`engine/repo/src/types.rs:1934-1950`, with `RemoveLink` as its ORset counterpart (`:1952`) and a verifier arm — a no-op `Ok(())` one — at `engine/verifier/src/commits/mod.rs:681-693`. So:
- it lives on the **User branch** — created only on the **private store** (`engine/repo/src/store.rs:448-452`; the public/protected stores get an `Overlay` branch instead), which also carries `AddInboxCap { repo_id, overlay, priv_key }`*"so that a user can share with all its device"* (`engine/repo/src/types.rs:1969-1981`). So the User branch answers two questions with one mechanism: **which caps I received**, and **which inboxes I may read**;
- it is explicitly for **external repos** — someone else's documents, exactly the received-cap case;
- and its stated purpose is to **share the link with all of the user's devices**. It is wallet-resident and cross-device, not a local cache.
**Level 3 (local user storage) is therefore a cache, not the register.** The register is level 2': `AddLink` on the User branch of the private store.
**Level 3 (local user storage) is therefore a cache, not the register.** The register — the durable, cross-device record — is level 2': `AddLink` on the User branch of the private store.
What remains true, and is a separate matter — the *delivery* path is unimplemented:
**But "the register exists, only the delivery is missing" is FALSE, and this section said it until 2026-08-10.** Both ends are declared and stubbed, as the table above now records: nothing constructs an `AddLink` commit anywhere in the workspace, and the arm that would apply one returns `Ok(())`. So the corrected statement is: `AddLink` on the User branch is **where a received cap belongs in the model** — a placement the source states unambiguously and this library aligns on — and no part of the gesture runs today, neither the road nor the destination. Per the design principle that gap says nothing about the target; it forbids only calling the register "implemented".
- `InboxMsgContent::ContactDetails` processing (`engine/verifier/src/inbox_processor.rs:778-847`) creates a contact document holding the profile, inbox, name and email — and **never reads `details.read_cap`**. Confirmed on sight: the receiver discards it. So the cap never reaches the User branch today — the register exists, the road to it does not.
The delivery half, in detail:
- `InboxMsgContent::ContactDetails` processing (`engine/verifier/src/inbox_processor.rs:778-847`) creates a contact document holding the profile, inbox, name and email — and **never reads `details.read_cap`**. Confirmed on sight: the receiver discards it. So no cap reaches the User branch today.
- `RepoLinkV0` states the intended flow (`engine/net/src/types.rs:5055-5061`): *"the link is shared and then the recipient opens it and subscribes soon afterward"*. **The key IS kept**: opening the repo persists its `read_cap` in local user storage, so the next session decrypts fine. What is not durable is the key's **validity** — a `RootCapRefresh` (§3) mints a new one, and receiving it depends on **the rotating party choosing to send it to you** (§3's DIRECTION block), not on any subscription state.
> **Do not write "only a subscriber receives the new key".** That reads the `RepoLinkV0` comment as intent, which §3 already forbids. **Subscribing is a purely LOCAL act** — automatic pull of changes — and the other party records nothing about it; there is no subscriber list to send to. Who gets a rotated key is the rotating party's decision, delivered to an inbox.
- `PermaCap` — still a **TODO** (`engine/repo/src/types.rs:578`) — covers exactly the gap that leaves: a link *"stored on disk and kept there unopened for a long period"*, i.e. never loaded, therefore never subscribed, therefore missing every refresh.
> **So there are TWO registers, by origin**: `AddRepo` on the **Store** branch for the documents a user creates in that store, and `AddLink` on the **User** branch of the private store for caps received for someone else's documents. Local user storage caches both. Opening a repo persists its cap locally, but that is the cache filling — not the durable record.
> **So there are TWO registers, by origin**: `AddRepo` on the **Store** branch for the documents a user creates in that store, and `AddLink` on the **User** branch of the private store for caps received for someone else's documents. (`AddRepo` runs; `AddLink` is declared and stubbed — see the table above. The *split by origin* is the model's, whatever each half's state.) Local user storage caches both. Opening a repo persists its cap locally, but that is the cache filling — not the durable record.
*Consequence for this library*: **both durable registers are now emulated** (2026-07-30) — `AddRepo` as a `shim:readCap` record on a distinct subject of the store document (`storeBranch`), `AddLink` as `shim:link` on another (`userBranch`) — and the in-memory `CapRegistry` is what it always was, level 3: the cache. Caps are READ back from those records, never recomputed. What stays an invention is representing branches as RDF subjects at all: upstream both branches carry `BranchCrdt::None` and hold service commits, not triples. What is faithful is that the key sits beside the document, and that the listing (`contains`, the Main branch) is separate from the keys.
+1 -1
View File
@@ -20,7 +20,7 @@ Without a minimum of crypto simulation, damaging shortcuts get taken (reading th
Concretely, **in the target**: a document's data is **stored encrypted** (per-doc symmetric encryption, however lightweight); the **ReadCap = the key**; without it, **decrypting/reading is impossible**. No ACL, no plaintext accessible "on the side". Obtaining read access = **holding the key**, exactly as in the target model.
> **Not yet true here, and saying so matters.** The shape is in place — possession decides, every access is confined to the connected virtual user, caps are stored and read back — but the cap value is the constant `OK` and nothing is encrypted. Per-document encryption is **P1b**, and it is one function (`nuri.ts` `mintCap`). Until it lands, nothing this library does may be described as anonymous or private.
> **Not yet true here, and saying so matters.** The shape is in place — possession decides, every access is confined to the connected virtual user, caps are stored and read back — but the cap value is the constant `OK` and nothing is encrypted. Per-document encryption is **P1b**, and it is one function (`mintCap`, in `emulated-verifier/caps.ts` — this said `nuri.ts` until 2026-08-10). Until it lands, nothing this library does may be described as anonymous or private.
## Shape consequences (to respect everywhere)
@@ -432,12 +432,22 @@ export async function readLinks(): Promise<ReadCap[]> {
* construction, and at any time (see the User-branch note above).
*
* What is true is narrower: no code path CREATES one for a document `new_store_default`
* attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None`
* (`repo.rs:574`), and the only two `AddInboxCap` commits in the engine are for the
* attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None`,
* and the only two `AddInboxCap` commits in the engine are for the
* public and protected STORE repos (`engine/verifier/src/site.rs:128,149`). So the
* capability exists and is simply unexposed above level 1: this function is aligned on
* the engine's model, it does not bet past it.
*
* *(The `inbox: None` claim is true; its citation was wrong until 2026-08-10. It pointed
* at `repo.rs:574`, inside `Repo::new_with_member` (`engine/repo/src/repo.rs:543`)
* a constructor reached only from `Repo::new_with_perms`, itself gated
* `#[cfg(any(test, feature = "testing"))]` (`repo.rs:186-192`), and from `#[cfg(test)]`
* blocks (`branch.rs:387,490`; `commit.rs:1659,1849,1919`). The PRODUCTION path is
* `doc_create` `Verifier::new_repo_default` (`engine/verifier/src/verifier.rs:3004`,
* called at `request_processor.rs:689`) `Store::create_repo_default`
* (`engine/repo/src/store.rs:264`) `create_repo_with_keys` (`store.rs:284`), which
* builds the `Repo` with `inbox: None` at `store.rs:691`.)*
*
* Lazy on purpose, for the same reason: creating an inbox document for every entity up
* front would double every `createEntityDoc` for inboxes most documents never receive
* anything in. Upstream the keypair is cheap; here an inbox is a document, so it is
@@ -472,9 +482,18 @@ export async function openDocumentInbox(docLike: NuriLike): Promise<Nuri> {
// false: that commit lands on the committer's OWN User branch, so anyone may write
// one naming anyone's repo. What protects upstream is that an inbox address is never
// PUBLISHED — it is TRANSMITTED (in a `ContactDetails` message, or a profile QR
// code), and `inboxes: PubKey → RepoId` is a per-verifier local table
// (`engine/verifier/src/verifier.rs:105`, rebuilt empty each session). A forged pair
// reaches nobody, because nobody was told about it.
// code), and `inboxes: PubKey → RepoId` is a table of the VERIFIER
// (`engine/verifier/src/verifier.rs:105`) — one per user. A forged pair reaches
// nobody because it only ever lands in the forger's OWN table; nobody else was told.
//
// The motive matters, and it was wrong here until 2026-08-10: this comment said the
// table is "rebuilt empty each session", which is not what the source does. It is
// initialized empty at construction (`:520`, `:2820`) and then REPOPULATED at every
// load — `Verifier::load` (`:534-566`) → `add_repo_without_saving` (`:2871`) →
// `add_repo_` (`:2887`), which re-inserts `repo.inbox.to_pub() → repo.id` for each
// repo it reloads — and the inbox private key itself is persisted per repo
// (`INBOX_CAP`, `engine/verifier/src/user_storage/repo.rs:61,171,207,362`). So the
// knowledge is durable; what it is not is SHARED. Per-verifier, not ephemeral.
//
// We publish instead of transmitting — the only way a third party can find the
// address at all here — which creates a vector upstream does not have: whoever can
@@ -1,26 +1,44 @@
/**
* public-store a document in a PUBLIC store gives up its ReadCap to whoever asks.
*
* The upstream mechanism this emulates (VERIFIED)
* `PublicRepoLinkV0` (`engine/net/src/types.rs:5098-5124`) carries `repo`,
* `public_store` and `peers` and **no `read_cap`**. Its own doc comment says why:
* The upstream mechanism this emulates a DECLARED model, so a BET
* **Labelled VERIFIED until 2026-08-10, wrongly.** What supports it is a doc COMMENT
* on a type nothing constructs a statement of intent, not of behaviour and this
* repo's own rules say both halves of that: a comment describing the current state is
* not the intent, and an absent implementation is not evidence either. So this is a
* bet, and `docs/document-links.md` § 5 and `docs/readcap-and-nuri-model.md` § 4sexies
* already called it one. This header now says the same word.
*
* What IS read in source: `PublicRepoLinkV0` (`engine/net/src/types.rs:5098-5124`)
* carries `repo`, `public_store` and `peers` and **no `read_cap`**. Its own doc
* comment says why:
*
* > *"The latest ReadCap of the branch (or main branch) will be **downloaded from
* > the outerOverlay**, if the peer brokers listed below allow it. [] This link is
* > durable, because the public site are **served differently by brokers**."*
* > the outerOverlay**, **if the peer brokers listed below allow it**. [] This link
* > is durable, because the public site are **served differently by brokers**."*
*
* So for a repo in a public store, the key is not something a sender hands over: it is
* something the **network gives to anyone who asks**. The broker decides, by pinning
* the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`).
* That is the whole of the property nothing about the reader, everything about where
* the document sits and how brokers serve it.
* something the **network gives to whoever asks and whom the peer brokers allow**.
* That condition is part of the mechanism, not decoration: the broker decides, by
* pinning the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`).
* Nothing about the reader; everything about where the document sits and how brokers
* serve it.
*
* And what is NOT wired, which is precisely why this is a bet: both `PinRepo`
* constructors hard-code `expose_outer: false`
* (`engine/net/src/actors/client/pin_repo.rs:51,79`), so no client ever asks for the
* exposure; and `ExtTopicSyncReq` the anonymous branch-sync such a link needs is
* declared and falls into `unimplemented!()` (`engine/net/src/types.rs:4523,4533`).
* The emulation follows the model the engine DECLARES, in a place the engine does not
* yet serve. That is this library's intended posture, named here as the bet it is.
*
* What that means for the model, and why nothing is special-cased
* Possession stays the ONE criterion. A public document is readable not because the
* guard makes an exception for it, but because its cap is **obtainable**: you ask, you
* receive, you hold it, and from there the ordinary path applies. `reach.ts` is
* untouched, and "whoever has the reference AND the key reads" still describes
* everything a public store simply hands the key to whoever has the reference.
* everything a public store hands the key to whoever has the reference, where the
* brokers serving that store allow it (see the condition above).
*
* The consequence an application must be able to rely on: **a bare reference to a
* document in a public store is enough**, and that is why nothing in this library
+17 -1
View File
@@ -46,7 +46,23 @@
// At migration the build alias is removed and these resolve to the real SDK. The
// per-symbol ruling, with its epistemic label, is in `docs/api-contract.md`.
export * from "./model/types";
// A type is published only when a PUBLISHED SIGNATURE uses it. `export *` published
// eight in one gesture (2026-08-10: it was a blanket re-export), of which two named
// nothing a consumer can reach — `ReadCap` (used only by two private helpers of
// `surface/inbox.ts`) and `InboxScope` (used only by the unpublished
// `account-registry.userInbox`). A published type with no published signature is a
// promise about the target that nothing here keeps: it invites a consumer to hold a
// value it has no call to obtain — and for `ReadCap`, the one value the model says a
// caller must never be handed on request. They stay DEFINED in `model/types.ts`, where
// the library uses them; they stop being surface. `docs/api-contract.md` § 10, § 14.
// Each one, and the signature that earns it its place:
// Nuri every reference the surface RETURNS
// NuriLike every reference the surface ACCEPTS
// Scope `storeRegistry.*`, `watchShape`
// PrincipalId `ensureIdentity`, `inbox.Deposit`/`PostOptions`, `EventuallyConfig`
// NgLike `EventuallyConfig.ng`
// UseShapeLike `EventuallyConfig.useShape`
export type { Nuri, NuriLike, Scope, PrincipalId, NgLike, UseShapeLike } from "./model/types";
export { useShape } from "./surface/use-shape";
export { watchShape } from "./surface/watch-shape";
export type { ShapeQuery, ShapeObservable } from "./surface/watch-shape";
+15 -9
View File
@@ -1,10 +1,13 @@
/**
* NURI primitives the cap-less / cap-bearing distinction, kept as ONE object.
*
* Upstream a NURI is a single type, `NuriV0 { target, access }`: a cap-less NURI
* simply has an empty `access`. `did:ng:` is the URI SCHEME prefix (inboxes,
* branches and overlays all carry it) it does NOT mean "without cap". The
* discriminant is the `:r:` segment:
* Upstream a NURI is a single type, `NuriV0` TEN fields: `identity, target,
* entire_store, objects, signature, branch, overlay, access, topic, locator`
* (`engine/net/src/app_protocol.rs:181-194`) and a cap-less NURI is simply one
* whose `access` is empty. This module transcribes **two** of those ten (`target`,
* and the cap half of `access`); the other eight have no counterpart here.
* `did:ng:` is the URI SCHEME prefix (inboxes, branches and overlays all carry it)
* it does NOT mean "without cap". The discriminant is the `:r:` segment:
*
* did:ng:o:{doc}:v:{overlay} names, does NOT read (a {@link Nuri})
* did:ng:o:{doc}:v:{overlay}:r:{cap} names AND reads (a {@link ReadCap})
@@ -19,9 +22,11 @@
* 2026-07-30; it was the wrong letter *and* the wrong structure.
*
* These helpers are INTERNAL to the library. The parsed form {@link parseNuri}
* mirrors `NuriV0 { target, access }` 1:1 but never surfaces in the SDK-identical
* entry's signatures the real SDK takes plain `String`s and enforces at runtime,
* through cryptography, so no branded type and no parsed struct leaks outward.
* mirrors that PAIR `target` and the cap and not the type: it was described as a
* "1:1 mirror of `NuriV0`" until 2026-08-10, which claimed eight fields it has never
* carried. It never surfaces in the SDK-identical entry's signatures either the
* real SDK takes plain `String`s and enforces at runtime, through cryptography, so no
* branded type and no parsed struct leaks outward.
*
* The stand-in key (deliberately NOT a secret)
* This library is deliberately insecure (see docs/vision.md). The only question it
@@ -80,8 +85,9 @@ export function targetOf(nuri: Nuri): Nuri {
}
/**
* The parsed form a 1:1 mirror of upstream `NuriV0 { target, access }`, where a
* cap-less NURI has no `readCap`. Library-internal (see the module header).
* The parsed form upstream `NuriV0`'s `target` plus the cap half of its `access`,
* and none of the type's eight other fields; a cap-less NURI has no `readCap`.
* Library-internal (see the module header).
*/
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap } {
return hasReadCap(nuri) ? { target: targetOf(nuri), readCap: nuri } : { target: nuri };
+7 -3
View File
@@ -292,9 +292,13 @@ function capOfPayload(payload: unknown): ReadCap | null {
* - the field exists, `ContactDetails.read_cap: Option<ReadCap>`
* (`engine/net/src/types.rs:4233`), but building a message that carries one is
* `read_cap: if with_readcap { unimplemented!() }` (`types.rs:3786`);
* - and the receiver ignores it: `InboxMsgContent::ContactDetails` writes only
* `ng:site`/`ng:protected` + `ng:*_inbox` into a fresh contact document
* (`engine/verifier/src/inbox_processor.rs:778-830`), never `details.read_cap`.
* - and the receiver ignores it: `InboxMsgContent::ContactDetails` creates a fresh
* contact document and writes `ng:site`/`ng:protected` + `ng:*_inbox`, a
* `vcard:Individual` type, a `vcard:fn` name and an optional `vcard:hasEmail`,
* then sets the header title (`engine/verifier/src/inbox_processor.rs:778-845`)
* but never `details.read_cap`. *(The list was "only the two `ng:` predicates"
* until 2026-08-10, which understated what the arm writes; the load-bearing part
* is the omission, not the length of the list.)*
*
* Do NOT read `InboxMsgContent::Link` as the intended channel either: it is a **unit
* variant carrying nothing** (`engine/net/src/types.rs:4251`).