diff --git a/.project/concepts/app-contract/contract_sdk-surface.md b/.project/concepts/app-contract/contract_sdk-surface.md index 329174d..adfa764 100644 --- a/.project/concepts/app-contract/contract_sdk-surface.md +++ b/.project/concepts/app-contract/contract_sdk-surface.md @@ -36,11 +36,12 @@ export interface EventuallyConfig { export async function ensureIdentity(): Promise; // 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"; +export type Scope = "public" | "protected" | "private"; // ── placement: where an application's documents live ───────────────────── export const storeRegistry: { // no identity parameter — the session is one user's diff --git a/docs/api-contract.md b/docs/api-contract.md index fd668c5..40d3e4f 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -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` 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` (`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` 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. @@ -468,8 +479,11 @@ 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 +// surface/sparql.ts escapeLiteral, escapeIri, assertNuri +// 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; - **`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` (`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` (`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; ## 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 diff --git a/docs/document-links.md b/docs/document-links.md index 2a0748b..dfefc02 100644 --- a/docs/document-links.md +++ b/docs/document-links.md @@ -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` — 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` — 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 diff --git a/docs/internal-contract.md b/docs/internal-contract.md index 88e9a03..da852c2 100644 --- a/docs/internal-contract.md +++ b/docs/internal-contract.md @@ -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. diff --git a/docs/migration-guide.md b/docs/migration-guide.md index 55b8332..8fca97c 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -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 diff --git a/docs/nextgraph-current-state.md b/docs/nextgraph-current-state.md index 5a0f1d4..87ae494 100644 --- a/docs/nextgraph-current-state.md +++ b/docs/nextgraph-current-state.md @@ -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` is a field of EVERY `Repo` (`engine/repo/src/repo.rs:126`), not of a store structure. `AddInboxCapV0` is keyed by diff --git a/docs/readcap-and-nuri-model.md b/docs/readcap-and-nuri-model.md index 7d03d15..c20f943 100644 --- a/docs/readcap-and-nuri-model.md +++ b/docs/readcap-and-nuri-model.md @@ -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, 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. diff --git a/docs/vision.md b/docs/vision.md index 8d14825..cfb60d5 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -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) diff --git a/packages/sdk/src/emulated-verifier/branch-registers.ts b/packages/sdk/src/emulated-verifier/branch-registers.ts index ccbfed5..a1cc428 100644 --- a/packages/sdk/src/emulated-verifier/branch-registers.ts +++ b/packages/sdk/src/emulated-verifier/branch-registers.ts @@ -432,12 +432,22 @@ export async function readLinks(): Promise { * 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 { // 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 diff --git a/packages/sdk/src/emulated-verifier/public-store.ts b/packages/sdk/src/emulated-verifier/public-store.ts index d267776..21dfcbc 100644 --- a/packages/sdk/src/emulated-verifier/public-store.ts +++ b/packages/sdk/src/emulated-verifier/public-store.ts @@ -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 diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 2ceff2e..81d2f83 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -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"; diff --git a/packages/sdk/src/model/nuri.ts b/packages/sdk/src/model/nuri.ts index da896d2..47030cc 100644 --- a/packages/sdk/src/model/nuri.ts +++ b/packages/sdk/src/model/nuri.ts @@ -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 }; diff --git a/packages/sdk/src/surface/inbox.ts b/packages/sdk/src/surface/inbox.ts index 2d7ea15..4ae5d89 100644 --- a/packages/sdk/src/surface/inbox.ts +++ b/packages/sdk/src/surface/inbox.ts @@ -292,9 +292,13 @@ function capOfPayload(payload: unknown): ReadCap | null { * - the field exists, `ContactDetails.read_cap: Option` * (`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`).