/** * Polyfill e2e harness entry — the MINIMAL page loaded inside the broker iframe. * * It imports the REAL `@ng-org/web` `ng` + this package (`@ng-eventually/polyfill`), * configures the polyfill session injection exactly the way a consumer does * (`configure` + `configureStoreRegistry`), waits for the real broker to hand back * a session, then exposes `window.__sdk`: a flat bag of async methods the * Playwright test drives. This has ZERO application domain — no Festipod shapes, * screens, or entities. It exercises the polyfill's OWN surface against the real * broker. * * Why a bridge object of async methods (not calling the lib from Node): the real * `ng` is browser-only (WASM + iframe RPC). Every SDK call must run INSIDE the * broker iframe; Playwright reaches in via `frame.evaluate(() => window.__sdk.x())`. */ import { ng as realNg, init as realInit } from "@ng-org/web"; // The REAL ORM. `useShapeQuery` reads through the SDK's own shape query since // `1.0.0-dev.6`, so the harness has to hand it one: a fake Set-like is enough for the // read FILTER (which is pure) and says nothing about a read that reaches the broker. // `initNg` is what resolves the ORM's own session promise; nothing starts before it. import { initNg as ormInitNg, RdfOrmSubscription } from "@ng-org/orm"; import { configure, docs, init, subscribeDoc, subscribeDocs, inbox, storeRegistry, useShape as libUseShape, useShapeQuery, initNg, } from "@ng-eventually/polyfill"; // The harness tests the LIBRARY, so it legitimately reaches machinery a consumer // application must not — but through the internal path, never the published entry. // `storeRegistry` above is the app-facing slice; these are the shim internals. import * as registryInternals from "../src/shared-wallet/account-registry"; // The shared `useShape` fake, the same one `bun test` injects. Type-only imports // inside it, so nothing of `bun:test` reaches this bundle. import { makeUseShapeHook } from "../test/use-shape-fake"; // The harness plays SEVERAL identities on one page — something no application does, and // the reason `setCurrentUser` / `configureStoreRegistry` are no longer published. It // reaches them by their internal path, like the rest of its machinery. import { adoptCurrentUser, configureStoreRegistry, setCurrentUser, getCaps, getCurrentUser, resetCaps, } from "../src/shared-wallet/bootstrap"; import { connectedUser } from "../src/emulated-verifier/connect"; import * as virtualUsers from "../src/shared-wallet/virtual-users"; import { ensureIdentity } from "@ng-eventually/polyfill"; // The harness narrows for its OWN assertions; a consumer never has to (the entries take // plain strings and validate inside). Internal path, like the rest of its machinery. import { isNuri } from "../src/model/nuri"; /** * The published reference of one of the connected identity's three stores. * * Placement stopped being spelled as a word on the published surface in `1.0.0-dev.5`: * an application names a store by what `privateStore()`, `protectedStore()` or * `publicStore()` hands it. The journeys below still walk the three, so this is the step * from one to the other - through the published calls. */ /** The whole local dataset - upstream's own marker, what `""` normalises to. */ const EVERYTHING = { graphs: "did:ng:i" }; /** * The shape the two `useShapeQuery` journeys read - built here rather than inline, since * both of them need the same one and it is now handed to the SDK. * * Two predicates, both `minCardinality: 1`, because that is what * `engine/verifier/src/orm/graph/query.rs:schema_shape_to_sparql` turns into REQUIRED * triple patterns: a shape constraining nothing would match every subject in the dataset. * The title is the second one, and it is what proves the real data landed rather than an * empty object of the right type. */ function wsShape(cls: string) { return { shape: "urn:e2e:ws:Shape", schema: { "urn:e2e:ws:Shape": { iri: "urn:e2e:ws:Shape", predicates: [ { iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", readablePredicate: "type", maxCardinality: 1, minCardinality: 1, dataTypes: [{ literals: [cls], valType: "iri" }], }, { iri: "urn:e2e:ws:title", readablePredicate: "title", maxCardinality: 1, minCardinality: 1, dataTypes: [{ valType: "string" }], }, ], }, }, } satisfies ShapeType; } const storeOfType = (t: "public" | "protected" | "private"): Promise => t === "public" ? storeRegistry.publicStore() : t === "protected" ? storeRegistry.protectedStore() : storeRegistry.privateStore(); import type { BaseType, Nuri, ShapeObservable, ShapeQuery, ShapeType } from "@ng-eventually/polyfill"; const { IdentityStore } = virtualUsers; /** * The Playwright boundary. Every NURI reaching this harness crosses the bridge as * a plain `string` (Playwright serializes arguments), so it arrives untyped even * though the library's `Nuri` is a template literal type. Narrow it here, loudly: * a test that passes something which is not a NextGraph reference should fail with * that message, not with a confusing downstream error. Never cast — a cast would * re-open exactly the confusion the types exist to close. */ function asNuri(s: string): Nuri { if (!isNuri(s)) throw new Error(`[e2e] not a NextGraph reference: ${JSON.stringify(s)}`); return s; } /** Same, for an optional anchor. */ function asAnchor(s?: string): Nuri | undefined { return s === undefined ? undefined : asNuri(s); } /** The reserved prefix: triples whose SUBJECT falls under it are this package's own * machinery, not the document's data. `readUnion` dropped them for the caller; an * application composing its own read drops them itself, and the contract says so. */ const RESERVED_SUBJECT_PREFIX = "urn:ng-eventually:"; /** * Read ONE document with the published anchored query and group its rows per subject - * what an application composes since `readUnion` was removed in `1.0.0-dev.8`. * * It THROWS, exactly like `docs.sparqlQuery` under it: a document that cannot be read is * a failure, never an empty answer. Every caller below that wants to carry on past one * says so with a `catch` of its own, which is where upstream leaves that decision too. */ async function readOneDocument( sessionId: string, docLike: string, ): Promise }>> { const doc = asNuri(docLike); // Anchored -> default graph = this repo, so the body carries no `GRAPH <...>` wrapper. const res: unknown = await docs.sparqlQuery( sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, doc, ); const rows = Array.isArray(res) ? (res as Array>) : ((res as { results?: { bindings?: Array> } }) .results?.bindings ?? []); const bySubject = new Map }>(); for (const row of rows) { const subject = row["s"]?.value; const p = row["p"]?.value; const o = row["o"]?.value; if (subject === undefined || !p || o === undefined) continue; if (subject.startsWith(RESERVED_SUBJECT_PREFIX)) continue; let entry = bySubject.get(subject); if (!entry) { entry = { subject, graph: doc, props: {} }; bySubject.set(subject, entry); } (entry.props[p] ??= []).push(o); } return [...bySubject.values()]; } // ── The broker session, resolved once the iframe connects ────────────────── interface BrokerSession { session_id: string; private_store_id: string; protected_store_id: string; public_store_id: string; [k: string]: unknown; } let session: BrokerSession | null = null; let sessionResolve!: (s: BrokerSession) => void; const sessionReady = new Promise((r) => (sessionResolve = r)); // The injected `useShape`, so the lib's read-filtered view can be exercised WITHOUT a // React tree. It is `test/use-shape-fake.ts` — the ONE fake in this repository that // returns the shape `@ng-org/orm`'s hook really ships, read off the installed package's // own `.d.ts`. This page used to carry a hand-written Set-like of its own, which is // exactly why the capability filter could hand three members of that envelope back raw // and still pass every gate: no fake anywhere said what the hook returns. const shapeHook = makeUseShapeHook(); // ── Inject the real SDK + polyfill settings (the consumer bootstrap) ──────── configure({ ng: realNg, useShape: shapeHook.hook, // The SDK's own shape query, passed as it stands - no wrapper, no cast. This is what // `useShapeQuery` reads through, so the journeys below run against the REAL broker // rather than against anything this page composed. ormSubscription: RdfOrmSubscription.getOrCreate, init: realInit, initNg: ormInitNg, // The harness already holds an open wallet (the suite imports it itself), so it never // needs the gate's assisted import. These values exist only so the gate has something // to render when the access-gate steps exercise it — they are never used to import. sharedWallet: { fileUrl: "/harness-not-used.ngw", password: "harness" }, }); /** * Who this page opens as — and why it has to open as somebody. * * The boot below goes through the POLYFILL's `init`, not the injected one, so that the * 42 checks run over the ordering an application actually gets: settle the identity, THEN * hand the page to the broker (`surface/lifecycle.ts`). A page with no identity would * raise the barrier instead and never hand over, so the harness supplies one. * * Recorded with `adoptCurrentUser`, which names the identity and stops there — the * session-free half, the same one the access gate settles with (`bootstrap.ts`). Naming it * through `setCurrentUser` would FIRE the connection work: it costs the batch an account * lookup and a broker round-trip for a boot identity nothing reads, and it registers a * connection in flight for a user that does not exist. It used to fire nothing here for an * incidental reason — the registry was still unwired at this line — and `configure` wires * it now, so the intent is stated by the call instead of by the ordering. Every check that * cares about identity sets its own anyway; this one is only what the page opened as. */ const BOOT_IDENTITY = "e2e-harness"; adoptCurrentUser(BOOT_IDENTITY); // The harness keeps its OWN route to the session, substituted through the internal wiring // path AFTER `configure` has pointed the registry at the package's. Not redundancy: the // checks below tear a session down and start another (`session_stop` + `session_start`, the // reconnection cold-start), and only this page knows about the second one — the package's // holder is fed by `init()`'s callback, which the broker fires once per page. configureStoreRegistry({ // The registry (+ subscribe/inbox/read-model) reach the session // through this. It resolves once the broker connects. getSession: async () => { // Read the CURRENT session (mutable): a fresh session (session_stop+session_start // for the reconnection cold-start test) swaps `session` in place, and the SDK must // route reads/opens through the NEW session_id. Fall back to the first-connect // promise until the initial session lands. const s = session ?? (await sessionReady); return { sessionId: s.session_id, privateStoreId: s.private_store_id, protectedStoreId: s.protected_store_id, publicStoreId: s.public_store_id, }; }, // Identity normalization used as the shim key (lowercase, strip leading `@`). normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(), // REAL broker: enable the POINTER micro-guard. The account records live in a // subscribable doc-shim reached via a write-once pointer triple in the store-root; // the account read is barrier-authoritative (no account retry). The only residual // store-root sync-lag is the pointer read — this bounded guard re-reads JUST that // one write-once triple on a cold reconnect (CONTRACT 2 non-fork). It can never // provision or fork an account. pointerGuard: { attempts: 8, baseMs: 150, maxStepMs: 2000 }, }); // ── Bridge marshaling note ───────────────────────────────────────────────── // Everything crossing frame.evaluate must be structured-cloneable. NURIs/strings/ // numbers/plain objects are fine. We never return functions or the raw `ng`. const state: { status: string; error?: string } = { status: "connecting" }; // Identity store over the iframe's localStorage (the real VirtualUserStorage). const identity = new IdentityStore( typeof window !== "undefined" && window.localStorage ? window.localStorage : null, ); // Expose the bridge immediately (status reflects connection progress). (window as any).__sdk = { status: () => state.status, error: () => state.error ?? null, sessionInfo: () => session ? { session_id: session.session_id, private_store_id: session.private_store_id, protected_store_id: session.protected_store_id, public_store_id: session.public_store_id, } : null, /** * Export the CURRENT wallet as a `.ngw` file, base64-encoded so it can cross the * frame.evaluate bridge back to Node. Used by the reconnection cold-start test to * re-import the SAME wallet into a CLEAN browser profile (empty local repo storage) * — the only faithful way to force the broker-only cold-start (a fresh profile has * no local IndexedDB copy of the repos to eagerly rehydrate). */ async exportWalletFile() { const ws = await (realNg as any).get_wallets(); const walletName = Object.keys(ws ?? {})[0]; const file = await (realNg as any).wallet_get_file(walletName); const bytes = file instanceof Uint8Array ? file : new Uint8Array(file); let bin = ""; for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!); return { walletName, b64: btoa(bin), len: bytes.length }; }, /** * THE ACCESS GATE, in a real browser — ported from the consumer's * `barriere-acces-identifiant` feature, which the library took over with the flow. * * Unit tests pin the resolution ORDER (`test/access-gate.test.ts`); only a real DOM can * pin the barrier itself: that it appears on a first access, that entering a value * settles the identity normalized, and that it stands aside once the identity is known. * * That last one holds HERE because this page runs inside the broker iframe, and only * there. Top-level the barrier shows on every load — a known identifier prefills the * field instead of skipping the screen, since knowing who someone is says nothing about * whether their browser still holds the wallet, and the broker's answer for someone who * does not is a dead end with no way back. That side is the applicative suite's to walk * (`e2e/notebook.ts`), with a real page and a real Back button. */ async accessGateFirstVisit(raw: string) { setCurrentUser(null); try { window.localStorage.removeItem("ng-eventually:identity"); } catch {} // A first visit has no `?ng-id=` either, and the URL is the branch the gate consults // FIRST — so clearing storage alone stopped describing a first visit the moment the // boot started settling an identity (which writes the parameter, as every settling // path must). Leaving it there would make this check pass for the wrong reason on a // gate that had stopped asking at all. try { const withoutIdentity = new URL(window.location.href); withoutIdentity.searchParams.delete("ng-id"); window.history.replaceState(null, "", withoutIdentity.toString()); } catch {} const done = ensureIdentity(); const gate = document.querySelector('[data-ng-eventually="access-gate"]'); const root = gate?.shadowRoot ?? null; const input = root?.querySelector("input") as HTMLInputElement | null; const button = root?.querySelector("button.go") as HTMLButtonElement | null; const shown = input !== null && button !== null; const disabledWhenEmpty = button?.disabled ?? null; if (input && button) { input.value = raw; input.dispatchEvent(new Event("input")); button.click(); } await done; return { shown, disabledWhenEmpty, identity: getCurrentUser(), stillMounted: document.querySelector('[data-ng-eventually="access-gate"]') !== null, }; }, /** Inside the iframe, the barrier must stay away once an identity is known. */ async accessGateIdentityAlreadyKnown(known: string) { setCurrentUser(known); await ensureIdentity(); return { shown: document.querySelector('[data-ng-eventually="access-gate"]') !== null, identity: getCurrentUser(), }; }, // ── docs primitives ────────────────────────────────────────────────────── async docCreate() { const s = await sessionReady; return docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); }, async sparqlUpdate(query: string, anchor?: string) { const s = await sessionReady; return docs.sparqlUpdate(s.session_id, query, asAnchor(anchor)); }, async sparqlQuery(query: string, anchor?: string) { const s = await sessionReady; return docs.sparqlQuery(s.session_id, query, undefined, asAnchor(anchor)); }, /** * The load-bearing graph-behavior characterization against the REAL broker. * fake-ng cannot verify any of this — it needs the broker's repo_graph_name * overlay. This is the ground truth the lib's write/read comments cite. * * Anchored to doc D, it measures each write shape: * (a) `INSERT DATA {

