0eb25286c8
Deux mouvements de surface, aucun changement de comportement. **`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.** « client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans `docs/source-layout-by-fate.md` et le tableau des paquets du README. **Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types — vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`. Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de `docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par `test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de vocabulaire sur les noms publiés. **Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le choix visible plutôt qu'hérité : - `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par `shared-wallet/bootstrap` ; - `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes par leur chemin interne, ce qui est leur raison d'être ; - le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux fois brouillait la frontière qu'elle servait à marquer. Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` / `hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait `capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire. 179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
272 lines
31 KiB
Markdown
272 lines
31 KiB
Markdown
# Internal contract — what `@ng-eventually/sdk` keeps off its surface, and what NextGraph does or would do about each 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 complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/sdk/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.` → `src/index.ts` and `./polyfill` → `src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` 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 the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07; its polyfill-era symbols — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` and their types — now sit in a marked block of the single entry, and the machinery accessors and test resets it used to publish are no longer published at all.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
|
|
|
|
**Labels** are those of `docs/api-contract.md`: **PASSTHROUGH (level 3/2, VERIFIED)**, **LEVEL-1 SHAPE (model VERIFIED, JS surface ASSUMED)**, **ASSUMPTION**, **NO COUNTERPART**. Level numbers per `README.md` § *The three references*: 3 = JS ORM, 2 = wasm binding (`@ng-org/web`), 1 = Rust engine. One label recurs here that the surface contract rarely needs: **NO COUNTERPART, shared-wallet machinery** — the code below the emulation's floor, which the target has no image of because the target has no shared wallet. Per the design principle, an absent implementation is never treated as evidence about the future.
|
|
|
|
---
|
|
|
|
## 1. The wrapped `ng` factory — `surface/ng-proxy.ts`
|
|
|
|
```ts
|
|
// ng-proxy.ts:10
|
|
export function makeNg(): Record<string, any>;
|
|
```
|
|
|
|
Builds the published `ng` Proxy (consumed once, `index.ts:61`): forwards every property to the injected real `ng`, overriding `login`/`session_start` (passthrough with a shared-wallet-credentials TODO) and `sparql_update` (the emulated write-cap guard).
|
|
|
|
- The factory itself is **NO COUNTERPART, by design** — the target has no "wrap the SDK" step; at migration `ng` IS `@ng-org/web`'s and `makeNg` is deleted.
|
|
- The `sparql_update` guard stands in for the engine's write-permission model (`verify_perm` inside `Commit::verify`, `engine/repo/src/commit.rs:892-899`) — same analysis as `docs/api-contract.md` § 3.
|
|
- **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`
|
|
|
|
```ts
|
|
// nuri.ts:73
|
|
export function targetOf(nuri: Nuri): Nuri;
|
|
// nuri.ts:82
|
|
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap };
|
|
// nuri.ts:116
|
|
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` / `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).
|
|
|
|
## 3. The reach boundary — `emulated-verifier/reach.ts`
|
|
|
|
```ts
|
|
// reach.ts:66
|
|
export function declareInfrastructure(nuri: Nuri): void;
|
|
// reach.ts:71
|
|
export function isInfrastructure(nuri: Nuri): boolean;
|
|
// reach.ts:76
|
|
export function resetInfrastructure(): void;
|
|
// reach.ts:93
|
|
export function mayReach(nuri: Nuri): boolean;
|
|
// reach.ts:110
|
|
export function assertMayReach(nuri: Nuri, op: string): void;
|
|
// reach.ts:131
|
|
export function mustNotAttempt(nuri: Nuri): boolean;
|
|
```
|
|
|
|
The single predicate deciding whether the CONNECTED virtual user may touch a document at all: cap possession, or explicitly-declared infrastructure (the store-root and doc-shim). `assertMayReach` guards the passage points (rule 1, throw on refusal); `mustNotAttempt` guards the callers (rule 2, do not even issue the operation). Inert until the first cap exists (`caps.isEnforcing()`).
|
|
|
|
- **NO COUNTERPART, shared-wallet machinery — the emulated stand-in for the wallet boundary itself.** In the target the boundary is cryptographic, not a predicate: a repo whose cap the wallet does not hold is never decrypted, a targeted read of it errors `RepoNotFound` (`resolve_target_for_sparql`, `engine/verifier/src/request_processor.rs:264,269`), and the only path that loads a repo from a cap is `pub(crate)` (`Verifier::load_repo_from_read_cap`, `engine/verifier/src/verifier.rs:2237`). The two-rules split (refuse at the gate AND do not attempt) is redundancy this lib chose; upstream only "cannot" exists — there is nothing to refuse because the request cannot be formed.
|
|
- The infrastructure exemption (`declareInfrastructure`, registered by `shared-wallet/account-registry.ts` for the store-root and doc-shim only) has **no image in the target**: there is no shim to exempt. Registration-not-pattern-matching is a lib-internal safety choice.
|
|
- Everything here disappears at migration; the durable lesson it protects (naming a document does not grant access) is the target's own model.
|
|
|
|
## 4. The physical user's primitives — `shared-wallet/physical.ts`
|
|
|
|
```ts
|
|
// physical.ts:54
|
|
export async function physicalCreate(sessionId: string, crdt = "Graph", cls = "data:graph", dest = "store", store?: unknown): Promise<Nuri>;
|
|
// physical.ts:80
|
|
export async function physicalQuery(sessionId: string, query: string, base: string | undefined, anchor: Nuri, label = "physicalQuery"): Promise<unknown>;
|
|
// physical.ts:94
|
|
export async function physicalUpdate(sessionId: string, query: string, anchor: Nuri, label = "physicalUpdate"): Promise<void>;
|
|
```
|
|
|
|
The unguarded counterparts of `docs.docCreate` / `sparqlQuery` / `sparqlUpdate`, callable only by the library's own machinery on the shim's documents (store-root pointer, doc-shim, provisioning). Separated as FUNCTIONS rather than as an exemption list so machinery never gets "waved through" a guard (module header, `physical.ts:19-27`).
|
|
|
|
- As wire calls: **PASSTHROUGH (level 2, VERIFIED)** — the same `doc_create` / `sparql_query` / `sparql_update` the published `docs.*` forwards to (`index.d.ts:60,295,297`; sources `sdk/js/lib-wasm/src/lib.rs:1575` web / `:1618` nodejs, `:352`/`:555`), minus the lib-internal `label`.
|
|
- As a CONCEPT: **NO COUNTERPART, shared-wallet machinery.** The physical/virtual user split exists only because one wallet hosts many identities; the target has exactly one user per wallet and no privileged "machinery caller". The module disappears with the shim.
|
|
|
|
## 5. Physical subscription — the unexported slice of `subscribe.ts`
|
|
|
|
```ts
|
|
// subscribe.ts:118
|
|
export function subscribePhysicalDoc(nuri: Nuri, onChange: (r: DocChange, type: DocChangeType) => void): Unsubscribe;
|
|
```
|
|
|
|
`subscribeDoc` minus the reach guard — the machinery's door to `doc_subscribe`, used by `emulated-verifier/open-repo.ts` to hold shim repos open. Same wire behaviour as the published `subscribeDoc` (analysed in `docs/api-contract.md` § 8, target `doc_subscribe`, `index.d.ts:66`, `sdk/js/lib-wasm/src/lib.rs:1908`).
|
|
|
|
- **NO COUNTERPART, shared-wallet machinery** — the guarded/unguarded pair collapses to one call when the wallet is the boundary. Disappears with `shared-wallet/physical.ts`.
|
|
|
|
## 6. Bootstrap repo opening — `emulated-verifier/open-repo.ts`
|
|
|
|
```ts
|
|
// open-repo.ts:75
|
|
export type SyncState = "syncing" | "synced" | "timed-out";
|
|
// open-repo.ts:104 — TEST-ONLY
|
|
export function setOpenTimeoutForTests(ms: number): void;
|
|
// open-repo.ts:110
|
|
export function resetOpenedRepos(): void;
|
|
// open-repo.ts:135
|
|
export function getSyncState(nuri: Nuri): SyncState | "unknown";
|
|
// open-repo.ts:167
|
|
export async function ensureRepoOpen(nuri: Nuri): Promise<void>;
|
|
// open-repo.ts:184
|
|
export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise<void>;
|
|
// open-repo.ts:259
|
|
export async function ensureReposOpen(nuris: Nuri[]): Promise<void>;
|
|
```
|
|
|
|
Heals the cold-start defect of the anchored read path: on a fresh session a not-yet-open repo reads empty, so before an anchored read the repo is opened by subscribing (`subscribePhysicalDoc`) and awaiting the first `State` push — the sync barrier — with a bounded timeout. The subscription is held for the session; per-nuri `SyncState` keeps `synced` and `timed-out` apart.
|
|
|
|
- The opening mechanism is **level 2, VERIFIED as a composition**: `doc_subscribe` exists (`sdk/js/lib-wasm/src/lib.rs:1908`), and the push variants `TabInfo`/`State`/`Patch` are the engine's `AppResponseV0` (`engine/net/src/app_protocol.rs:1354-1358`). The ORDER (TabInfo first, then the initial State) and "first State = presence guaranteed, absence definitive" are **empirical, pinned by the in-repo e2e CONTRACT-3 probe — an ASSUMPTION about ordering as far as upstream is concerned**: no upstream statement fixes the push order, so a future reordering upstream would silently break the barrier. Bound: the e2e probe fails loudly if the order changes.
|
|
- "Hold a live subscription to keep the repo open" — **ASSUMPTION** (nothing upstream documents subscription lifetime as what retains a repo in `self.repos`); observed to work, bounded by the same probe.
|
|
- **Defect — the header's mechanism claim is contradicted at the source (see Findings F2).** `open-repo.ts:10-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::error` (`:1293-1296`), and the web binding REJECTS the JS promise with it (`sdk/js/lib-wasm/src/lib.rs:606`). The observed 0-rows-no-error behaviour has two candidate explanations that the source does support: a persistent verifier reloads every known repo into `self.repos` at `Verifier::load` (`engine/verifier/src/verifier.rs:535-560`) so the repo is present-but-unsynced (a genuine 0-row read), and/or the lib's own per-doc tolerance (`readUserStore`, `surface/read-model.ts` per-doc catch) converts a rejection into an empty result. The healed symptom is real and the fix correct; the stated mechanism is not established, and a maintainer reasoning from it would mispredict behaviour whenever the verifier is not persistent.
|
|
- `SyncState` and `getSyncState` are lib-invented vocabulary — **NO COUNTERPART** (upstream has no consumer-facing "sync state of a repo" API at any level; `OrmSubscription.readyPromise`, `sdk/js/orm/src/connector/GraphOrmSubscription.ts:260`, is the closest level-3 signal, per-subscription not per-repo).
|
|
- At migration the whole module becomes "open the store by cap at bootstrap" (native) and is removed with the shim.
|
|
|
|
## 7. The read filter — `emulated-verifier/read-filter.ts`
|
|
|
|
```ts
|
|
// read-filter.ts:47
|
|
export function filterReadable<T>(items: Iterable<T>, caps: CapRegistry): T[];
|
|
// read-filter.ts:60
|
|
export function makeReadFilteredView<S extends object>(set: S, caps: CapRegistry): S;
|
|
```
|
|
|
|
The polyfill of capability-based read access: a Proxy view over the reactive set keeping only items whose `@graph` document the current holder holds; applied by `surface/use-shape.ts` once `caps.isEnforcing()`.
|
|
|
|
- **NO COUNTERPART, by design — it stands in for cryptographic non-delivery.** In the target the broker/verifier simply never yields what the wallet holds no cap for (targeted read errors, `request_processor.rs:264,269`; union read yields nothing for undecrypted repos — the § 4 analysis of `docs/api-contract.md`). There is no post-hoc filter to migrate to; the module is deleted.
|
|
- The `@graph` key it filters on is **level 3, VERIFIED**: the ORM annotates every object with its graph NURI (`sdk/js/orm/src/frontendAdapters/react/useShape.ts:41`, `sdk/js/orm/src/model/types.ts:19`). Items with no `@graph` are kept — a lib policy choice (they name no document), not an upstream rule.
|
|
- Access unit = the DOCUMENT, not the item — faithful to the model (a ReadCap opens a repo, `types.rs:518-521`), and the reason the filter is all-or-nothing per document.
|
|
|
|
## 8. Connection trigger — the unexported slice of `emulated-verifier/connect.ts`
|
|
|
|
```ts
|
|
// connect.ts:91
|
|
export function startConnect(): void;
|
|
```
|
|
|
|
Fire-and-forget wrapper over the published `connectedUser()` (restore Links, then drain every inbox), called by `setCurrentUser` so inbox processing is the library's job, not the app's.
|
|
|
|
- **LEVEL-1 SHAPE for the timing, VERIFIED**: upstream the recipient's verifier processes inbox messages as they arrive, with no consumer call (`Verifier::inbox` → `process_inbox`, `engine/verifier/src/verifier.rs:1674-1690`); firing on connection is the emulation's equivalent moment. The restore-before-drain order is a lib choice; upstream "restore" does not exist as a step (applied caps are already in the User branch replay).
|
|
- `startConnect` itself disappears at migration; the automatic-processing behaviour it fabricates is native.
|
|
|
|
## 9. The shim registry — the unexported slice of `shared-wallet/account-registry.ts`
|
|
|
|
The sharpest boundary case: `surface/placement.ts` publishes the 7 app-facing calls; the 9 exports below stay internal (importable by the lib's modules, unit tests and the e2e harness, not by an application through the package entries). The type `RegistrySession` is published by the entry (a consumer types its injected `getSession` with it) and covered by the surface contract; `VirtualUserRecord` is internal.
|
|
|
|
### 9a. Account shim — provision, resolve, reserved names, cache
|
|
|
|
```ts
|
|
// store-registry.ts:213
|
|
export function reservedAccount(name: string): string;
|
|
// store-registry.ts:278
|
|
export function resetRegistryCache(): void;
|
|
// store-registry.ts:542
|
|
export async function resolveAccount(id: string): Promise<VirtualUserRecord | null>;
|
|
// store-registry.ts:631
|
|
export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
|
```
|
|
|
|
`resolveAccount` — barrier-authoritative O(1) lookup of one account's record in the doc-shim; `ensureAccount` — resolve-or-provision (creates the three scope docs on first sight, concurrency-deduped); `reservedAccount` — NUL-prefixed sentinel namespace for lib-internal accounts; `resetRegistryCache` — test/wallet-switch reset.
|
|
|
|
- **NO COUNTERPART, shared-wallet machinery — the whole group.** The target has no directory of identities to resolve or provision: a user's site (three stores + their inboxes) is created once at wallet creation (`engine/verifier/src/site.rs` — the site-creation flow committing the stores and the two store-inbox `AddInboxCap`s at `:128,149`), and "which user" is the session. `ensureAccount`'s provision-on-first-sight has no target analogue and is exactly what `connectedUser` refuses to trigger (`connect.ts:60-65`). All of it disappears with the shim (`docs/migration-guide.md` § 3).
|
|
- `reservedAccount`'s collision-safety rests on an **ASSUMPTION about a consumer-injected function**: the comment (`store-registry.ts:200-206`) asserts the injected `normalizeId` can never produce a U+0000-prefixed key, but `normalizeId` is injected by the consumer and the lib's own default is a bare `trim()` (`shared-wallet/bootstrap.ts`), which does not strip U+0000. Bound: a consumer id would have to begin with a literal NUL to collide — implausible from any UI, but the property is the consumer's to keep, not the lib's (see Findings F5).
|
|
|
|
### 9b. Scope-index resolution
|
|
|
|
```ts
|
|
// store-registry.ts:1079
|
|
export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri>;
|
|
```
|
|
|
|
The scope-INDEX document of one account (what `watchShape` subscribes to for container changes). Same resolution as the published `resolveWriteGraph`, without doubling as a write target.
|
|
|
|
- **Level 2, VERIFIED counterpart with the indirection removed**: in the target the container IS the store, and its id is on the session (`Session.private_store_id | protected_store_id | public_store_id`, `index.d.ts:264-272`); the listing a subscriber watches is the store's `ldp:contains` graph, written natively by `doc_create` (`request_processor.rs:706-708`). The function's question survives; the per-account parameter and the index-document indirection disappear.
|
|
|
|
### 9c. Inbox ownership predicates
|
|
|
|
```ts
|
|
// store-registry.ts:837
|
|
export async function isOwnInbox(nuri: Nuri): Promise<boolean>;
|
|
// store-registry.ts:1286
|
|
export async function myInboxes(): Promise<Nuri[]>;
|
|
```
|
|
|
|
`isOwnInbox` — the read guard's question (may the current identity READ this inbox); `myInboxes` — the drain list for `connectedUser` (own wallet inbox + one per document this user opened an inbox on).
|
|
|
|
- **LEVEL-1 SHAPE for the underlying record, VERIFIED; NO COUNTERPART as API.** Upstream "which inboxes may I read" is answered inside the verifier by the User branch's `AddInboxCap` records (`AddInboxCapV0 { repo_id, overlay, priv_key }`, `engine/repo/src/types.rs:1969-1981`, applied at `verifier.rs:1916-1932`) and consulted implicitly when a message arrives (`verifier.rs:1674-1690`); nothing suggests a JS API for the question at any level. Both functions exist only because the emulation must ANSWER it in JS (the read guard, the drain); they disappear with the emulated inbox read side.
|
|
|
|
### 9d. The durable Link register
|
|
|
|
```ts
|
|
// store-registry.ts:1307
|
|
export async function addLink(cap: ReadCap): Promise<void>;
|
|
// store-registry.ts:1331
|
|
export async function readLinks(): Promise<ReadCap[]>;
|
|
```
|
|
|
|
File / read back a received cap on the emulated User branch of the private store — what makes a received cap durable across sessions.
|
|
|
|
- **LEVEL-1 SHAPE, model VERIFIED; no JS surface anywhere.** The record emulated is `AddLink { read_cap }` on the User branch — *"so that a user can share with all its device a new Link they received"*, external repos only (`engine/repo/src/types.rs:1934-1950`). Upstream the filing happens inside the verifier while processing the inbox; the future SDK most likely never exposes these as calls (the surface contract's § 12 finding, restated here because these are now internal-only: the one caller is `emulated-verifier/connect.ts` / `inbox.ts`, which is exactly where upstream's verifier sits).
|
|
- At migration both are deleted; the verifier files and replays.
|
|
|
|
## 10. The machinery namespace — `emulated-verifier/machinery.ts`
|
|
|
|
```ts
|
|
// machinery.ts:32
|
|
export const MACHINERY_NS = "urn:ng-eventually:";
|
|
// machinery.ts:40
|
|
export function isMachinerySubject(subject: string | undefined): boolean;
|
|
```
|
|
|
|
The URN prefix every triple the lib writes for itself lives under, and the one predicate read paths use to keep machinery out of consumer data.
|
|
|
|
- **NO COUNTERPART, by design — the seam where the emulation pays for having no branches.** Upstream the separation is structural: a compartment is a different BRANCH with its own CRDT and topic (`BranchType`, `engine/repo/src/types.rs:1536-1551`; the Store/User/Overlay branches carry no triples at all, `BranchCrdt::None`, `types.rs:1420`), so machinery cannot appear in a content read and no subject filter exists to write. The namespace, the filter, and the four `shim:*` compartment subjects it protects all disappear at migration.
|
|
|
|
## 11. Diagnostics — `shared-wallet/access-log.ts` and `shared-wallet/outbox-log.ts`
|
|
|
|
```ts
|
|
// access-log.ts:25,45,50,63,76,87,99,116
|
|
export type AccessOp = "READ" | "WRITE";
|
|
export function setAccessLog(on: boolean): void;
|
|
export function enabled(): boolean;
|
|
export function activeIdentity(): string;
|
|
export function accessLogPrefix(): string;
|
|
export function logStage(line: string): void;
|
|
export function shortNuri(nuri: string): string;
|
|
export function logAccess(op: AccessOp, nuri: string, label: string, extra?: string): void;
|
|
|
|
// outbox-log.ts:62
|
|
export function inspectOutbox(): void;
|
|
```
|
|
|
|
`shared-wallet/access-log.ts` — the off-by-default per-identity access trace for the shared-wallet isolation leak (toggled by `configure({ debugAccessLog })` or `NG_EVENTUALLY_ACCESS_LOG=1`). `shared-wallet/outbox-log.ts` — a read-only count of the real SDK's offline write outbox at session bootstrap, warning when non-empty.
|
|
|
|
- `shared-wallet/access-log.ts` — **NO COUNTERPART, shared-wallet machinery**: the leak it makes visible cannot exist in the target (isolation is per-wallet), and the "active identity" it prefixes is the relayed virtual id that disappears with `setCurrentUser`. Deleted at migration.
|
|
- `shared-wallet/outbox-log.ts` — **NO COUNTERPART as API, but every fact it relies on is level-2 VERIFIED** in the clone: the outbox is persisted through `JsStorageConfig` (`sdk/rust/src/local_broker.rs:89-100`), keyed `ng_peer_last_seq@<peerId>` (`:119,141`) and `ng_outboxes@<peerId>@start` / `@<idx>` with zero-padded `{:05}` indexes (`:163-213`, pad at `:183,210`); the real `outbox_read_function` DRAINS on read (`session_del` per key plus the start key, `:218-224`) — which is why the probe only counts and never touches; and the storage callbacks land in browser `sessionStorage` (`sdk/js/api-web/main.ts:47,57,66`), whose access-denied error string is the one `convert_error` handles (`main.ts:18-22`). The probe reads a private persistence format of the injected SDK — acknowledged in its header as out-of-contract, hence count-only. Deleted with the rest of the trace instrumentation at migration.
|
|
|
|
---
|
|
|
|
## Findings — defects and migration risks
|
|
|
|
**F1 — `surface/ng-proxy.ts` fabricates a `login` member the real SDK does not have.** `ng-proxy.ts:16-22` intercepts `prop === "login"`, but `@ng-org/web` exports no `login` (none in `index.d.ts`, re-verified against the full `declare function` list; no `fn login` in `sdk/js/lib-wasm/src/lib.rs`). On the wrapper `ng.login` is a function; on the real SDK it is `undefined`; calling it throws. This contradicts the module's own "surface stays identical" header and `docs/api-contract.md` § 3's "the proxy adds no member and removes none". No target layer names a `login` — the arm is an unprovenanced assumption. Cheap fix: drop the `login` case (keep `session_start`), or gate it on `typeof ng.login === "function"`.
|
|
|
|
**F2 — `emulated-verifier/open-repo.ts`'s stated mechanism is contradicted at the source.** The header (`open-repo.ts:10-12`) asserts an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Upstream, absence from `self.repos` errors `RepoNotFound` (`engine/verifier/src/request_processor.rs:264,269`), the ReadQuery arm wraps it as `AppResponse::error` (`:1293-1296`), and the web binding rejects the JS promise (`sdk/js/lib-wasm/src/lib.rs:606`). The observed behaviour is real but its cause is one (or both) of: the repo WAS in `self.repos` (a persistent verifier reloads all known repos at `Verifier::load`, `engine/verifier/src/verifier.rs:535-560`) and read 0 rows because unsynced; or the lib's own catch-and-continue layers absorbed the rejection. The fix (open before reading) is correct either way; the diagnosis in the header should not be relied on, and mispredicts non-persistent-verifier behaviour.
|
|
|
|
**F3 — incomplete citation in `subscribe.ts`.** `subscribe.ts:31` cites the ORM fan-out abort as "`initialize.rs:125-128`" with no path. The file is `engine/verifier/src/orm/graph/initialize.rs`; lines 125-128 are the graph loop calling `self.open_for_target(&nuri.target, true).await?` — verified, the `?` propagates `RepoNotFound` and aborts the whole subscription. Substance correct; the bare filename is unfindable without this note.
|
|
|
|
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts` routes through `surface/placement.ts`) those are internal. That file is being edited concurrently; noted here, deliberately not fixed by this document.
|
|
|
|
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`shared-wallet/bootstrap.ts`), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on `StoreRegistryDeps.normalizeId`, or reject NUL-prefixed raw ids at `accountKey`.
|
|
|
|
**Migration-risk flags (shapes that will not travel):**
|
|
|
|
- **The `(document, inbox)` pair is persisted as a space-joined string literal** (`"${doc} ${inbox}"`, written `store-registry.ts:1174`, parsed by `split(" ")` at `:1268`). Upstream the record is the typed `AddInboxCapV0 { repo_id, overlay, priv_key }` (`engine/repo/src/types.rs:1969-1981`). Internal-only and replaced wholesale at migration, but it is the one shim record with an ad hoc micro-format a future reader must know to parse.
|
|
- **`isOwnInbox` / `myInboxes`** encode questions the target answers only inside the verifier (§ 9c) — any new internal caller added to them deepens a dependency that has no successor API; keep callers to the read guard and the connection drain.
|
|
- **The sync barrier is empirical** (§ 6): "TabInfo before the first State" and "held subscription keeps the repo open" are pinned by the in-repo e2e probe, not by any upstream statement. If upstream changes push ordering or repo retention, `emulated-verifier/open-repo.ts` is the module that breaks first; the probe is the tripwire.
|
|
- **`ensureAccount`'s provision-on-first-sight** (§ 9a) is a behaviour with no target image; `emulated-verifier/connect.ts` already refuses to trigger it. Any future internal path that provisions as a side effect of resolving would be teaching the emulation something the target contradicts (creation is an explicit act at wallet/site creation, `engine/verifier/src/site.rs`).
|
|
|
|
---
|
|
|
|
## Appendix — full internal export inventory (for diffing)
|
|
|
|
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`).
|
|
|
|
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.
|