fix: quatre écarts entre la surface publiée et ce que NextGraph déclare
Un audit de la surface contre la source amont en a trouvé cinq ; voici les
quatre mécaniques. La cinquième — l'adresse d'inbox, qui traverse sept symboles
— relève du dessin et reste ouverte.
L'identifiant de session bloquait. Amont le déclare string | number
(sdk/js/web/src/index.ts:16) et le binding désérialise un u64 ; nous exigions
une chaîne. Une application ne pouvait donc pas passer la valeur que le SDK
venait de lui remettre. Élargi à ce qu'amont déclare, sur toute la chaîne, et
jamais converti : une chaîne échoue pour de vrai (Deserialization error of
session_id JsValue("1"), observé).
sparqlUpdate annonçait Promise<void> alors qu'il relayait DÉJÀ les commits.
C'était donc un mensonge de typage, pas un comportement — et la doublure de test
qui rendait undefined, un état que le vrai broker ne produit jamais, est ce qui
l'a laissé sans contradicteur.
ng était publié en Record<string, any>, ce qui perdait les 88 membres typés
d'amont — 88, pas 77 : le chiffre de notre propre documentation était faux.
Et materialize, second nom publié de read, sans appelant ni contrepartie amont,
est retiré.
docs/api-contract.md qualifiait docs.* de passthrough « 1:1 ». C'était faux sur
les deux premiers points. Corrigé, pas complété : un document qui se déclare
vérifié et qui ment est pire qu'un document absent, parce qu'on cesse d'aller
voir.
Une déviation assumée : amont type le retour en any, interdit ici ; on rend
unknown, comme sparqlQuery le fait déjà pour le même any amont.
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
# Doc-debt — app-contract
|
||||||
|
|
||||||
|
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||||
|
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||||
|
|
||||||
|
## Raw markers (consolidate into blocks, then delete)
|
||||||
|
- TOUCHED packages/polyfill/src/surface/docs.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/surface/read-model.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/surface/inbox.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/surface/subscribe.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/surface/ng-proxy.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/index.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED docs/api-contract.md @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
@@ -61,9 +61,14 @@ export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: Doc
|
|||||||
|
|
||||||
// ── low-level document / SPARQL primitives ───────────────────────────────
|
// ── low-level document / SPARQL primitives ───────────────────────────────
|
||||||
export const docs: {
|
export const docs: {
|
||||||
docCreate(sessionId: string, crdt: string, cls: string, dest: string, store?: unknown): Promise<Nuri>;
|
// `sessionId` is `string | number` — upstream's own declared type (`Session.session_id`).
|
||||||
sparqlQuery(sessionId: string, query: string, base?: string, anchor?: NuriLike, label?: string): Promise<unknown>;
|
// It is RELAYED, never converted: the wasm side deserializes a `u64`, and stringifying it
|
||||||
sparqlUpdate(sessionId: string, query: string, anchor?: NuriLike, label?: string): Promise<void>;
|
// fails for real (`Deserialization error of session_id JsValue("1")`).
|
||||||
|
docCreate(sessionId: string | number, crdt: string, cls: string, dest: string, store?: unknown): Promise<Nuri>;
|
||||||
|
sparqlQuery(sessionId: string | number, query: string, base?: string, anchor?: NuriLike, label?: string): Promise<unknown>;
|
||||||
|
// Returns the commits the update produced, as upstream does (it typed this `void` until
|
||||||
|
// 2026-08-14 while already relaying the value). A caller that ignores it is unaffected.
|
||||||
|
sparqlUpdate(sessionId: string | number, query: string, anchor?: NuriLike, label?: string): Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── inbox: giving to read, and depositing ────────────────────────────────
|
// ── inbox: giving to read, and depositing ────────────────────────────────
|
||||||
@@ -76,12 +81,15 @@ export const inbox: {
|
|||||||
readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
|
readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
|
||||||
processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
|
processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
|
||||||
watch(targetInbox: NuriLike, onDeposits: (d: Deposit[]) => void): () => void;
|
watch(targetInbox: NuriLike, onDeposits: (d: Deposit[]) => void): () => void;
|
||||||
materialize: typeof read;
|
// `materialize` (a second published name for `read`) was REMOVED on 2026-08-14 —
|
||||||
|
// an alias with no call site, and no counterpart upstream. Use `read`.
|
||||||
};
|
};
|
||||||
export interface Deposit { from: PrincipalId | null; payload: unknown; ts: number }
|
export interface Deposit { from: PrincipalId | null; payload: unknown; ts: number }
|
||||||
|
|
||||||
// ── the wrapped SDK objects ──────────────────────────────────────────────
|
// ── the wrapped SDK objects ──────────────────────────────────────────────
|
||||||
export const ng: Record<string, any>; // call this instead of the `ng` passed to `configure`
|
export const ng: NG; // call this instead of the `ng` passed to `configure`
|
||||||
|
// `NG` is upstream's own type (`@ng-org/web`), 88 typed
|
||||||
|
// members; it was `Record<string, any>` until 2026-08-14
|
||||||
export function init(...args: any[]): any; // likewise — not the `init` passed to `configure`
|
export function init(...args: any[]): any; // likewise — not the `init` passed to `configure`
|
||||||
export function initNg(...args: any[]): any;
|
export function initNg(...args: any[]): any;
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -5,3 +5,5 @@
|
|||||||
|
|
||||||
## Raw markers (consolidate into blocks, then delete)
|
## Raw markers (consolidate into blocks, then delete)
|
||||||
- TOUCHED packages/polyfill/src/shared-wallet/account-registry.ts @2026-08-13 (session f93872b5-293a-4916-a353-181409a96d42)
|
- TOUCHED packages/polyfill/src/shared-wallet/account-registry.ts @2026-08-13 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/shared-wallet/physical.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
- TOUCHED packages/polyfill/src/shared-wallet/session.ts @2026-08-14 (session f93872b5-293a-4916-a353-181409a96d42)
|
||||||
|
|||||||
+26
-19
@@ -135,19 +135,19 @@ Why it lives here and not in the consumer application: the first consumer had ~3
|
|||||||
### Today — `@ng-eventually/polyfill`
|
### Today — `@ng-eventually/polyfill`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// index.ts:55
|
// index.ts:152
|
||||||
export const ng: Record<string, any>;
|
export const ng: NG;
|
||||||
// type re-export, index.ts:50
|
// type re-export, index.ts:86
|
||||||
export type { NG } from "@ng-org/web";
|
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).
|
`ng` is a `Proxy` (`ng-proxy.ts:26`) forwarding every property to the injected real `ng`, overriding exactly one thing: `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). `session_start` is intercepted but is a pure passthrough today, with a TODO for shared-wallet credentials. (`login` was named here until 2026-08-14 but has not existed since 2026-08-03: `@ng-org/web` exposes no such method, and the proxy was FABRICATING it.)
|
||||||
|
|
||||||
### Target
|
### 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.
|
**PASSTHROUGH (level 2, VERIFIED).** `export declare const ng: NG` with `NG = typeof NGModule`, **88** exported members (`index.d.ts:136-231`; the namespace body is `:140-231`). The surface is identical by construction — the proxy adds no member and removes none — **and identical at the type level too since 2026-08-14**, when the published `ng` stopped being declared `Record<string, any>` and took `NG`, upstream's own type. That escape hatch silently dropped all 88 signatures: an application got no completion, and a misspelt member typechecked.
|
||||||
|
|
||||||
The two overrides:
|
The overrides:
|
||||||
|
|
||||||
- `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears.
|
- `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears.
|
||||||
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`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 cap-enforcement.
|
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`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 cap-enforcement.
|
||||||
@@ -276,24 +276,24 @@ The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query
|
|||||||
### Today — `@ng-eventually/polyfill` (namespace `docs`)
|
### Today — `@ng-eventually/polyfill` (namespace `docs`)
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// docs.ts:46
|
// docs.ts:53
|
||||||
export async function docCreate(
|
export async function docCreate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
crdt: string,
|
crdt: string,
|
||||||
cls: string,
|
cls: string,
|
||||||
dest: string,
|
dest: string,
|
||||||
store?: unknown,
|
store?: unknown,
|
||||||
): Promise<Nuri>;
|
): Promise<Nuri>;
|
||||||
// docs.ts:85
|
// docs.ts:101
|
||||||
export async function sparqlUpdate(
|
export async function sparqlUpdate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
anchor?: Nuri,
|
anchor?: Nuri,
|
||||||
label = "sparqlUpdate",
|
label = "sparqlUpdate",
|
||||||
): Promise<void>;
|
): Promise<unknown>;
|
||||||
// docs.ts:130
|
// docs.ts:146
|
||||||
export async function sparqlQuery(
|
export async function sparqlQuery(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
base?: string,
|
base?: string,
|
||||||
anchor?: Nuri,
|
anchor?: Nuri,
|
||||||
@@ -309,7 +309,12 @@ export async function sparqlQuery(
|
|||||||
|
|
||||||
### Target
|
### Target
|
||||||
|
|
||||||
**PASSTHROUGH (level 2, VERIFIED)** — these mirror the real methods 1:1 minus the trailing `label` (a lib-internal access-log tag, never forwarded):
|
**PASSTHROUGH (level 2, VERIFIED)** — these forward to the real methods, and the ONLY argument they do not pass on is the trailing `label` (a lib-internal access-log tag). Until 2026-08-14 this entry claimed a 1:1 mirror, which was false in two ways, both now fixed rather than documented as deltas:
|
||||||
|
|
||||||
|
- **The session id is `string | number`, upstream's own declared type for it** (`Session.session_id`, `sdk/js/web/src/index.ts:16` and the installed `index.d.ts:266`) — it used to be narrowed to `string` here, which made the value the SDK hands an application impossible to pass back into this library. It is RELAYED, never converted: the wasm side deserializes a `u64` (`sdk/js/lib-wasm/src/lib.rs:352-358` `sparql_query`, `:452-457` `sparql_update`, `:1575` `doc_create`), and stringifying it fails that deserialization for real — observed as `Deserialization error of session_id JsValue("1")`.
|
||||||
|
- **`sparqlUpdate` returns what the real method returns** — the commits the update produced (`lib.rs:481-483` serialises `AppResponseV0::Commits`; the installed `index.d.ts:297` types it `Promise<any>`). It was declared `Promise<void>` while already relaying the value at runtime, so the answer was thrown away for every caller. Typed `unknown` rather than `any`, exactly as `sparqlQuery` already renders the same upstream `Promise<any>`. A caller that ignores it is unaffected.
|
||||||
|
|
||||||
|
The real signatures forwarded to:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// index.d.ts:60 — the installed web SDK's doc_create
|
// index.d.ts:60 — the installed web SDK's doc_create
|
||||||
@@ -389,7 +394,9 @@ export async function postToDocument(doc: NuriLike, opts: PostOptions): Promise<
|
|||||||
export async function share(doc: NuriLike, toUser: string): Promise<void>;
|
export async function share(doc: NuriLike, toUser: string): Promise<void>;
|
||||||
export async function read(targetInbox: NuriLike): Promise<Deposit[]>;
|
export async function read(targetInbox: NuriLike): Promise<Deposit[]>;
|
||||||
export async function readForDocument(doc: NuriLike): Promise<Deposit[]>;
|
export async function readForDocument(doc: NuriLike): Promise<Deposit[]>;
|
||||||
export const materialize = read;
|
// `materialize` — a second published name for `read` — was REMOVED on 2026-08-14. It was
|
||||||
|
// an alias and nothing else: no call site, and upstream has no such member, so it was a
|
||||||
|
// symbol an application could learn and would then have to unlearn. Use `read`.
|
||||||
export async function readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
|
export async function readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
|
||||||
export async function processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
|
export async function processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
|
||||||
export function watch(
|
export function watch(
|
||||||
@@ -412,7 +419,7 @@ 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 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`.
|
- `post` / `postToDocument` — the sender-side act exists in the model (the broker routes `InboxPost` natively, `engine/net/src/server_broker.rs`); its JS surface does not. **The future SDK's name and signature are unknown** — `docs/nextgraph-current-state.md:187` records that nothing is announced. `postToDocument`'s resolution step (find the document's inbox address) rides on a **deliberate divergence**: this lib PUBLISHES the address on the document (Header-branch emulation), whereas upstream an address is only ever TRANSMITTED (`ContactDetails` carries `ng:site_inbox`/`ng:protected_inbox`, `engine/verifier/src/inbox_processor.rs:778-830`; the address→repo association lives in `inboxes: PubKey → RepoId`, a table of the **verifier** — one per user, `verifier.rs:105`). *(Corrected 2026-08-10: this said the table was "session-local, rebuilt empty". It is initialized empty (`verifier.rs:520,2820`) and then repopulated at every load — `Verifier::load` → `add_repo_without_saving` → `add_repo_`, `verifier.rs:534-566,2871,2887` — with the inbox private key persisted per repo, `user_storage/repo.rs:61,171,207,362`. The property that matters is that it is **per verifier**, not that it is ephemeral.)* Documented in `docs/briefs/2026-08-03-document-inbox-addressing.md`.
|
||||||
- `share` — a **gap upstream, not a disagreement**, verified at both ends: `ContactDetails.read_cap: Option<ReadCap>` exists (`engine/net/src/types.rs:4233`) but building a message with it is `unimplemented!()` (`types.rs:3786`), its only caller passes `with_readcap: false`, and the receiving arm never reads the field (`inbox_processor.rs:778-830`). `InboxMsgContent::Link` is a **unit variant carrying nothing** (`types.rs:4252`) — do not read it as the delivery channel. The recipient-side filing the lib emulates is real: `AddLink { read_cap }` on the User branch (`engine/repo/src/types.rs:1939-1948`). The consumer's *act* (share one document's cap to one inbox) is target-shaped; only the transport is emulated.
|
- `share` — a **gap upstream, not a disagreement**, verified at both ends: `ContactDetails.read_cap: Option<ReadCap>` exists (`engine/net/src/types.rs:4233`) but building a message with it is `unimplemented!()` (`types.rs:3786`), its only caller passes `with_readcap: false`, and the receiving arm never reads the field (`inbox_processor.rs:778-830`). `InboxMsgContent::Link` is a **unit variant carrying nothing** (`types.rs:4252`) — do not read it as the delivery channel. The recipient-side filing the lib emulates is real: `AddLink { read_cap }` on the User branch (`engine/repo/src/types.rs:1939-1948`). The consumer's *act* (share one document's cap to one inbox) is target-shaped; only the transport is emulated.
|
||||||
- `read` / `materialize` / `readSynced` / `processInbox` / `watch` — **stand-ins for the recipient's own verifier processing**, which has no consumer-facing JS surface upstream and may never have this list-of-deposits shape. A consumer should treat "my inbox gets processed when I connect, and applied caps just appear in what I hold" as the durable contract (that is what `connectedUser` automates, § 13); code that leans on enumerating raw deposits as a mailbox UI is coding against emulation detail it may have to unlearn. The consumer-payload case (`Deposit.payload` as app data) maps to `InboxMsgContent` variants upstream (`types.rs:4249-4260`), of which only `ContactDetails` and `SocialQuery` are more than unit variants today — arbitrary app payloads through the inbox are an **ASSUMPTION**, constrained by the model only in that messages are sealed, per-recipient, and applied by the recipient.
|
- `read` / `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.
|
- `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.
|
||||||
- `share(doc, toUser)` **refuses an unknown recipient** since 2026-08-10. It used to provision one: a mistyped name minted that name's stores and an inbox, and the cap landed where nobody looks. Upstream a deposit is sealed to an inbox pubkey that reached you through an inbound contact, so you cannot address a name you invented.
|
- `share(doc, toUser)` **refuses an unknown recipient** since 2026-08-10. It used to provision one: a mistyped name minted that name's stores and an inbox, and the cap landed where nobody looks. Upstream a deposit is sealed to an inbox pubkey that reached you through an inbound contact, so you cannot address a name you invented.
|
||||||
- `watch`'s `_opts?: { intervalMs?: number }` is accepted and **ignored** (kept for signature compatibility with a removed polling watcher) — dead surface, see § 15.
|
- `watch`'s `_opts?: { intervalMs?: number }` is accepted and **ignored** (kept for signature compatibility with a removed polling watcher) — dead surface, see § 15.
|
||||||
@@ -531,7 +538,7 @@ interface VirtualUserRecord {
|
|||||||
}
|
}
|
||||||
// RegistrySession is INTERNAL since 2026-08-12 (shape kept here for the ruling below).
|
// RegistrySession is INTERNAL since 2026-08-12 (shape kept here for the ruling below).
|
||||||
interface RegistrySession {
|
interface RegistrySession {
|
||||||
sessionId: string;
|
sessionId: string | number; // relayed untouched — upstream's own type (§ 7)
|
||||||
privateStoreId: string;
|
privateStoreId: string;
|
||||||
protectedStoreId?: string;
|
protectedStoreId?: string;
|
||||||
publicStoreId?: string;
|
publicStoreId?: string;
|
||||||
@@ -661,7 +668,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
|||||||
- ~~**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".
|
- ~~**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".
|
||||||
- ~~**`share` is importable from both entries.**~~ **FIXED 2026-08-07** with the entry merge: there is one entry and one `share`, under `inbox`.
|
- ~~**`share` is importable from both entries.**~~ **FIXED 2026-08-07** with the entry merge: there is one entry and one `share`, under `inbox`.
|
||||||
- **One entry means the import line no longer says what disappears.** Until 2026-08-07 a second import path (`/polyfill`) WAS the deletion list. It is now the `POLYFILL-ERA` block in `src/index.ts`, this appendix's note above, and the per-subject rulings in this document. That is a documentation-carried signal where it used to be a mechanical one — the appendix is pinned by a test, the grouping is not.
|
- **One entry means the import line no longer says what disappears.** Until 2026-08-07 a second import path (`/polyfill`) WAS the deletion list. It is now the `POLYFILL-ERA` block in `src/index.ts`, this appendix's note above, and the per-subject rulings in this document. That is a documentation-carried signal where it used to be a mechanical one — the appendix is pinned by a test, the grouping is not.
|
||||||
- **`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.
|
- **`inbox.read` 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.
|
- **`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).
|
- **`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.
|
- **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.
|
||||||
@@ -677,7 +684,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
|||||||
```text
|
```text
|
||||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, NG, NgLike, Nuri, NuriLike, PrincipalId, 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, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
|
||||||
docs: docCreate, sparqlQuery, sparqlUpdate
|
docs: docCreate, sparqlQuery, sparqlUpdate
|
||||||
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
|
inbox: Deposit, PostOptions, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
|
||||||
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import type { Nuri } from "../model/types";
|
|||||||
* nothing else. Never exported from the package.
|
* nothing else. Never exported from the package.
|
||||||
*/
|
*/
|
||||||
export async function registerUpdate(
|
export async function registerUpdate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
anchor: Nuri,
|
anchor: Nuri,
|
||||||
label = "registerUpdate",
|
label = "registerUpdate",
|
||||||
@@ -66,7 +66,7 @@ export async function registerUpdate(
|
|||||||
* only caller, and reading is guarded separately (`inbox.read`).
|
* only caller, and reading is guarded separately (`inbox.read`).
|
||||||
*/
|
*/
|
||||||
export async function depositInto(
|
export async function depositInto(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
targetInbox: Nuri,
|
targetInbox: Nuri,
|
||||||
label = "deposit",
|
label = "deposit",
|
||||||
|
|||||||
@@ -139,7 +139,15 @@ export type { EventuallyConfig } from "./shared-wallet/bootstrap";
|
|||||||
export { ensureIdentity } from "./shared-wallet/access-gate";
|
export { ensureIdentity } from "./shared-wallet/access-gate";
|
||||||
export type { SharedWalletConfig } from "./shared-wallet/access-gate";
|
export type { SharedWalletConfig } from "./shared-wallet/access-gate";
|
||||||
|
|
||||||
|
import type { NG } from "@ng-org/web";
|
||||||
|
|
||||||
import { makeNg } from "./surface/ng-proxy";
|
import { makeNg } from "./surface/ng-proxy";
|
||||||
|
|
||||||
/** SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`. */
|
/**
|
||||||
export const ng: Record<string, any> = makeNg();
|
* SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`.
|
||||||
|
*
|
||||||
|
* Declared `NG` — upstream's own type for its `ng` (`index.d.ts:138`). It was published as
|
||||||
|
* `Record<string, any>` until 2026-08-14, which announced a different surface from the one
|
||||||
|
* it forwards to: an application got no completion and no check on any of the 88 members.
|
||||||
|
*/
|
||||||
|
export const ng: NG = makeNg();
|
||||||
|
|||||||
@@ -250,7 +250,8 @@ export function accountKey(id: string): string {
|
|||||||
|
|
||||||
/** Minimal session shape the registry needs — provided by the consumer. */
|
/** Minimal session shape the registry needs — provided by the consumer. */
|
||||||
export interface RegistrySession {
|
export interface RegistrySession {
|
||||||
sessionId: string;
|
/** Relayed untouched to `ng` — upstream declares it `string | number` (`index.d.ts:266`). */
|
||||||
|
sessionId: string | number;
|
||||||
/** The shared wallet's private store id — the pointer anchor. */
|
/** The shared wallet's private store id — the pointer anchor. */
|
||||||
privateStoreId: string;
|
privateStoreId: string;
|
||||||
/** The shared wallet's protected store id (native store). Optional: only the
|
/** The shared wallet's protected store id (native store). Optional: only the
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import type { Nuri } from "../model/types";
|
|||||||
* afterwards, which the caller decides by filing the cap among the caps that holder holds.
|
* afterwards, which the caller decides by filing the cap among the caps that holder holds.
|
||||||
*/
|
*/
|
||||||
export async function physicalCreate(
|
export async function physicalCreate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
crdt = "Graph",
|
crdt = "Graph",
|
||||||
cls = "data:graph",
|
cls = "data:graph",
|
||||||
dest = "store",
|
dest = "store",
|
||||||
@@ -81,7 +81,7 @@ export async function physicalCreate(
|
|||||||
* read a virtual user's content — that is `docs.sparqlQuery`, which is confined.
|
* read a virtual user's content — that is `docs.sparqlQuery`, which is confined.
|
||||||
*/
|
*/
|
||||||
export async function physicalQuery(
|
export async function physicalQuery(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
base: string | undefined,
|
base: string | undefined,
|
||||||
anchor: Nuri,
|
anchor: Nuri,
|
||||||
@@ -95,7 +95,7 @@ export async function physicalQuery(
|
|||||||
|
|
||||||
/** Write as the PHYSICAL user — the shim's own records. See {@link physicalQuery}. */
|
/** Write as the PHYSICAL user — the shim's own records. See {@link physicalQuery}. */
|
||||||
export async function physicalUpdate(
|
export async function physicalUpdate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
anchor: Nuri,
|
anchor: Nuri,
|
||||||
label = "physicalUpdate",
|
label = "physicalUpdate",
|
||||||
|
|||||||
@@ -95,15 +95,15 @@ export function sessionIsComing(): boolean {
|
|||||||
*
|
*
|
||||||
* ── The session id is RELAYED, never rebuilt — and that is load-bearing ────
|
* ── The session id is RELAYED, never rebuilt — and that is load-bearing ────
|
||||||
* Upstream declares it `string | number` (`Session`, `index.d.ts:266`) and the broker
|
* Upstream declares it `string | number` (`Session`, `index.d.ts:266`) and the broker
|
||||||
* returns a NUMBER; the whole chain below here types it `string` and hands it to `ng.*`,
|
* returns a NUMBER; the chain below here now types it the same way and hands it to `ng.*`,
|
||||||
* whose binding takes it as-is. So this reads the field and passes it on untouched. It is
|
* whose binding takes it as-is. So this reads the field and passes it on untouched. It is
|
||||||
* not a detail: normalizing it to a string was written here first, and the applicative e2e
|
* not a detail: normalizing it to a string was written here first, and the applicative e2e
|
||||||
* refused every call in the batch with `Deserialization error of session_id JsValue("1")`
|
* refused every call in the batch with `Deserialization error of session_id JsValue("1")`
|
||||||
* — the wasm side deserializes the id by its own type, and a stringified number is not it.
|
* — the wasm side deserializes the id by its own type, and a stringified number is not it.
|
||||||
*
|
*
|
||||||
* The `string` in the declared shape is therefore inherited, not asserted: the inaccuracy
|
* The chain used to narrow it to `string`, which made the value the SDK hands an application
|
||||||
* is the chain's and predates this module (every consumer's thunk declared it the same way
|
* unpassable through this library. Widened to upstream's own declared type on 2026-08-14, at
|
||||||
* and relayed the same value). Widening it belongs to the chain, not to the capture.
|
* the capture and along every hop that relays it; the id is still only ever RELAYED.
|
||||||
*/
|
*/
|
||||||
export function captureSession(event: unknown): boolean {
|
export function captureSession(event: unknown): boolean {
|
||||||
if (typeof event !== "object" || event === null) return false;
|
if (typeof event !== "object" || event === null) return false;
|
||||||
@@ -116,7 +116,7 @@ export function captureSession(event: unknown): boolean {
|
|||||||
protected_store_id: protectedStoreId,
|
protected_store_id: protectedStoreId,
|
||||||
public_store_id: publicStoreId,
|
public_store_id: publicStoreId,
|
||||||
} = session as {
|
} = session as {
|
||||||
session_id?: string;
|
session_id?: string | number;
|
||||||
private_store_id?: string;
|
private_store_id?: string;
|
||||||
protected_store_id?: string;
|
protected_store_id?: string;
|
||||||
public_store_id?: string;
|
public_store_id?: string;
|
||||||
|
|||||||
@@ -44,8 +44,15 @@ function rowCount(result: unknown): number {
|
|||||||
* document in the (shared) private store: `docCreate(sid, "Graph", "data:graph",
|
* document in the (shared) private store: `docCreate(sid, "Graph", "data:graph",
|
||||||
* "store")` (store_repo left undefined → private store).
|
* "store")` (store_repo left undefined → private store).
|
||||||
*/
|
*/
|
||||||
|
// The session id is `string | number` because that is what upstream DECLARES for it
|
||||||
|
// (`Session.session_id`, `sdk/js/web/src/index.ts:16` and the installed `index.d.ts:266`),
|
||||||
|
// and the wasm side deserializes it as a `u64` (`sdk/js/lib-wasm/src/lib.rs:352-358`
|
||||||
|
// `sparql_query`, `:452-457` `sparql_update`, `:1575` `doc_create`). It only ever TRAVELS
|
||||||
|
// through this chain — never normalise it, and above all never stringify it: a JS string
|
||||||
|
// fails that deserialization, observed live as
|
||||||
|
// `Deserialization error of session_id JsValue("1")`.
|
||||||
export async function docCreate(
|
export async function docCreate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
crdt: string,
|
crdt: string,
|
||||||
cls: string,
|
cls: string,
|
||||||
dest: string,
|
dest: string,
|
||||||
@@ -82,13 +89,21 @@ export async function docCreate(
|
|||||||
*
|
*
|
||||||
* Mirrors `ng.sparql_update(session_id, query, anchor?)`, where `anchor` is the
|
* Mirrors `ng.sparql_update(session_id, query, anchor?)`, where `anchor` is the
|
||||||
* document NURI the update is scoped/base'd to (optional).
|
* document NURI the update is scoped/base'd to (optional).
|
||||||
|
*
|
||||||
|
* Returns what the real method returns: upstream answers the COMMITS the update
|
||||||
|
* produced (`sdk/js/lib-wasm/src/lib.rs:481-483` serialises `AppResponseV0::Commits`;
|
||||||
|
* the installed `index.d.ts:297` types it `Promise<any>`). This function already
|
||||||
|
* relayed that value at runtime — only the declared type said `void`, which threw the
|
||||||
|
* answer away for every caller. Typed `unknown` rather than `any`, exactly as
|
||||||
|
* {@link sparqlQuery} already renders the same upstream `Promise<any>`: the value is
|
||||||
|
* the broker's to shape, and a caller that ignores it is unaffected.
|
||||||
*/
|
*/
|
||||||
export async function sparqlUpdate(
|
export async function sparqlUpdate(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
anchorLike?: NuriLike,
|
anchorLike?: NuriLike,
|
||||||
label = "sparqlUpdate",
|
label = "sparqlUpdate",
|
||||||
): Promise<void> {
|
): Promise<unknown> {
|
||||||
const { ng } = getConfig();
|
const { ng } = getConfig();
|
||||||
const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlUpdate");
|
const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlUpdate");
|
||||||
// The boundary, in two questions that are NOT the same one.
|
// The boundary, in two questions that are NOT the same one.
|
||||||
@@ -117,7 +132,7 @@ export async function sparqlUpdate(
|
|||||||
* query base IRI (usually `undefined`); `anchor` is the document NURI to query.
|
* query base IRI (usually `undefined`); `anchor` is the document NURI to query.
|
||||||
*/
|
*/
|
||||||
export async function sparqlQuery(
|
export async function sparqlQuery(
|
||||||
sessionId: string,
|
sessionId: string | number,
|
||||||
query: string,
|
query: string,
|
||||||
base?: string,
|
base?: string,
|
||||||
anchorLike?: NuriLike,
|
anchorLike?: NuriLike,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const P = {
|
|||||||
|
|
||||||
/** The inbox documents live in the shared wallet, so we reuse the registry's
|
/** The inbox documents live in the shared wallet, so we reuse the registry's
|
||||||
* injected session provider for the sessionId. Disappears at migration. */
|
* injected session provider for the sessionId. Disappears at migration. */
|
||||||
async function sessionId(): Promise<string> {
|
async function sessionId(): Promise<string | number> {
|
||||||
return (await getStoreRegistryDeps().getSession()).sessionId;
|
return (await getStoreRegistryDeps().getSession()).sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,8 +519,10 @@ export async function read(targetInboxLike: NuriLike): Promise<Deposit[]> {
|
|||||||
return delivered;
|
return delivered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias for {@link read} — the name that reads as "process the inbox now". */
|
// `materialize` — a second published name for {@link read} — was REMOVED from the surface
|
||||||
export const materialize = read;
|
// on 2026-08-14. It was an alias and nothing more: no call site anywhere, and upstream has
|
||||||
|
// no such member, so it was a symbol an application could learn and would have to unlearn.
|
||||||
|
// `read` is the name; the word "materialize" survives only as prose in the docs.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* COLD, BARRIER-GATED read of `targetInbox` — the reliable "process the inbox at
|
* COLD, BARRIER-GATED read of `targetInbox` — the reliable "process the inbox at
|
||||||
|
|||||||
@@ -4,14 +4,31 @@
|
|||||||
* surface stays identical to `@ng-org/web`'s `ng`.
|
* surface stays identical to `@ng-org/web`'s `ng`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { NG } from "@ng-org/web";
|
||||||
|
|
||||||
import { getConfig, getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
import { getConfig, getCaps, getCurrentUser } from "../shared-wallet/bootstrap";
|
||||||
import type { Nuri } from "../model/types";
|
import type { Nuri } from "../model/types";
|
||||||
|
|
||||||
export function makeNg(): Record<string, any> {
|
/**
|
||||||
return new Proxy({} as Record<string, any>, {
|
* Typed `NG` — upstream's own type for `ng` (`NG = typeof NGModule`, 88 exported members,
|
||||||
get(_target, prop: string) {
|
* installed `index.d.ts:136-231`). The proxy adds no member and removes none, so this is
|
||||||
|
* the honest declaration; it used to be `Record<string, any>`, which silently dropped all
|
||||||
|
* 88 signatures and let any misspelt member typecheck.
|
||||||
|
*
|
||||||
|
* The empty object is only the Proxy's TARGET: every property is answered by the `get`
|
||||||
|
* trap below, never by the target, so there is nothing to put in it. Typing the target is
|
||||||
|
* how `new Proxy` carries a type to its result — not an escape hatch, and the surface it
|
||||||
|
* announces is checked against upstream's for every caller.
|
||||||
|
*/
|
||||||
|
export function makeNg(): NG {
|
||||||
|
return new Proxy({} as NG, {
|
||||||
|
get(_target, prop: string | symbol) {
|
||||||
const { ng } = getConfig();
|
const { ng } = getConfig();
|
||||||
|
// The overrides below are keyed by NAME, so they are only consulted for a string
|
||||||
|
// property. A symbol (`Symbol.toStringTag`, `Symbol.asyncIterator`, …) skips them and
|
||||||
|
// falls to the same passthrough as everything else at the bottom — bound identically,
|
||||||
|
// so the proxy stays transparent for every kind of key.
|
||||||
|
if (typeof prop === "string") {
|
||||||
// session_start → open the SHARED wallet invisibly.
|
// session_start → open the SHARED wallet invisibly.
|
||||||
//
|
//
|
||||||
// `login` used to be listed here too. `@ng-org/web` exposes no such method —
|
// `login` used to be listed here too. `@ng-org/web` exposes no such method —
|
||||||
@@ -56,9 +73,10 @@ export function makeNg(): Record<string, any> {
|
|||||||
|
|
||||||
// TODO(anticipated API): a sealed inbox deposit + capability operations — expose
|
// TODO(anticipated API): a sealed inbox deposit + capability operations — expose
|
||||||
// here with their anticipated signatures, emulated for now.
|
// here with their anticipated signatures, emulated for now.
|
||||||
|
}
|
||||||
|
|
||||||
// Everything else: passthrough to the real SDK, unchanged.
|
// Everything else: passthrough to the real SDK, unchanged.
|
||||||
const real = ng[prop];
|
const real = Reflect.get(ng, prop);
|
||||||
return typeof real === "function" ? real.bind(ng) : real;
|
return typeof real === "function" ? real.bind(ng) : real;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ function bindings(
|
|||||||
return anyRes.results?.bindings ?? [];
|
return anyRes.results?.bindings ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sessionId(): Promise<string> {
|
async function sessionId(): Promise<string | number> {
|
||||||
return (await getStoreRegistryDeps().getSession()).sessionId;
|
return (await getStoreRegistryDeps().getSession()).sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ async function sessionId(): Promise<string> {
|
|||||||
* store repo by cap is a native broker fetch (`verifier.rs:1423` `OpenRepo` TODO).
|
* store repo by cap is a native broker fetch (`verifier.rs:1423` `OpenRepo` TODO).
|
||||||
*/
|
*/
|
||||||
async function readDoc(
|
async function readDoc(
|
||||||
sid: string,
|
sid: string | number,
|
||||||
doc: Nuri,
|
doc: Nuri,
|
||||||
): Promise<Array<Record<string, { value: string } | undefined>>> {
|
): Promise<Array<Record<string, { value: string } | undefined>>> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function docChangeType(resp: DocChange): DocChangeType {
|
|||||||
/** An unsubscribe function — idempotent (calling it twice is a no-op). */
|
/** An unsubscribe function — idempotent (calling it twice is a no-op). */
|
||||||
export type Unsubscribe = () => void;
|
export type Unsubscribe = () => void;
|
||||||
|
|
||||||
async function sessionId(): Promise<string> {
|
async function sessionId(): Promise<string | number> {
|
||||||
return (await getStoreRegistryDeps().getSession()).sessionId;
|
return (await getStoreRegistryDeps().getSession()).sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,16 @@ import { configure } from "../src/index";
|
|||||||
import { setCurrentUser } from "../src/shared-wallet/bootstrap";
|
import { setCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||||
import { resetCaps } from "../src/shared-wallet/bootstrap";
|
import { resetCaps } from "../src/shared-wallet/bootstrap";
|
||||||
|
|
||||||
|
// What a real `ng.sparql_update` answers: the COMMITS the update produced
|
||||||
|
// (`sdk/js/lib-wasm/src/lib.rs:481-483` serialises `AppResponseV0::Commits`). The fake
|
||||||
|
// used to answer `undefined` — a result the broker never returns — which is precisely
|
||||||
|
// what let the surface declare `Promise<void>` unchallenged.
|
||||||
|
const COMMITS = [{ id: "did:ng:c:commit-1" }, { id: "did:ng:c:commit-2" }];
|
||||||
|
|
||||||
function fakeNg() {
|
function fakeNg() {
|
||||||
return {
|
return {
|
||||||
doc_create: mock(async (..._a: unknown[]) => "did:ng:o:new-doc"),
|
doc_create: mock(async (..._a: unknown[]) => "did:ng:o:new-doc"),
|
||||||
sparql_update: mock(async (..._a: unknown[]) => undefined),
|
sparql_update: mock(async (..._a: unknown[]) => COMMITS),
|
||||||
sparql_query: mock(async (..._a: unknown[]) => ({ results: { bindings: [] } })),
|
sparql_query: mock(async (..._a: unknown[]) => ({ results: { bindings: [] } })),
|
||||||
// A sentinel: makeNg(), if ever used, would `.bind` and call THIS through
|
// A sentinel: makeNg(), if ever used, would `.bind` and call THIS through
|
||||||
// the JS Proxy. We assert the primitives call the raw fns above directly.
|
// the JS Proxy. We assert the primitives call the raw fns above directly.
|
||||||
@@ -83,6 +89,41 @@ test("sparqlQuery forwards (sessionId, query, base, anchor) and returns the raw
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── the session id: RELAYED, never converted ──────────────────────────────────
|
||||||
|
// Upstream declares it `string | number` (`Session.session_id`, `index.d.ts:266`) and the
|
||||||
|
// broker hands back a NUMBER, which the wasm binding deserializes as a `u64`. Stringifying
|
||||||
|
// it fails that deserialization for real (`Deserialization error of session_id JsValue("1")`),
|
||||||
|
// so what reaches the boundary must be the very value the caller passed — same `typeof`.
|
||||||
|
test("a NUMERIC session id reaches ng untouched, still a number", async () => {
|
||||||
|
const ng = inject();
|
||||||
|
// Anchor the update and the read on the document just CREATED: creating it mints its
|
||||||
|
// cap, so the reach guard (process-wide once any cap exists) is satisfied the way a real
|
||||||
|
// application satisfies it — rather than by naming a document this user does not hold.
|
||||||
|
const created = await docCreate(1, "Graph", "data:graph", "store", undefined);
|
||||||
|
await sparqlUpdate(2, "INSERT DATA {}", created);
|
||||||
|
await sparqlQuery(3, "SELECT * {}", undefined, created);
|
||||||
|
|
||||||
|
const createdSid = ng.doc_create.mock.calls[0]![0];
|
||||||
|
expect(createdSid).toBe(1);
|
||||||
|
expect(typeof createdSid).toBe("number");
|
||||||
|
|
||||||
|
const updated = ng.sparql_update.mock.calls[0]![0];
|
||||||
|
expect(updated).toBe(2);
|
||||||
|
expect(typeof updated).toBe("number");
|
||||||
|
|
||||||
|
const queried = ng.sparql_query.mock.calls[0]![0];
|
||||||
|
expect(queried).toBe(3);
|
||||||
|
expect(typeof queried).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sparqlUpdate hands back what the boundary returned", async () => {
|
||||||
|
// The commits must arrive at the caller unchanged — the surface used to declare
|
||||||
|
// `Promise<void>` and throw this answer away.
|
||||||
|
inject();
|
||||||
|
const returned = await sparqlUpdate("sid-c", "INSERT DATA {}", "did:ng:o:a");
|
||||||
|
expect(returned).toBe(COMMITS);
|
||||||
|
});
|
||||||
|
|
||||||
test("the primitives do NOT route through the public ng proxy (makeNg)", async () => {
|
test("the primitives do NOT route through the public ng proxy (makeNg)", async () => {
|
||||||
// makeNg builds a JS Proxy over the injected ng. If a primitive went through
|
// makeNg builds a JS Proxy over the injected ng. If a primitive went through
|
||||||
// it, calls would land on the proxy's `get` trap, not on our raw mock fns.
|
// it, calls would land on the proxy's `get` trap, not on our raw mock fns.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { test, expect, mock, beforeEach, afterAll } from "bun:test";
|
import { test, expect, mock, beforeEach, afterAll } from "bun:test";
|
||||||
import { post, read, materialize, watch } from "../src/surface/inbox";
|
import { post, read, watch } from "../src/surface/inbox";
|
||||||
|
import * as polyfill from "../src/index";
|
||||||
import { userInbox, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
import { userInbox, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||||
import type { Deposit } from "../src/surface/inbox";
|
import type { Deposit } from "../src/surface/inbox";
|
||||||
import { configure } from "../src/index";
|
import { configure } from "../src/index";
|
||||||
@@ -249,14 +250,23 @@ test("from: null makes an anonymous deposit even when a current user is set", as
|
|||||||
expect(deposits[0]!.from).toBeNull();
|
expect(deposits[0]!.from).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("read returns deposits sorted by ts ascending and materialize is an alias", async () => {
|
test("read returns deposits sorted by ts ascending", async () => {
|
||||||
await post(TARGET, { from: null, payload: "second", ts: 300 });
|
await post(TARGET, { from: null, payload: "second", ts: 300 });
|
||||||
await post(TARGET, { from: null, payload: "first", ts: 100 });
|
await post(TARGET, { from: null, payload: "first", ts: 100 });
|
||||||
await post(TARGET, { from: null, payload: "third", ts: 500 });
|
await post(TARGET, { from: null, payload: "third", ts: 500 });
|
||||||
const deposits = await materialize(TARGET);
|
const deposits = await read(TARGET);
|
||||||
expect(deposits.map((d) => d.payload)).toEqual(["first", "second", "third"]);
|
expect(deposits.map((d) => d.payload)).toEqual(["first", "second", "third"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the published inbox surface exposes `read` and no `materialize` alias", () => {
|
||||||
|
// `materialize` was a second published name for `read` — a symbol an application could
|
||||||
|
// learn and would have to unlearn, since upstream has no such member. This asserts the
|
||||||
|
// PUBLISHED entry (`src/index.ts`'s `inbox` namespace), which is what an app imports,
|
||||||
|
// not merely the module it re-exports.
|
||||||
|
expect(typeof polyfill.inbox.read).toBe("function");
|
||||||
|
expect("materialize" in polyfill.inbox).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
test("read is scoped to one inbox — deposits in another inbox are not returned", async () => {
|
test("read is scoped to one inbox — deposits in another inbox are not returned", async () => {
|
||||||
// The OTHER inbox is obtained from the system, not invented. A made-up NURI would be
|
// The OTHER inbox is obtained from the system, not invented. A made-up NURI would be
|
||||||
// a target no deposit can legitimately reach (`inbox.post` refuses what is not an
|
// a target no deposit can legitimately reach (`inbox.post` refuses what is not an
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ test("write guard: passthrough when anchor is omitted (cannot scope the guard)",
|
|||||||
getCaps().grantWrite(DOC, "alice");
|
getCaps().grantWrite(DOC, "alice");
|
||||||
setCurrentUser("bob");
|
setCurrentUser("bob");
|
||||||
const proxy = makeNg();
|
const proxy = makeNg();
|
||||||
await proxy.sparql_update("sid", "INSERT DATA {}"); // no anchor → passthrough
|
// Anchor explicitly `undefined` — upstream declares all three parameters (`index.d.ts:297`),
|
||||||
|
// and the guard reads `args[2]`, which is `undefined` whether the argument is omitted or
|
||||||
|
// passed as such. Same branch, same passthrough.
|
||||||
|
await proxy.sparql_update("sid", "INSERT DATA {}", undefined); // no anchor → passthrough
|
||||||
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user