# API contract — what `@ng-eventually/client` exposes today, and what the future SDK should expose per subject > **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises. **Scope: the APP-FACING contract only.** Everything reachable from the two published entry points, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15. **Scope.** The real exported surface of `@ng-eventually/client` (verified against the `export` statements in `packages/client/src/index.ts` and `packages/client/src/polyfill.ts` — `package.json` maps exactly two entry points, `.` and `./polyfill`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`). **How to read the epistemic labels.** Every target-side claim carries one of: - **PASSTHROUGH (level 3 / level 2, VERIFIED)** — the target function exists today; the lib forwards to it. Citation into `nextgraph-rs` or the installed `.d.ts`. Level numbers per `README.md` § *The three references*: 3 = JS ORM (`sdk/js/orm`), 2 = wasm binding / `@ng-org/web` (`sdk/js/lib-wasm`, `sdk/js/web`), 1 = Rust engine (`engine/`). - **LEVEL-1 SHAPE (model VERIFIED, JS surface ASSUMED)** — the engine's model constrains the shape and is cited, but **no JS surface exists at any level**, so the signature offered here is this library's invention. The future SDK's name and parameter order for it are unknown. - **ASSUMPTION** — nothing at any layer constrains this; the bet and what bounds it are stated. - **NO COUNTERPART** — the subject has no image in the target at any layer, usually because it is shared-wallet machinery that disappears at migration. That is a finding about the emulation, not a gap in the target. Per the design principle (`README.md` § *Design principle*): an absent implementation is never treated as evidence about the future — "the engine does not do X" and "the SDK will not offer X" are kept apart throughout. --- ## 1. Bootstrap and configuration ### Today — `@ng-eventually/client/polyfill` (everything here is removed at migration) ```ts // polyfill.ts:44 export interface EventuallyConfig { ng: NgLike; useShape: UseShapeLike; sharedWallet?: { name: string; secret: string }; currentUser?: PrincipalId; debugAccessLog?: boolean; init?: (...args: any[]) => any; initNg?: (...args: any[]) => any; } // polyfill.ts:99 export function configure(c: EventuallyConfig): void; // polyfill.ts:113 — tests only export function resetConfig(): void; // polyfill.ts:24 export interface StoreRegistryDeps { getSession: () => Promise; normalizeId?: (id: string) => string; pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number }; } // polyfill.ts:124 export function configureStoreRegistry(deps: StoreRegistryDeps): void; // polyfill.ts:161 — tests only export function resetStoreRegistry(): void; // polyfill.ts:106 / :153 — both tagged @internal, exported so the SDK-shaped wrappers can reach the injected SDK export function getConfig(): EventuallyConfig; export function getStoreRegistryDeps(): ResolvedRegistryDeps; ``` ### Target **NO COUNTERPART, by design.** The whole subject is the polyfill bootstrap: it exists to inject the real SDK without a hard import (build-alias safety). At migration the consumer initializes the real SDK directly, with the two calls in § 2, and `configure` / `configureStoreRegistry` are deleted (`docs/migration-guide.md` § 7). Nothing in the target takes an "injected `ng`". --- ## 2. Lifecycle ### Today — `@ng-eventually/client` ```ts // lifecycle.ts:11 — forwards to the real @ng-org/web init injected at configure() export function init(...args: any[]): any; // lifecycle.ts:18 — forwards to the real @ng-org/orm initNg injected at configure() export function initNg(...args: any[]): any; ``` ### Target **PASSTHROUGH, VERIFIED at both levels.** The wrapper's `...args: any[]` is deliberately shapeless; the real signatures it forwards to are: ```ts // level 2 — @ng-org/web: index.d.ts:108, source sdk/js/web/src/index.ts:51 export declare const init: (callback: Function | null, singleton: boolean, access_requests: any) => Promise; // level 3 — @ng-org/orm: sdk/js/orm/src/connector/initNg.ts:51 (exported as initNg from core.ts) export function initNgSignals(ngImpl: NG, session: Session): void; // level 2 — the Session initNg consumes: index.d.ts:264-272 export declare type Session = { session_id: string | number; protected_store_id: string; private_store_id: string; public_store_id: string; ng: typeof NGModule; [key: string]: unknown; }; ``` Divergence: none in behaviour (pure forwarding), but the wrapper erases the parameter types. A consumer typing calls against the wrapper learns nothing it must unlearn — it just gets no compile-time help the real SDK would give. --- ## 3. The `ng` object ### Today — `@ng-eventually/client` ```ts // index.ts:55 export const ng: Record; // type re-export, index.ts:50 export type { NG } from "@ng-org/web"; ``` `ng` is a `Proxy` (`ng-proxy.ts:10`) forwarding every property to the injected real `ng`, overriding exactly two things: `login` / `session_start` (currently a passthrough with a TODO for shared-wallet credentials) and `sparql_update` (the emulated write-cap guard, rejecting a write when a write policy governs the anchored document and the current user lacks the cap). ### Target **PASSTHROUGH (level 2, VERIFIED).** `export declare const ng: NG` with `NG = typeof NGModule`, 77 exported members (`index.d.ts:136-231`). The surface is identical by construction — the proxy adds no member and removes none. The two overrides: - `session_start(wallet_name: string, user_id: any): Promise` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears. - `sparql_update(session_id: any, sparql: string, nuri: any): Promise` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until P1b. --- ## 4. Reactive typed reads — `useShape` ### Today — `@ng-eventually/client` ```ts // use-shape.ts:12 export function useShape(shapeType: unknown, scope: unknown): unknown; // type re-exports, index.ts:48-49 export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm"; export type { DeepSignalSet } from "@ng-org/alien-deepsignals"; ``` Behaviour: forwards to the injected real `useShape`; once any emulated cap exists (`caps.isEnforcing()`), the returned set is wrapped in a read-filtered view keeping only items whose document cap the current holder has. ### Target **PASSTHROUGH (level 3, VERIFIED), with a signature the wrapper widens.** The real hook: ```ts // level 3 — @ng-org/orm/react: sdk/js/orm/src/frontendAdapters/react/useShape.ts:86-124 const useShape = ( shape: ShapeType, scope: Scope | string | undefined ) => DeepSignalSet; // its Scope — sdk/js/orm/src/model/types.ts:25-38 (NOT this lib's Scope, see § 12) export type Scope = { graphs?: string[] | string; subjects?: string[]; }; ``` The read filter disappears at migration: in the target, isolation is cryptographic — a repo whose cap the wallet does not hold is never decrypted, a union read over it yields nothing, and a targeted read errors `RepoNotFound` (`engine/verifier/src/request_processor.rs:155,163` via `resolve_target`). VERIFIED at level 1; the *consumer-visible* result (you only see what you hold) is the same, which is the point of the emulation. Divergence to note: the wrapper types everything `unknown`, losing the generic `T`. A consumer wanting typed sets today must cast; at migration the real generic signature gives it back. Nothing to unlearn, only ergonomics deferred. --- ## 5. Reactive typed reads with load state — `watchShape` ### Today — `@ng-eventually/client` ```ts // watch-shape.ts:73 export interface ShapeQuery { data: T[]; isPending: boolean; isSuccess: boolean; isError: boolean; error: unknown; } // watch-shape.ts:90 export interface ShapeObservable { getSnapshot(): ShapeQuery; subscribe(onChange: () => void): () => void; refetch(): void; } // watch-shape.ts:166 export function watchShape( shapeType: unknown, scope: Scope, ): ShapeObservable; ``` ### Target **Partly ASSUMPTION — flagged deliberately.** `surface/watch-shape.ts`'s header says it "anticipates NextGraph's planned `useShape(shape, scope)` upgrade, which will natively distinguish 'sync in progress' from 'synced but empty'". **No provenance for that plan exists in this repo's docs or in the `nextgraph-rs` clone** — treat the "planned upgrade" as an assumption, not a stated NextGraph direction. What IS verified at level 3 is that the distinction is *expressible* today, just not through the hook: ```ts // level 3, VERIFIED — sdk/js/orm/src/connector/GraphOrmSubscription.ts:228,260,274 OrmSubscription.getOrCreate(shape: ShapeType, scope: NormalizedScope): OrmSubscription; get readyPromise(): Promise; // resolves when the subscription is synced — the native "no longer pending" signal public close(): void; ``` So the constraint on the bet: the target can already answer "synced?" (`readyPromise`), and `useShape` today returns "an empty set, if still loading" (its own doc comment, `useShape.ts:29-31`) — indistinguishable from synced-empty. `watchShape` surfaces the distinction with a TanStack-`useQuery`-minimal vocabulary (`isPending`/`isSuccess`/`isError`), which is a **shape of this library's choosing**. If the future hook exposes load state under different names, the consumer's binding code changes; the underlying distinction it teaches (pending ≠ empty) is target-expressible and safe to learn. --- ## 6. One-shot listing — the read-model ### Today — `@ng-eventually/client` ```ts // read-model.ts:59 export interface UnionSubject { subject: string; graph: string; props: Record; } // read-model.ts:140 export async function readUnion(docs: Nuri[]): Promise; ``` Behaviour: one anchored `sparql_query` per doc (default-graph body, no `GRAPH` wrapper), parallel, per-doc failure tolerance, cap filter applied inside, machinery subjects dropped. ### Target Two verified counterparts, one per level; neither returns `UnionSubject` — that grouping is lib-invented: ```ts // level 2, VERIFIED — the primitive readUnion composes: index.d.ts:295, source sdk/js/lib-wasm/src/lib.rs:352 (nodejs) / :555 (web) declare function sparql_query(session_id: any, sparql: string, base: any, nuri: any): Promise; // level 3, VERIFIED — the one-shot typed read: sdk/js/orm/src/connector/getObjects.ts:23 export async function getObjects( shapeType: ShapeType, scope: Scope | string ); // returns a deep-cloned Set of matching objects ``` The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query to that repo's graph as default graph (`resolve_target_for_sparql`, `engine/verifier/src/request_processor.rs:256-285`), an anchorless query unions every named graph in the session store (same function, `UserSite → None` → `set_default_graph_as_union`). At migration `readUnion` survives as composition (the anchored per-doc read is native); a consumer that wants typed results should be on `useShape`/`getObjects`, not on `UnionSubject` — the property-bag shape is a polyfill artifact, kept generic precisely so the consumer maps it into its own types and can drop it later. --- ## 7. Raw document / SPARQL primitives — `docs.*` ### Today — `@ng-eventually/client` (namespace `docs`) ```ts // docs.ts:46 export async function docCreate( sessionId: string, crdt: string, cls: string, dest: string, store?: unknown, ): Promise; // docs.ts:85 export async function sparqlUpdate( sessionId: string, query: string, anchor?: Nuri, label = "sparqlUpdate", ): Promise; // docs.ts:130 export async function sparqlQuery( sessionId: string, query: string, base?: string, anchor?: Nuri, label = "sparqlQuery", ): Promise; // docs.ts:113 — machinery, see § 15 export async function depositInto( sessionId: string, query: string, targetInbox: Nuri, label = "deposit", ): Promise; ``` ### Target **PASSTHROUGH (level 2, VERIFIED)** — these mirror the real methods 1:1 minus the trailing `label` (a lib-internal access-log tag, never forwarded): ```ts // index.d.ts:60 — the installed web SDK's doc_create declare function doc_create(session_id: any, crdt: string, class_name: string, destination: string, store_repo: any): Promise; // index.d.ts:297 declare function sparql_update(session_id: any, sparql: string, nuri: any): Promise; // index.d.ts:295 declare function sparql_query(session_id: any, sparql: string, base: any, nuri: any): Promise; ``` `depositInto` has **NO COUNTERPART as a SPARQL write**: upstream a deposit is a sealed message, not an update into the recipient's graph (§ 9). It exists only because the emulated inbox is an RDF document. **Store targeting — finer than "not JS-constructible".** *(The other docs were corrected on 2026-08-03 to match this entry; they used to state the blanket form.)* Verified in the clone: - The **web** wasm variant (`sdk/js/lib-wasm/src/lib.rs:1575`, `#[cfg(not(wasmpack_target = "nodejs"))]`) deserializes its 5th argument as `Option` via serde — so a value CAN be passed, but no JS helper exists to build the serde form, which keeps it out of practical reach. The published `.d.ts` documents this 5-arg form. - The **nodejs** variant (`lib.rs:1618`, 6 args) takes `store_type: Option` + `store_repo: Option` and builds the store via `StoreRepo::from_type_and_repo(store_type, repo_id_str)` with `store_type ∈ "public" | "protected" | "private" | "group"` (`sdk/rust/src/local_broker.rs:2969-2987`, `engine/repo/src/types.rs:819-828`). So the target's direction for scope placement is **already visible in the source** (level 2, VERIFIED, nodejs SDK): name the store by type + repo id strings. The migration-guide's anticipated `getNativeStore(scope)`-style resolver should expect to produce exactly that pair (or the serde `StoreRepo` once a web helper lands) — not a new concept. --- ## 8. Per-document subscription — `subscribeDoc` ### Today — `@ng-eventually/client` ```ts // subscribe.ts:47,60,79 export type DocChange = unknown; export type DocChangeType = string | undefined; export type Unsubscribe = () => void; // subscribe.ts:69 export function docChangeType(resp: DocChange): DocChangeType; // subscribe.ts:104 export function subscribeDoc( nuri: Nuri, onChange: (r: DocChange, type: DocChangeType) => void, ): Unsubscribe; // subscribe.ts:184 export function subscribeDocs( nuris: Nuri[], onChange: (nuri: Nuri, r: DocChange, type: DocChangeType) => void, ): Unsubscribe; ``` ### Target **PASSTHROUGH (level 2, VERIFIED) with two deliberate ergonomic deltas:** ```ts // index.d.ts:66, source sdk/js/lib-wasm/src/lib.rs:1908 declare function doc_subscribe(repo_o: string, session_id: any, callback: Function): Promise; ``` - The real call is `async` and resolves to an unsubscribe function; the wrapper returns the unsubscribe **synchronously** and honours an early cancel when the promise settles. A consumer coding against the sync return will keep working against the real SDK only through an adapter — a small, known unlearn, traded for not forcing `await` on every subscription site. - The real callback receives one argument, the serialized `AppResponse` (`{ V0: { State | Patch | TabInfo | … } }`); the wrapper adds a second, pre-extracted `type`. `docChangeType` is a convenience over the verified payload shape (pinned by the e2e CONTRACT-3 probe), not an upstream API. - `subscribeDocs` has **NO COUNTERPART and needs none**: it is client-side composition (a set of `doc_subscribe` with per-doc error isolation). The upstream fan-out primitive that looks like it (`orm_start_graph(graph_scope, …)`, `index.d.ts:243`) aborts wholesale on one `RepoNotFound` (`sdk/js/orm` → `engine/verifier/src/request_processor.rs:53-66`) — the reason this composition exists. --- ## 9. Inbox — deposits, and cap delivery ### Today — `@ng-eventually/client` (namespace `inbox`; `shareCap` also re-exported from `/polyfill`) ```ts // inbox.ts:48,58 export interface Deposit { from: PrincipalId | null; payload: unknown; ts: number; } export interface PostOptions { from?: PrincipalId | null; payload: unknown; ts?: number; } // inbox.ts:140 export async function post(targetInbox: Nuri, opts: PostOptions): Promise; // inbox.ts:215 export async function postToDocument(doc: Nuri, opts: PostOptions): Promise; // inbox.ts:282 export async function shareCap(cap: ReadCap, toInbox: Nuri): Promise; // inbox.ts:339 export async function read(targetInbox: Nuri): Promise; // inbox.ts:419 export const materialize = read; // inbox.ts:441 export async function readSynced(targetInbox: Nuri): Promise; // inbox.ts:466 export async function processInbox(targetInbox: Nuri): Promise; // inbox.ts:492 export function watch( targetInbox: Nuri, onDeposits: (deposits: Deposit[]) => void, _opts?: { intervalMs?: number }, ): () => void; ``` ### Target **LEVEL-1 SHAPE throughout — the model is VERIFIED, every JS signature here is this library's invention.** There is no inbox method in `@ng-org/web` (none in the 77 `index.d.ts` exports, re-verified), and the verifier's dispatch has no `InboxPost` arm (arms actually handled listed at `engine/verifier/src/request_processor.rs:53-1444`, re-verified). The engine model that constrains the shape: - An inbox is a keypair on **exactly one repo**: `pub inbox: Option` (`engine/repo/src/repo.rs:126`); routing is `inboxes: HashMap` on the verifier (`engine/verifier/src/verifier.rs:105`, looked up at `:1677`, inserted at `:1928`). - A message is sealed to the inbox pubkey and carries **no target document** — `InboxMsgBody { to_overlay, to_inbox: PubKey, from_overlay: Option, from_inbox: Option, … }` (`engine/net/src/types.rs:4265`). The address identifies the recipient repo; nothing else is needed. This is why `Deposit` has no document field and why `post` takes only the inbox NURI. - `from` optional upstream (`from_inbox: Option`) — the "identified if known, anonymous otherwise" behaviour `PostOptions.from` mirrors, including the `null`-means-anonymous case. - The recipient's own verifier unseals and **applies** queued messages when it processes its inbox (`engine/verifier/src/verifier.rs:1674-1690` → `process_inbox`); an inbox is a consumed queue, not a store you re-read. Consequences per function: - `post` / `postToDocument` — the sender-side act exists in the model (the broker routes `InboxPost` natively, `engine/net/src/server_broker.rs`); its JS surface does not. **The future SDK's name and signature are unknown** — `docs/nextgraph-current-state.md:187` records that nothing is announced. `postToDocument`'s resolution step (find the document's inbox address) rides on a **deliberate divergence**: this lib PUBLISHES the address on the document (Header-branch emulation), whereas upstream an address is only ever TRANSMITTED (`ContactDetails` carries `ng:site_inbox`/`ng:protected_inbox`, `engine/verifier/src/inbox_processor.rs:778-830`; the verifier's `inboxes` table is session-local, rebuilt empty — `verifier.rs:520,2820`). Documented in `docs/briefs/2026-08-03-document-inbox-addressing.md`. - `shareCap` — a **gap upstream, not a disagreement**, verified at both ends: `ContactDetails.read_cap: Option` exists (`engine/net/src/types.rs:4233`) but building a message with it is `unimplemented!()` (`types.rs:3786`), its only caller passes `with_readcap: false`, and the receiving arm never reads the field (`inbox_processor.rs:778-830`). `InboxMsgContent::Link` is a **unit variant carrying nothing** (`types.rs:4252`) — do not read it as the delivery channel. The recipient-side filing the lib emulates is real: `AddLink { read_cap }` on the User branch (`engine/repo/src/types.rs:1939-1948`). The consumer's *act* (share one document's cap to one inbox) is target-shaped; only the transport is emulated. - `read` / `materialize` / `readSynced` / `processInbox` / `watch` — **stand-ins for the recipient's own verifier processing**, which has no consumer-facing JS surface upstream and may never have this list-of-deposits shape. A consumer should treat "my inbox gets processed when I connect, and applied caps just appear in what I hold" as the durable contract (that is what `connectedUser` automates, § 13); code that leans on enumerating raw deposits as a mailbox UI is coding against emulation detail it may have to unlearn. The consumer-payload case (`Deposit.payload` as app data) maps to `InboxMsgContent` variants upstream (`types.rs:4249-4260`), of which only `ContactDetails` and `SocialQuery` are more than unit variants today — arbitrary app payloads through the inbox are an **ASSUMPTION**, constrained by the model only in that messages are sealed, per-recipient, and applied by the recipient. - `watch`'s `_opts?: { intervalMs?: number }` is accepted and **ignored** (kept for signature compatibility with a removed polling watcher) — dead surface, see § 15. --- > **Known divergence, low impact today — `inbox.share` always deposits on the recipient's PROTECTED inbox.** Upstream the choice follows the profile through which the person was reached (`a_or_b = if details.profile.is_public() { "site" } else { "protected" }`, `engine/verifier/src/inbox_processor.rs:787`). This library has no notion of "which profile I know this person by", so it picks one. It flattens a distinction the model makes; it will be wrong the day an application shares with someone met through a public profile. Recorded rather than fixed, because the fix needs a notion nothing here has established — note that the `Identity` enum that would name it is entirely commented out upstream (`engine/repo/src/types.rs:586-595`), so there is no profile model to read yet. ## 10. Capabilities — possession, not ACL ### Today ```ts // @ng-eventually/client — nuri.ts:50,60 (type guards; the only doors from string to typed) export function isNuri(s: string): s is Nuri; export function hasReadCap(s: string): s is ReadCap; // @ng-eventually/client — types.ts:11,34 export type Nuri = `did:ng:${string}`; export type ReadCap = `did:ng:${string}:r:${string}`; // @ng-eventually/client/polyfill — polyfill.ts:205 export function capFor(nuri: Nuri): ReadCap | undefined; // polyfill.ts:193 — hands out the registry itself export function getCaps(): CapRegistry; // polyfill.ts:215 — tests / fresh wallet only export function resetCaps(): void; // @ng-eventually/client/polyfill — caps.ts:59 (class CapRegistry) constructor(holder?: () => PrincipalId | null); mint(nuri: Nuri): ReadCap; learn(cap: ReadCap): void; capFor(nuri: Nuri): ReadCap | undefined; publishRepoLink(nuri: Nuri): ReadCap; isPublished(nuri: Nuri): boolean; open(nuri: Nuri, scope: Scope): ReadCap; isEnforcing(): boolean; onChange(listener: () => void): () => void; grantWrite(doc: Nuri, principal: PrincipalId): void; // decorative until P1b governsWrite(doc: Nuri): boolean; // decorative until P1b canWrite(doc: Nuri, principal: PrincipalId | null): boolean; // decorative until P1b hasWritePolicy(): boolean; // decorative until P1b clear(): void; ``` ### Target **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). - 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. `capFor(nuri)` asks the only question the model admits — "do I hold this document's key?" — and returning `undefined` is the whole possible answer. There is no "may principal P read D?" anywhere, and the future SDK cannot offer one without inventing an ACL the engine does not have. That absence is a **finding about the target's model**, not a missing feature: a consumer should never expect a cap-introspection API. The `CapRegistry` class itself is machinery (the in-memory record of what the connected holder holds — upstream's local user storage). The consumer-facing surface is `capFor` + the acts (`shareCap`, creating a document, processing one's inbox); see § 15. --- ## 11. NURI and SPARQL string utilities ### Today — `@ng-eventually/client` ```ts // sparql.ts:32,66,95 export function escapeLiteral(value: string): string; export function escapeIri(value: string): string; export function assertNuri(nuri: T): T; ``` (`isNuri` / `hasReadCap` are in § 10; `targetOf`, `parseNuri`, `mintCap` exist in `nuri.ts` but are **not** exported from either entry point — deliberately: nothing on the surface turns a bare reference into a cap.) ### Target **NO COUNTERPART at any level, and none expected.** Neither `@ng-org/web` nor the ORM exposes SPARQL escaping helpers (re-verified against `index.d.ts` and `sdk/js/orm/src`); the engine does its own ad-hoc literal escaping internally where it builds SPARQL (e.g. `update_header`, `engine/verifier/src/request_processor.rs:196-208`). These are generic injection-safety utilities, not SDK anticipation: they stay useful to any app that builds SPARQL by interpolation, against this lib or the real SDK. Nothing to unlearn; also nothing that migration replaces. --- ## 12. Scope resolution, per-entity documents, and the store registry ### Today — `@ng-eventually/client` (namespace `storeRegistry`) — plus `Scope` from `types.ts` > **Narrowed 2026-08-03.** The entry used to re-export the WHOLE `store-registry` module. It now re-exports an app-facing slice (`src/surface/placement.ts`): `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `userInbox`, `openDocumentInbox`, `documentInboxAddress`. The rest — `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `resolveAccount`, `ensureAccount`, `reservedAccount`, `resetRegistryCache`, and the `VirtualUserRecord` / `RegistrySession` types — is **no longer importable from `@ng-eventually/client`** and is covered by `docs/internal-contract.md`. The signatures below are kept for the record, marked accordingly. ```ts // types.ts:38 — NB: NOT the ORM's Scope (a graphs/subjects filter); this is the store scope export type Scope = "public" | "protected" | "private"; // store-registry.ts:90,234 export interface VirtualUserRecord { id: string; docPublic: Nuri; docProtected: Nuri; docPrivate: Nuri; } export interface RegistrySession { sessionId: string; privateStoreId: string; protectedStoreId?: string; publicStoreId?: string; } // consumer-facing, designed to survive migration (store-registry.ts:917, 1358, 1079, 735, 698) export async function createEntityDoc(id: string, scope: Scope): Promise; export async function listMyEntityDocs(id: string, scope: Scope): Promise; export async function userStoreDoc(id: string, scope: Scope): Promise; export async function resolveScopeGraph(scope: Scope): Promise; export async function resolveWriteGraph(id: string, scope: Scope): Promise; // inbox-side (store-registry.ts:772, 837, 1133, 1203, 1286) export async function userInbox(id: string): Promise; export async function isOwnInbox(nuri: Nuri): Promise; export async function openDocumentInbox(doc: Nuri): Promise; export async function documentInboxAddress(doc: Nuri): Promise; export async function myInboxes(): Promise; // User-branch registers (store-registry.ts:1307, 1331) export async function addLink(cap: ReadCap): Promise; export async function readLinks(): Promise; // shim machinery (store-registry.ts:542, 631, 213, 278) export async function resolveAccount(id: string): Promise; export async function ensureAccount(id: string): Promise; export function reservedAccount(name: string): string; export function resetRegistryCache(): void; ``` ### Target — split by what each piece maps to - **`createEntityDoc(id, scope)` → level 2, VERIFIED direction.** Target: `doc_create(session_id, crdt, class_name, destination, store_repo)` aimed at the identity's real per-scope store (see § 7 for the store-targeting nuance — the nodejs SDK already takes `store_type`/`store_repo` strings). The two writes the lib performs by hand are **native side effects** of `doc_create` upstream: the `ldp:contains` listing on the store's Main branch and the `AddRepo { read_cap }` on its Store branch (`engine/verifier/src/request_processor.rs:697-710`). The `id` parameter disappears (the session IS the identity); expect `createEntityDoc(id, scope)` to become `doc_create(sid, …, storeOf(scope))` with no listing/cap bookkeeping. - **`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. - **`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. --- ## 13. Identity and connection ### Today ```ts // @ng-eventually/client — virtualUsers.ts (namespace accounts) export const ACCOUNT_STORAGE_KEY = "ng-eventually.account.id"; // :18 export interface VirtualUserStorage { // :26 getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; } export class IdentityStore { // :37 constructor(storage: VirtualUserStorage | null, key?: string); get(): string | null; set(id: string): string | null; clear(): void; } export function browserIdentityStore(key?: string): IdentityStore; // :89 // @ng-eventually/client/polyfill export function setCurrentUser(id: PrincipalId | null): void; // polyfill.ts:171 export function getCurrentUser(): PrincipalId | null; // polyfill.ts:187 export async function connectedUser(): Promise; // connect.ts:52 ``` ### Target **PASSTHROUGH-to-be at level 2, VERIFIED signatures.** In the target the identity is established by opening one's own wallet and starting a per-user session — there is no "set the current user" call because the session IS the user: ```ts // index.d.ts:276, 280, 313, 315 declare function session_start(wallet_name: string, user_id: any): Promise; declare function session_stop(user_id: string): Promise; declare function user_connect(client_info: any, user_id: string, location?: string | null): Promise; declare function user_disconnect(user_id: string): Promise; ``` - `virtualUsers.*` (the persisted identity id) — **NO COUNTERPART**; removed at migration (`docs/migration-guide.md` § 5). It exists only because every virtual user shares one wallet. It is exported from the SDK entry, which is a placement wart (§ 15). - `setCurrentUser` / `getCurrentUser` — **NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet. - `connectedUser()` — the awaitable form of what the target does **automatically**: the recipient's verifier processes its inbox as messages arrive/at connection (`Verifier::inbox`, `engine/verifier/src/verifier.rs:1674`). VERIFIED at level 1 that no consumer call is needed upstream; the polyfill fires it from `setCurrentUser` for the same reason. A consumer should treat it as "await a deterministic start" (tests), not as an operation the future SDK will name. --- ## 14. Type re-exports `@ng-eventually/client` re-exports, type-only (erased at build, `index.ts:48-50`): ```ts export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm"; export type { DeepSignalSet } from "@ng-org/alien-deepsignals"; 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. --- ## 15. Machinery on the surface — what a consumer should NOT code against Exported, but not SDK surface. Coding against these builds knowledge that migration deletes: - **`docs.depositInto`** — the named boundary-crossing write `inbox.post` uses. It is exported only because `inbox.ts` lives in another module; a consumer must always go through `inbox.post` / `inbox.shareCap`. Upstream a deposit is a sealed message, not a SPARQL update — this function's very signature is emulation. - **`getConfig` / `getStoreRegistryDeps`** — tagged `@internal` in source, exported for the lib's own wrappers. - **`resetConfig` / `resetStoreRegistry` / `resetCaps` / `storeRegistry.resetRegistryCache`** — test/reset machinery. In particular `resetCaps` wipes EVERY holder's caps, which no product flow should ever do. - **`getCaps()` and the `CapRegistry` class** — the registry is the emulation's engine room. The consumer surface is `capFor` (possession lookup), `inbox.shareCap` (grant), and the acts that file caps implicitly (creating a document, processing one's inbox). `CapRegistry.grantWrite` / `governsWrite` / `canWrite` / `hasWritePolicy` are explicitly decorative until P1b — the guard they feed is bypassed by every internal writer. - ~~**`storeRegistry.reservedAccount`, `resolveAccount`, `ensureAccount`, `VirtualUserRecord`, `RegistrySession`**~~ — **RESOLVED 2026-08-03**: no longer exported. Shim internals, now in `docs/internal-contract.md`. The consumer's legitimate touchpoint is `configureStoreRegistry` (bootstrap) plus the scope/entity resolvers. - ~~**`storeRegistry.addLink` / `readLinks`**~~ — **RESOLVED 2026-08-03**: no longer exported. Consumers receive caps by processing their inbox (automated at connection); calling these directly baked in a register the verifier owns upstream. - ~~**`virtualUsers.*` on the SDK entry**~~ — **RESOLVED 2026-08-03**: moved to `/polyfill`, where its disappearance at migration is visible at the import line. - **`inbox.watch`'s `_opts?: { intervalMs?: number }`** — accepted and ignored (no polling exists). Dead compatibility surface; do not pass it. - **The `label` parameters** on `docs.sparqlUpdate` / `docs.sparqlQuery` / `docs.depositInto` — lib-internal access-log tags, never forwarded to `ng`. The real signatures have no such parameter. ### Places the current surface teaches something to unlearn - ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `watchShape`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery". - **`shareCap` is importable from both entries** (`inbox.shareCap` on the SDK entry via `export * as inbox`, and a named re-export on `/polyfill`). The polyfill re-export exists "so the cap vocabulary stays on the polyfill side" — but the namespace export undoes that. Harmless functionally; blurs the same boundary. - **`inbox.read`/`materialize` as a mailbox** — enumerating raw deposits is emulation detail (§ 9); the durable contract is deposit-and-it-gets-applied. An app building UI on the deposit list should expect that surface to change shape entirely. - **`watchShape`'s "planned `useShape` upgrade"** — stated in the module header with no provenance in this repo or the clone (§ 5). The load-state *distinction* is safe; the claim that NextGraph plans this exact hook shape is an assumption and must not be cited as an announced API. - **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `watchShape`'s design already assumes). - **The sync-returning `subscribeDoc` unsubscribe** vs the target's promise-resolved one (§ 8) — a deliberate, documented ergonomic delta; an adapter is one line at migration, but it is a delta. --- ## Appendix — full export inventory (for diffing) *Generated from the `export` statements, and pinned by `packages/client/test/vocabulary.test.ts` — if this list and the code disagree, that test fails. It went stale once, still listing `storeRegistry`'s shim internals after the entry had been narrowed, which is what a hand-maintained inventory does.* ### `@ng-eventually/client` — `src/index.ts` ```text direct: BaseType, DeepSignalSet, DocChange, DocChangeType, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape docs: depositInto, docCreate, sparqlQuery, sparqlUpdate inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph ``` ### `@ng-eventually/client/polyfill` — `src/polyfill.ts` ```text direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, hasCap, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, share ```