o }` (NO GRAPH) → read anchored default graph * (b) `INSERT DATA { GRAPH {

o } }` (explicit plain NURI) → read * both the anchored default graph AND `GRAPH ` explicitly * (c) anchorless `SELECT … WHERE { GRAPH ?g { … } }` over TWO docs D and D2 → * does it see BOTH docs' graphs (the O(wallet) union scan)? */ async docRoundTrip() { const s = await sessionReady; const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); const subj = "urn:e2e:s"; // (a) anchored default-graph write (no GRAPH) — the canonical shape the lib writes. await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${subj}> "yes" }`, doc, ); // (b) explicit `GRAPH ` write, anchored to the same doc. await docs.sparqlUpdate( s.session_id, `INSERT DATA { GRAPH <${doc}> { <${subj}> "yes" } }`, doc, ); // read back anchored default graph — both predicates expected if (b) round-trips. const res: any = await docs.sparqlQuery( s.session_id, `SELECT ?p ?o WHERE { <${subj}> ?p ?o }`, undefined, doc, ); const rows = Array.isArray(res) ? res : res?.results?.bindings ?? []; const preds = rows.map((r: any) => r.p?.value).filter(Boolean); // (b, cont.) read the explicit-graph triple back via its OWN named graph, to // confirm `GRAPH ` when anchored resolves to the same repo graph. const explicitNamed: any = await docs.sparqlQuery( s.session_id, `SELECT ?o WHERE { GRAPH <${doc}> { <${subj}> ?o } }`, undefined, doc, ); const enRows = Array.isArray(explicitNamed) ? explicitNamed : explicitNamed?.results?.bindings ?? []; // (c) anchorless `GRAPH ?g` union scan across TWO docs. Create a second doc // with a DISTINCT triple, then run an ANCHORLESS scan (anchor undefined → // UserSite → local union). If it returns rows from BOTH graphs, the anchorless // union spans every named graph in the session store — the O(wallet-size) cost // the read path exists to avoid. Two docs is enough to demonstrate the union; // it does not bloat the wallet. let unionSpan: { sawDocA: boolean; sawDocB: boolean; graphCount: number } = { sawDocA: false, sawDocB: false, graphCount: 0, }; try { const docB = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); await docs.sparqlUpdate( s.session_id, `INSERT DATA { "yes" }`, docB, ); const union: any = await docs.sparqlQuery( s.session_id, `SELECT ?g ?s ?p WHERE { GRAPH ?g { ?s ?p ?o } }`, undefined, undefined, // ANCHORLESS → UserSite → local union of all named graphs ); const uRows = Array.isArray(union) ? union : union?.results?.bindings ?? []; const subjs = new Set(uRows.map((r: any) => r.s?.value).filter(Boolean)); const graphs = new Set(uRows.map((r: any) => r.g?.value).filter(Boolean)); unionSpan = { sawDocA: subjs.has(subj), sawDocB: subjs.has("urn:e2e:s2"), graphCount: graphs.size, }; } catch (e: any) { unionSpan = { sawDocA: false, sawDocB: false, graphCount: -1 }; (unionSpan as any).error = String(e?.message ?? e); } return { doc, predicates: preds, anchoredPresent: preds.includes("urn:e2e:anchored"), explicitGraphPresent: preds.includes("urn:e2e:explicitGraph"), explicitViaNamedGraph: enRows.length > 0, unionSpan, }; }, // ── the anchored document read ─────────────────────────────────────────── /** * Create N docs, write a marker triple into each (anchored default graph), then read * them ONE BY ONE with the published anchored query. Optionally inject a bad NURI, to * show where the tolerance lives now: at THIS call site, as a visible `catch`, not * inside a primitive. `readUnion` used to swallow it and answer with the rest, which * is a policy upstream does not have - an anchored query on a repo the verifier does * not know REJECTS (`resolve_target_for_sparql` -> `NgError::RepoNotFound`). */ async readDocsOneByOne(n: number, includeBad: boolean) { const s = await sessionReady; const docNuris: Nuri[] = []; for (let i = 0; i < n; i++) { const d = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); await docs.sparqlUpdate( s.session_id, `INSERT DATA { "${i}" }`, d, ); docNuris.push(d); } // Two SUBJECTS in the LAST document, so the probe can tell "one entry per document" // from "one entry per subject". Until 2026-08-10 the read folded every triple of a // document into one bag keyed by the document, and this step stayed green throughout // because it only ever counted entries and never looked at what they were. if (n > 0) { await docs.sparqlUpdate( s.session_id, `INSERT DATA { "extra" }`, docNuris[n - 1]!, ); } const toRead: Nuri[] = includeBad ? [...docNuris, "did:ng:o:definitely-not-a-real-doc-xyz"] : docNuris; const subjects: Array<{ subject: string; graph: Nuri; props: Record }> = []; const skipped: string[] = []; for (const d of toRead) { try { subjects.push(...(await readOneDocument(s.session_id, d))); } catch { // THIS harness chooses to carry on. Another caller may choose not to - that is // the point of the decision living here. skipped.push(d); } } return { docNuris, subjectCount: subjects.length, // What each entry actually IS — the assertion the count could not make. subjectIris: subjects.map((x) => x.subject), graphs: subjects.map((x) => x.graph), skipped, subjects, }; }, /** * The possession gate on the anchored read: create a doc as owner O (whose keyring * gets its cap), then read it as a DIFFERENT identity, which holds nothing. The * stranger has the document's NURI in hand throughout: naming is not reading. * * The stranger is now REFUSED rather than handed an empty list - `readUnion` dropped * an unreachable document from its batch, and there is no batch to drop it from. */ async anchoredReadCapGate() { const s = await sessionReady; resetCaps(); const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); await docs.sparqlUpdate(s.session_id, `INSERT DATA { "x" }`, doc); setCurrentUser("owner-O"); getCaps().open(doc, "protected"); setCurrentUser("someone-else"); let strangerRefused = false; let strangerCount = -1; try { strangerCount = (await readOneDocument(s.session_id, doc)).length; } catch { strangerRefused = true; } setCurrentUser("owner-O"); const asOwner = await readOneDocument(s.session_id, doc); resetCaps(); setCurrentUser(null); return { strangerRefused, strangerCount, ownerCount: asOwner.length }; }, // ── reactivity (doc_subscribe) ─────────────────────────────────────────── // The test installs a counter; subscribeDoc pushes an initial state, then a // push per subsequent write. We record every push and expose the count/log so // the test can wait event-driven (waitForFunction on the counter), never timed. _subs: {} as Record void }>, async subscribeDocStart(handle: string) { const s = await sessionReady; const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); const rec = { count: 0, unsub: () => {} }; (window as any).__sdk._subs[handle] = rec; rec.unsub = subscribeDoc(doc, () => { rec.count += 1; }); return { doc }; }, subscribeCount(handle: string) { return (window as any).__sdk._subs[handle]?.count ?? -1; }, async writeTo(doc: string, marker: string) { const s = await sessionReady; await docs.sparqlUpdate( s.session_id, `INSERT DATA { "${marker}" }`, asNuri(doc), ); }, subscribeStop(handle: string) { const rec = (window as any).__sdk._subs[handle]; if (rec) rec.unsub(); }, /** * subscribeDocs isolation: subscribe to [goodDoc, deadDoc]. The dead doc's * subscription fails in isolation; the good doc still fires on its write. */ _multiSub: { good: 0, bad: 0, unsub: () => {} }, async subscribeDocsStart() { const s = await sessionReady; const good = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); const bad = "did:ng:o:absent-doc-never-created"; const rec = { good: 0, bad: 0, unsub: () => {} }; (window as any).__sdk._multiSub = rec; rec.unsub = subscribeDocs([good, bad], (nuri) => { if (nuri === good) rec.good += 1; else rec.bad += 1; }); return { good, bad }; }, multiSubCounts() { const r = (window as any).__sdk._multiSub; return { good: r.good, bad: r.bad }; }, multiSubStop() { (window as any).__sdk._multiSub.unsub(); }, // ── inbox ──────────────────────────────────────────────────────────────── /** * `id` must be FRESH per run (run.ts stamps it). A user's inbox is stable over time — * that is the point of it — so re-running against a reused id accumulates the previous * runs' deposits on a persistent wallet, and the exact-count assertion drifts. The * thing to make disposable is the user, not the inbox. */ async inboxPostRead(id: string, payloadA: unknown, payloadB: unknown) { // The target must be that user's OWN inbox, not an arbitrary document: you may // deposit into anyone's, you may only read your own. Establishing the identity // FIRST is what makes `userInbox` resolve (and file) that user's inbox. setCurrentUser(id); const target = await registryInternals.userInbox(id, "protected"); await inbox.post(target, { payload: payloadA, from: null, ts: 1000 }); await inbox.post(target, { payload: payloadB, from: null, ts: 2000 }); const deposits = await inbox.read(target); setCurrentUser(null); return { target, deposits }; }, // watch (doc_subscribe-based) fires when a deposit lands. _inboxWatch: { fires: 0, lastLen: -1, unsub: () => {}, target: "" }, /** `id` fresh per run, for the same reason as {@link inboxPostRead}. */ async inboxWatchStart(id: string) { // Watching an inbox is READING it continuously, so the watcher stays connected // for the whole probe — including across `inboxWatchDeposit`. setCurrentUser(id); const target = await registryInternals.userInbox(id, "protected"); const rec = { fires: 0, lastLen: -1, unsub: () => {}, target }; (window as any).__sdk._inboxWatch = rec; rec.unsub = inbox.watch(target, (deposits) => { rec.fires += 1; rec.lastLen = deposits.length; }); return { target }; }, async inboxWatchDeposit(payload: unknown) { const rec = (window as any).__sdk._inboxWatch; await inbox.post(rec.target, { payload, from: null }); }, inboxWatchState() { const r = (window as any).__sdk._inboxWatch; return { fires: r.fires, lastLen: r.lastLen }; }, inboxWatchStop() { (window as any).__sdk._inboxWatch.unsub(); setCurrentUser(null); }, // spoof guard: depositing as another principal throws. async inboxSpoofGuard() { // A REAL inbox, obtained from the system. It used to be a plain `docs.docCreate` // document — a state the library never produces, and `inbox.post` now refuses it // (a deposit is addressed to an inbox, never to a document). The step is about the // `from` spoof guard; it should not also assert something the model forbids. setCurrentUser("alice"); const target = await registryInternals.userInbox("alice", "protected"); let threw = false; try { await inbox.post(target, { payload: { x: 1 }, from: "bob" }); } catch { threw = true; } // self + anonymous both allowed let selfOk = true, anonOk = true; try { await inbox.post(target, { payload: { x: 2 }, from: "alice" }); } catch { selfOk = false; } try { await inbox.post(target, { payload: { x: 3 }, from: null }); } catch { anonOk = false; } setCurrentUser(null); return { spoofRejected: threw, selfOk, anonOk }; }, // ── store-registry ─────────────────────────────────────────────────────── async ensureAccountIdempotent(id: string) { registryInternals.resetRegistryCache(); const first = await registryInternals.ensureAccount(id); registryInternals.resetRegistryCache(); const second = await registryInternals.ensureAccount(id); return { firstDocs: [first.docPublic, first.docProtected, first.docPrivate], secondDocs: [second.docPublic, second.docProtected, second.docPrivate], same: first.docPublic === second.docPublic && first.docProtected === second.docProtected && first.docPrivate === second.docPrivate, }; }, async entityDocsBounded(idA: string, idB: string) { registryInternals.resetRegistryCache(); // Each user creates its OWN documents: you act as one virtual user at a time, // and the caps of what you create are filed under the identity you were acting // as. Creating B's document while connected as A is not a thing the model has. setCurrentUser(idA); const dA1 = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); const dA2 = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); setCurrentUser(idB); const dB1 = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); // listMyEntityDocs(A) → only A's docs (poll: the index append can lag). setCurrentUser(idA); let listA: string[] = []; for (let i = 0; i < 12; i++) { registryInternals.resetRegistryCache(); listA = await registryInternals.listMyEntityDocs(idA, "public"); if (listA.includes(dA1) && listA.includes(dA2)) break; await new Promise((r) => setTimeout(r, 1000)); } setCurrentUser(null); return { dA1, dA2, dB1, listA, hasA1: listA.includes(dA1), hasA2: listA.includes(dA2), leaksB: listA.includes(dB1), }; }, /** * RECONNECTION cold-start seed (phase 1, run in the FIRST session). * * Create a per-entity document under (id, scope) — which also appends its NURI to * the account's scope-index document — and write a marker triple INTO the entity * doc (anchored default graph, the canonical shape). Poll until listMyEntityDocs * sees it, so the index append has actually landed on the broker before we tear * the session down. Returns everything the FRESH session needs to re-find it * purely from the persistent wallet: only `id` + `scope` are load-bearing (the * fresh session re-resolves the account from the shim); entityNuri/marker are the * expected values to assert against. */ async reconnectSeed(id: string, scope: "public" | "protected" | "private") { registryInternals.resetRegistryCache(); const s = await sessionReady; // Seed AS the user whose document this is — otherwise the cap of the created // document is filed under nobody and the very session that created it is // refused the write below. setCurrentUser(id); const entityNuri = await storeRegistry.createEntityDoc(await storeOfType(scope)); const marker = "recon-" + Date.now(); await docs.sparqlUpdate( s.session_id, `INSERT DATA { "${marker}" }`, entityNuri, ); // Wait until the index append + the triple are visible in THIS session (the // session that created them — where the repos are already open), so we know the // data is persisted before the fresh session tries to read it back. let listed: string[] = []; for (let i = 0; i < 15; i++) { registryInternals.resetRegistryCache(); listed = await registryInternals.listMyEntityDocs(id, scope); if (listed.includes(entityNuri)) break; await new Promise((r) => setTimeout(r, 1000)); } return { id, scope, entityNuri, marker, listedInSeed: listed }; }, /** * RECONNECTION read (phase 2, run in a FRESH session over the SAME wallet). First a * DIAGNOSTIC raw anchored read with NO open (rawRowCount), then re-resolve the * account's entity docs of `scope` (listMyEntityDocs → readUserStore) and read each * with its own anchored query, purely from the persistent wallet — nothing from * phase 1's session state carries over. The SDK's open-before-read heal (open-repo.ts) opens each repo via * doc_subscribe before the anchored reads. NB: on the SDK/broker version tested here * the broker-login bootstrap already opens the user's repos, so rawRowCount is * non-zero even without the heal — this is a reconnection REGRESSION guard, not a * fail-without-the-fix proof (see run.ts's reconnection step comment). */ async reconnectRead(id: string, scope: "public" | "protected" | "private", entityNuri: string, marker: string) { const s = session ?? (await sessionReady); // A fresh session holds nothing in memory: connect AS the user so the caps are // restored from the durable registers (own documents from the Store branches, // received ones from the Links) before anything is read back. setCurrentUser(id); await connectedUser(); registryInternals.resetRegistryCache(); const listed = await registryInternals.listMyEntityDocs(id, scope); // DIAGNOSTIC: a RAW anchored read of the entity doc with NO open — reports how // many rows the bare anchored query resolves for a not-yet-opened repo (the // premise: 0 until opened). Uses the low-level docs primitive directly, bypassing // `readOneDocument`'s open step. // // Placed AFTER `listMyEntityDocs`, which is what restores the caps of the user's // own documents from the Store branch. Before it, the boundary refuses the read // and the probe would measure the guard rather than the open — a number that // looks like the premise holding while proving nothing about it. let rawRowCount = -1; try { const raw: any = await docs.sparqlQuery(s.session_id, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, asNuri(entityNuri)); rawRowCount = Array.isArray(raw) ? raw.length : (raw?.results?.bindings?.length ?? 0); } catch (e: any) { rawRowCount = -2; // threw (e.g. RepoNotFound / InvalidNuri) } // One anchored read per listed document, and the tolerance is HERE, visibly: a // reconnect probe that lost one document to a broker hiccup still reports what the // others held, because THIS caller decided so. The primitive rejects. const subjects: Array<{ subject: string; graph: Nuri; props: Record }> = []; const unread: string[] = []; for (const d of listed.length ? listed : [asNuri(entityNuri)]) { try { subjects.push(...(await readOneDocument(s.session_id, d))); } catch { unread.push(d); } } const markers: string[] = []; for (const subj of subjects) { for (const vals of Object.values(subj.props)) { for (const v of vals) markers.push(v); } } return { rawRowCount, listed, listedCount: listed.length, foundEntity: listed.includes(asNuri(entityNuri)), subjectCount: subjects.length, unread, markerPresent: markers.includes(marker), markers, }; }, /** * NON-FORK contract probe. Resolve (or create on first sight) the account for * `id` and return its THREE scope-document NURIs (docPublic/docProtected/ * docPrivate) exactly as ensureAccount records them in the shim. Run in session 1 * it PROVISIONS the account (first NURIs); run in a FRESH faithful reconnect * session it must RE-RESOLVE the SAME account from the synced shim and return the * SAME NURIs — never a second provisioning (an account fork). resetRegistryCache * first so the resolve goes to the shim, not a same-session in-memory hit. */ async accountDocs(id: string) { registryInternals.resetRegistryCache(); const rec = await registryInternals.ensureAccount(id); return { docPublic: rec.docPublic, docProtected: rec.docProtected, docPrivate: rec.docPrivate }; }, // `resolveScopeGraph` stopped being published in `1.0.0-dev.5` — a store's graph holds // its containment triples and nothing else, so the value it handed out matched nothing // as a read scope upstream. The probe keeps measuring the mapping, at its internal path: // a harness may do that, an application may not. async scopeResolvers() { const priv = await registryInternals.resolveScopeGraph("private"); const prot = await registryInternals.resolveScopeGraph("protected"); const pub = await registryInternals.resolveScopeGraph("public"); return { priv, prot, pub }; }, /** * COLD-START anchor probe. Runs the EXACT shim SELECT the registry issues, * anchored to `did:ng:${private_store_id}` (the shim anchor), as the very first * thing in a fresh session — BEFORE anything opens that repo. Reports whether the * raw anchored query threw `RepoNotFound` (the cold-start bug: the private-store * repo not yet in `self.repos`) or returned rows. Uses the low-level docs * primitive directly so nothing (open-repo, ensureAccount) opens the repo first. */ async shimAnchorProbe() { const s = session ?? (await sessionReady); const anchor = `did:ng:${s.private_store_id}`; const query = "SELECT ?acc WHERE { GRAPH <" + anchor + "> { ?acc a } }"; try { const res: any = await docs.sparqlQuery(s.session_id, query, undefined, asNuri(anchor)); const rows = Array.isArray(res) ? res.length : (res?.results?.bindings?.length ?? 0); return { threw: false, error: null, rows, anchor }; } catch (e: any) { return { threw: true, error: String(e?.message ?? e), rows: -1, anchor }; } }, /** * COLD-START account provision. resetRegistryCache then ensureAccount(id) — the * real bootstrap the app runs on first login. On a fresh wallet, if the anchor * repo isn't open, resolveAccount's read AND ensureAccount's provision write both * hit RepoNotFound; the account never persists. Returns the 3 scope docs (all * truthy iff provisioning succeeded) so the runner can gate on real persistence. */ async coldEnsureAccount(id: string) { registryInternals.resetRegistryCache(); try { const rec = await registryInternals.ensureAccount(id); return { threw: false, error: null, docPublic: rec.docPublic, docProtected: rec.docProtected, docPrivate: rec.docPrivate, }; } catch (e: any) { return { threw: true, error: String(e?.message ?? e), docPublic: "", docProtected: "", docPrivate: "" }; } }, /** * VERIFY the provisioned account actually PERSISTED to the shim: resetRegistryCache * then re-resolve the SAME id via a fresh anchored read. Returns whether the read * threw + the resolved docs. After the fix, on a fresh wallet this returns the SAME * docs coldEnsureAccount minted (real persistence, no RepoNotFound). */ async verifyShimPersisted(id: string) { registryInternals.resetRegistryCache(); try { const rec = await registryInternals.ensureAccount(id); return { threw: false, error: null, docPublic: rec.docPublic, docProtected: rec.docProtected, docPrivate: rec.docPrivate }; } catch (e: any) { return { threw: true, error: String(e?.message ?? e), docPublic: "", docProtected: "", docPrivate: "" }; } }, // ── useShapeQuery (reactive useQuery-shaped read) ──────────────────────── // The shape is `wsShape(cls)` above: a hand-written `ShapeType`, two required // predicates, generic and with no application domain. The read goes through the // SDK's own shape query now, so these two journeys measure the REAL broker. _wsProbes: {} as Record< string, { obs: ShapeObservable; initial: ShapeQuery; unsub: () => void } >, /** * REAL-BROKER cycle proof. Under a fresh identity, create ONE protected entity * document carrying an rdf:type= triple, then open a `useShapeQuery` over * (that class shape, the whole dataset). Capture the FIRST snapshot right after subscribe * (must be isPending) so the caller can then wait event-driven for isSuccess with * the seeded datum present. Returns the handle + the initial snapshot + the seed * doc/type so the runner can assert the data landed. */ async useShapeQuerySeedAndSubscribe(handle: string, cls: string) { registryInternals.resetRegistryCache(); const id = "@ws-" + handle; setCurrentUser(id); const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore()); const s = await sessionReady; // Seed the entity doc with the shape's type + a title (anchored default graph). await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${doc}> <${cls}> ; "seeded" }`, doc, ); // Wait until this session sees the index append (data persisted on the broker). for (let i = 0; i < 15; i++) { registryInternals.resetRegistryCache(); const listed = await registryInternals.listMyEntityDocs(id, "protected"); if (listed.includes(doc)) break; await new Promise((r) => setTimeout(r, 1000)); } const shape = wsShape(cls); const obs = useShapeQuery(shape, EVERYTHING) as ShapeObservable; const unsub = obs.subscribe(() => {}); // First snapshot immediately after subscribe — the barrier is not yet crossed // (docs opening via doc_subscribe), so this MUST be pending. const initial = obs.getSnapshot(); (window as any).__sdk._wsProbes[handle] = { obs, initial, unsub }; // NB: leave the current user SET for the probe's lifetime — useShapeQuery resolves // the scope from getCurrentUser() on every (reactive) refresh, exactly as the app // keeps a stable identity. useShapeQueryStop clears it. return { doc, cls, initial: { isPending: initial.isPending, isSuccess: initial.isSuccess, dataLen: initial.data.length } }; }, /** Current snapshot of a useShapeQuery probe (event-driven poll target). */ useShapeQuerySnapshot(handle: string) { const rec = (window as any).__sdk._wsProbes[handle]; if (!rec) return null; const snap: ShapeQuery = rec.obs.getSnapshot(); return { isPending: snap.isPending, isSuccess: snap.isSuccess, isError: snap.isError, dataLen: snap.data.length, // The seeded title, if the datum is present (proves the real data landed). It is the // shape's own readable predicate now, not a property bag: the objects come from the // ORM. titles: snap.data.map((d) => d.title).filter((v) => typeof v === "string"), }; }, useShapeQueryStop(handle: string) { const rec = (window as any).__sdk._wsProbes[handle]; if (rec) rec.unsub(); setCurrentUser(null); }, /** * NAMED-SCOPE seed. Under a fresh identity, create TWO entity documents carrying the * shape's type - one PROTECTED, one PUBLIC - and seed each with a distinguishable * title. Returns the owner and both references, which is all any of the three named * subscriptions below is given. * * Why this exists at all: until `1.0.0-dev.9` every `useShapeQuery` journey in this * suite read `{ graphs: "did:ng:i" }`, so the scope axis that NAMES a document was * measured nowhere against the real broker - and the barrier that names introduced * shipped with no live coverage. A suite that is green on a path it never walks is * worse than one that is red. */ async useShapeQueryNamedSeed(cls: string) { registryInternals.resetRegistryCache(); const owner = "@wsn-" + Date.now(); setCurrentUser(owner); const s = await sessionReady; const prot = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore()); await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${prot}> <${cls}> ; "named-protected" }`, prot, ); // PUBLIC, so its ReadCap is exposed on its Header branch at creation - the one // channel by which a bare reference alone opens a document, and the acquisition the // barrier now performs on this call. const pub = await storeRegistry.createEntityDoc(await storeRegistry.publicStore()); await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${pub}> <${cls}> ; "named-public" }`, pub, ); setCurrentUser(null); return { owner, prot, pub }; }, /** * Subscribe a NAMED scope under `who` - the owner, or somebody holding nothing. * * The identity is set here and left set for the probe's lifetime, like the two * journeys above: `useShapeQuery` asks the keyring about whoever is connected on every * read, exactly as an application with one stable identity per page does. * `useShapeQuerySnapshot` and `useShapeQueryStop` serve this probe too. */ useShapeQueryNamedSubscribe(handle: string, who: string, doc: string, cls: string) { registryInternals.resetRegistryCache(); setCurrentUser(who); const obs = useShapeQuery(wsShape(cls), { graphs: [doc] }) as ShapeObservable; const unsub = obs.subscribe(() => {}); const initial = obs.getSnapshot(); (window as any).__sdk._wsProbes[handle] = { obs, initial, unsub }; return { initial: { isPending: initial.isPending, isSuccess: initial.isSuccess, isError: initial.isError, }, }; }, /** The error message a named scope published, so the runner can tell the refusal from * any other failure rather than accepting `isError` as such. */ useShapeQueryError(handle: string) { const rec = (window as any).__sdk._wsProbes[handle]; if (!rec) return null; const snap: ShapeQuery = rec.obs.getSnapshot(); return snap.error === undefined ? null : String((snap.error as Error)?.message ?? snap.error); }, /** * EMPTY-scope proof: a brand-new identity with NO entity docs at all. `useShapeQuery` * must reach isSuccess with data:[] (synced-but-empty), NOT stay pending. Returns * the handle; poll useShapeQuerySnapshot for the transition. */ useShapeQueryEmptyStart(handle: string, cls: string) { registryInternals.resetRegistryCache(); const id = "@ws-empty-" + handle; setCurrentUser(id); const shape = wsShape(cls); const obs = useShapeQuery(shape, EVERYTHING) as ShapeObservable; const unsub = obs.subscribe(() => {}); const initial = obs.getSnapshot(); (window as any).__sdk._wsProbes[handle] = { obs, initial, unsub }; // Keep the identity set for the probe's lifetime (useShapeQueryStop clears it): a // faithful "empty scope for a real identity", not "no identity at all". return { initial: { isPending: initial.isPending, isSuccess: initial.isSuccess } }; }, // ── caps / read-filter (in-memory cap model) ───────────────────────────── // The read-filter over the injected useShape Set-like. Boundary note: the // caps/read-filter are EMULATED in-memory (CapRegistry) — the real broker does // NOT yet enforce per-doc read caps here (one shared wallet reads everything). // We test what the SDK enforces: the in-memory read-filtered VIEW, which after // cap-surface is KEY POSSESSION — you read what your keyring holds, nothing else. capsReadFilter() { resetCaps(); shapeHook.items.length = 0; shapeHook.items.push( { "@graph": "did:ng:o:protdoc", "@id": "1", v: "protected-item" }, { "@graph": "did:ng:o:pubdoc", "@id": "2", v: "public-item" }, { "@graph": "did:ng:o:unheld", "@id": "3", v: "unheld-item" }, ); // What the hook hands back is the SDK's result envelope, so the items are read off // `data` - the member its own example destructures first, and one of the three that // used to come back unfiltered. const read = (): string[] => [...(libUseShape(wsShape("urn:e2e:caps:Item"), EVERYTHING).data as Iterable)].map( (i) => i.v, ); setCurrentUser("owner-O"); getCaps().open("did:ng:o:protdoc", "protected"); const link = getCaps().open("did:ng:o:pubdoc", "public"); const ownerView = read(); // A stranger holds nothing, and this VIEW asks nobody: it is pure possession, with // no round-trip to spend (see `read-filter.ts`). That a public store would serve // the cap is proven on the read paths, not here. setCurrentUser("stranger"); const strangerView = read(); // …and the SDK's own handle is refused outright, whoever asks: it reads and writes // with no capability filter at all. let subscriptionRefused: string | null = null; try { void libUseShape(wsShape("urn:e2e:caps:Item"), EVERYTHING).subscription; } catch (e) { subscriptionRefused = e instanceof Error ? e.message : String(e); } getCaps().learn(link); const strangerWithLinkView = read(); resetCaps(); shapeHook.items.length = 0; setCurrentUser(null); return { ownerView, strangerView, strangerWithLinkView, subscriptionRefused }; }, // MOVED to the applicative suite, `e2e/notebook.ts` (2026-08-07): // - `documentInboxDeposit` → "Bob leaves a message on Alice's note, and only Alice // reads it". This one is WHY that suite exists: it was green here while the // feature was unusable, because a harness can hand an inbox address across an // identity boundary through a variable and an application cannot. // - `capsShareCap` → "Alice's protected note stays shut until she gives Bob the key". // // What stays here is what an application does not do: primitives, characterisation, // and the cold-start regressions. // ── accounts (IdentityStore) ───────────────────────────────────────────── identitySet(id: string) { return identity.set(id); }, identityGet() { return identity.get(); }, identityClear() { identity.clear(); return identity.get(); }, // ── CONTRACT: first-State barrier (doc_subscribe sync-point) ──────────── // // The contract under test: the 1st event emitted by doc_subscribe is a `State` // that marks the end of the initial broker sync. After that push: // - if the doc has data → it MUST be present in the State (no "not yet synced") // - if the doc is empty → it IS definitively empty (no later arrival) // // We drive this via the polyfill's subscribeDoc (which calls ng.doc_subscribe // directly), but we capture the RAW AppResponse for each push so we can: // (a) identify the event type (State vs Patch vs TabInfo vs other) // (b) measure the time-to-first-State // (c) correlate "State received" with the SPARQL content query result // // Bridge state for a single active first-State probe. _stateProbe: null as null | { doc: string; startMs: number; events: Array<{ typeKey: string; elapsedMs: number }>; unsub: () => void; }, /** * PRESENCE probe — phase 1: write a triple into `doc` (same session, so the * write is already committed on the broker before we subscribe). Returns the doc * NURI so the test can hand it to stateProbeSubscribe. Split into write + subscribe * so the test controls the ordering precisely. */ async stateProbeWrite(triple: { s: string; p: string; o: string }): Promise { const s = await sessionReady; const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); await docs.sparqlUpdate( s.session_id, `INSERT DATA { <${triple.s}> <${triple.p}> "${triple.o}" }`, doc, ); return doc; }, /** * Subscribe to `doc` and record every raw event (type key + elapsed ms from * subscribe call). The state probe slot is single-use per call; a previous probe * is torn down first. `subscribeDoc` is the polyfill wrapper — it passes the raw * AppResponse straight through to the callback. */ stateProbeSubscribe(doc: string): void { // Tear down any prior probe. const prev = (window as any).__sdk._stateProbe; if (prev) { try { prev.unsub(); } catch { /* ignore */ } } const probe = { doc, startMs: Date.now(), events: [] as Array<{ typeKey: string; elapsedMs: number }>, unsub: () => {}, }; (window as any).__sdk._stateProbe = probe; probe.unsub = subscribeDoc(asNuri(doc), (resp: any) => { const elapsedMs = Date.now() - probe.startMs; // AppResponse shape: { V0: { State: … } } | { V0: { Patch: … } } | { V0: { TabInfo: … } } | … let typeKey = "unknown"; try { const v0 = resp?.V0 ?? resp?.v0 ?? resp; if (v0 && typeof v0 === "object") { const keys = Object.keys(v0); typeKey = keys[0] ?? "empty"; } else { typeKey = String(v0); } } catch { typeKey = "parse-error"; } probe.events.push({ typeKey, elapsedMs }); }); }, /** Return the accumulated event log (snapshot — safe to call at any time). */ stateProbeEvents(): Array<{ typeKey: string; elapsedMs: number }> { return (window as any).__sdk._stateProbe?.events ?? []; }, /** * Return the count of events with typeKey === `State` received so far. * Used by waitForFunction to wait for the first State (not just any event — * the broker pushes TabInfo first, State second). */ stateProbeStateCount(): number { const events: Array<{ typeKey: string }> = (window as any).__sdk._stateProbe?.events ?? []; return events.filter((e) => e.typeKey === "State").length; }, /** * After the first State has been received, run an anchored SPARQL query on the * probe doc to read back its triples. Returns the raw rows so the test can assert * presence/absence without re-querying from the test side. */ async stateProbeQuery(s_uri: string, p_uri: string): Promise<{ rows: number; found: boolean }> { const s = await sessionReady; const doc = (window as any).__sdk._stateProbe?.doc; if (!doc) return { rows: -1, found: false }; try { const res: any = await docs.sparqlQuery( s.session_id, `SELECT ?o WHERE { <${s_uri}> <${p_uri}> ?o }`, undefined, doc, ); const rows = Array.isArray(res) ? res.length : (res?.results?.bindings?.length ?? 0); return { rows, found: rows > 0 }; } catch (e: any) { return { rows: -2, found: false }; } }, /** Stop the active probe's subscription. */ stateProbeStop(): void { const probe = (window as any).__sdk._stateProbe; if (probe) { try { probe.unsub(); } catch { /* ignore */ } } (window as any).__sdk._stateProbe = null; }, /** * ABSENCE probe — subscribe to a doc NURI that was never written (non-existent). * Returns the doc immediately after creating it (empty), then subscribes. * The test waits for the first State, then verifies the doc is empty AND stays * empty for a grace window (no late Patch arrival). */ async stateProbeEmptyDoc(): Promise { // Create a real doc so it's valid for doc_subscribe, but write NOTHING into it. const s = await sessionReady; const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); // Subscribe immediately (no data written). (window as any).__sdk.stateProbeSubscribe(doc); return doc; }, }; // ── Connect to the real broker ───────────────────────────────────────────── // Mirrors ngSession.ts: register the init callback; the broker (this iframe is // loaded by it) drives the connection and calls back with the session. // // Through the POLYFILL's `init`, not the injected `realInit` — even though `realInit` is // what ends up being called (it is what `configure` injects, above). Calling it directly // skipped the forwarder that settles the identity before delegating, so the 42 checks ran // over an ordering no application has, and the defect that ordering exists to prevent — // the hand-over happening before the identity reaches the address bar — could not have // been caught here. What an application writes is this line. (async () => { try { await init( (event: { session: BrokerSession }) => { session = event.session; state.status = "connected"; // The ORM reaches the SDK through a module-level promise of its own, resolved // once. Nothing it opens starts before this, so it goes here rather than beside // the first read - through the PACKAGE's `initNg`, which is the call an // application makes. // // Since `1.0.0-dev.7` that call SUBSTITUTES the package's wrapped `ng` for the // one passed here, so the two `useShapeQuery` journeys below are now the proof // that `orm_start_graph` survives the wrapper - and they are the only thing that // is. A consumer wiring exactly this path got `orm_start_graph is not a function` // and a read pending for ever, while this suite stayed green precisely because // the ORM had been handed the raw SDK object here instead. initNg(realNg, session); sessionResolve(session); }, true, [], ); } catch (e) { state.status = "error"; state.error = e instanceof Error ? e.message : String(e); } })();