/** * SDK e2e harness entry — the MINIMAL page loaded inside the broker iframe. * * It imports the REAL `@ng-org/web` `ng` + this package (`@ng-eventually/client`), * 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"; import { configure, configureStoreRegistry, setCurrentUser, getCaps, resetCaps } from "@ng-eventually/client/polyfill"; import { docs, subscribeDoc, subscribeDocs, readModel, inbox, discovery, storeRegistry, useShape as libUseShape, watchShape, accounts, } from "@ng-eventually/client"; import type { ShapeObservable, ShapeQuery } from "@ng-eventually/client"; const { IdentityStore } = accounts; // ── 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)); // A minimal in-memory Set-like, so the lib's read-filtered `useShape` view can be // exercised WITHOUT the real ORM (`@ng-org/orm` is not installed here, and the // read-filter is pure — it wraps whatever Set-like the injected useShape returns). // The injected useShape receives `(shapeType, scope)`; we ignore both and return // the items array captured by the test through window.__sdk.caps.seedSet(). let injectedSetItems: any[] = []; function fakeUseShape(_shape: unknown, _scope: unknown): any { const items = () => injectedSetItems; return { get size() { return items().length; }, [Symbol.iterator]() { return items()[Symbol.iterator](); }, forEach(cb: (v: unknown) => void) { items().forEach(cb); }, add(item: any) { injectedSetItems.push(item); }, }; } // ── Inject the real SDK + polyfill settings (the consumer bootstrap) ──────── configure({ ng: realNg, useShape: fakeUseShape, init: realInit, }); configureStoreRegistry({ // The registry (+ subscribe/inbox/discovery/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 AccountStorage). 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 }; }, // ── 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, anchor); }, async sparqlQuery(query: string, anchor?: string) { const s = await sessionReady; return docs.sparqlQuery(s.session_id, query, undefined, 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, }; }, // ── read-model ─────────────────────────────────────────────────────────── /** * Create N docs, write a marker triple into each (anchored default graph), then * readUnion over them → one subject per doc. Optionally inject a bad NURI to * prove per-doc tolerance (the bad one is skipped, the batch survives). */ async readUnionOverDocs(n: number, includeBad: boolean) { const s = await sessionReady; const docNuris: string[] = []; 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); } const toRead = includeBad ? [...docNuris, "did:ng:o:definitely-not-a-real-doc-xyz"] : docNuris; const subjects = await readModel.readUnion(toRead); return { docNuris, subjectCount: subjects.length, subjects }; }, /** * readUnion cap gate: create a doc, mark it protected for owner O, set the * current user to a DIFFERENT identity, and readUnion → the doc is dropped. */ async readUnionCapGate() { 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); getCaps().open(doc, "protected", "owner-O"); setCurrentUser("someone-else"); const asStranger = await readModel.readUnion([doc]); setCurrentUser("owner-O"); const asOwner = await readModel.readUnion([doc]); resetCaps(); setCurrentUser(null); return { strangerCount: asStranger.length, 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}" }`, 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 ──────────────────────────────────────────────────────────────── async inboxPostRead(payloadA: unknown, payloadB: unknown) { const s = await sessionReady; const target = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); setCurrentUser("inbox-user"); 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: "" }, async inboxWatchStart() { const s = await sessionReady; const target = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); 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; setCurrentUser("watcher"); await inbox.post(rec.target, { payload, from: null }); setCurrentUser(null); }, inboxWatchState() { const r = (window as any).__sdk._inboxWatch; return { fires: r.fires, lastLen: r.lastLen }; }, inboxWatchStop() { (window as any).__sdk._inboxWatch.unsub(); }, // spoof guard: depositing as another principal throws. async inboxSpoofGuard() { const s = await sessionReady; const target = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined); setCurrentUser("alice"); 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 }; }, // ── discovery index ────────────────────────────────────────────────────── async discoverySubmitRead(ref: unknown) { setCurrentUser("publisher"); await discovery.submitToIndex(ref); setCurrentUser(null); const entries = await discovery.readIndex(); return { entries }; }, _discWatch: { fires: 0, lastLen: -1, unsub: () => {} }, discoveryWatchStart() { const rec = { fires: 0, lastLen: -1, unsub: () => {} }; (window as any).__sdk._discWatch = rec; rec.unsub = discovery.watchIndex((entries) => { rec.fires += 1; rec.lastLen = entries.length; }); }, async discoverySubmit(ref: unknown) { setCurrentUser("publisher2"); await discovery.submitToIndex(ref); setCurrentUser(null); }, discoveryWatchState() { const r = (window as any).__sdk._discWatch; return { fires: r.fires, lastLen: r.lastLen }; }, discoveryWatchStop() { (window as any).__sdk._discWatch.unsub(); }, // reserved @index account isolation: a real user named "index"/"@index" resolves // to a DIFFERENT account than the reserved index owner. async discoveryIndexIsolation() { const userIndex = await storeRegistry.ensureAccount("@index"); const reserved = await storeRegistry.ensureAccount(discovery.INDEX_ACCOUNT); return { userIndexDoc: userIndex.docPublic, reservedDoc: reserved.docPublic, disjoint: userIndex.docPublic !== reserved.docPublic, }; }, // ── store-registry ─────────────────────────────────────────────────────── async ensureAccountIdempotent(id: string) { storeRegistry.resetRegistryCache(); const first = await storeRegistry.ensureAccount(id); storeRegistry.resetRegistryCache(); const second = await storeRegistry.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) { storeRegistry.resetRegistryCache(); const dA1 = await storeRegistry.createEntityDoc(idA, "public"); const dA2 = await storeRegistry.createEntityDoc(idA, "public"); const dB1 = await storeRegistry.createEntityDoc(idB, "public"); // listMyEntityDocs(A) → only A's docs (poll: the index append can lag). let listA: string[] = []; for (let i = 0; i < 12; i++) { storeRegistry.resetRegistryCache(); listA = await storeRegistry.listMyEntityDocs(idA, "public"); if (listA.includes(dA1) && listA.includes(dA2)) break; await new Promise((r) => setTimeout(r, 1000)); } 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") { storeRegistry.resetRegistryCache(); const s = await sessionReady; const entityNuri = await storeRegistry.createEntityDoc(id, 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++) { storeRegistry.resetRegistryCache(); listed = await storeRegistry.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 → readScopeIndex) and readUnion * them, 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) { // DIAGNOSTIC: a RAW anchored read of the entity doc with NO open at all, first // thing in the fresh session — 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 readUnion's open step. const s = session ?? (await sessionReady); let rawRowCount = -1; try { const raw: any = await docs.sparqlQuery(s.session_id, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, entityNuri); rawRowCount = Array.isArray(raw) ? raw.length : (raw?.results?.bindings?.length ?? 0); } catch (e: any) { rawRowCount = -2; // threw (e.g. RepoNotFound / InvalidNuri) } storeRegistry.resetRegistryCache(); const listed = await storeRegistry.listMyEntityDocs(id, scope); const subjects = await readModel.readUnion(listed.length ? listed : [entityNuri]); 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(entityNuri), subjectCount: subjects.length, 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) { storeRegistry.resetRegistryCache(); const rec = await storeRegistry.ensureAccount(id); return { docPublic: rec.docPublic, docProtected: rec.docProtected, docPrivate: rec.docPrivate }; }, async scopeResolvers() { const priv = await storeRegistry.resolveScopeGraph("private"); const prot = await storeRegistry.resolveScopeGraph("protected"); const pub = await storeRegistry.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, 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) { storeRegistry.resetRegistryCache(); try { const rec = await storeRegistry.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) { storeRegistry.resetRegistryCache(); try { const rec = await storeRegistry.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: "" }; } }, // ── watchShape (reactive useQuery-shaped read) ─────────────────────────── // A minimal SHEX-ish ShapeType pinning rdf:type to an e2e class IRI. watchShape // reads only `.shape`/`.schema[...].predicates[rdf:type].dataTypes[].literals` // to know which @type to keep — generic, no application domain. _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 `watchShape` over * (that class shape, "protected"). 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 watchShapeSeedAndSubscribe(handle: string, cls: string) { storeRegistry.resetRegistryCache(); const id = "@ws-" + handle; setCurrentUser(id); const doc = await storeRegistry.createEntityDoc(id, "protected"); 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++) { storeRegistry.resetRegistryCache(); const listed = await storeRegistry.listMyEntityDocs(id, "protected"); if (listed.includes(doc)) break; await new Promise((r) => setTimeout(r, 1000)); } const shape = { 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" }], }, ], }, }, }; const obs = watchShape(shape, "protected") 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 — watchShape resolves // the scope from getCurrentUser() on every (reactive) refresh, exactly as the app // keeps a stable identity. watchShapeStop clears it. return { doc, cls, initial: { isPending: initial.isPending, isSuccess: initial.isSuccess, dataLen: initial.data.length } }; }, /** Current snapshot of a watchShape probe (event-driven poll target). */ watchShapeSnapshot(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). titles: snap.data.flatMap((d: any) => d.props?.["urn:e2e:ws:title"] ?? []), }; }, watchShapeStop(handle: string) { const rec = (window as any).__sdk._wsProbes[handle]; if (rec) rec.unsub(); setCurrentUser(null); }, /** * EMPTY-scope proof: a brand-new identity with NO entity docs of `scope`. watchShape * must reach isSuccess with data:[] (synced-but-empty), NOT stay pending. Returns * the handle; poll watchShapeSnapshot for the transition. */ watchShapeEmptyStart(handle: string, cls: string) { storeRegistry.resetRegistryCache(); const id = "@ws-empty-" + handle; setCurrentUser(id); const shape = { 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" }], }, ], }, }, }; const obs = watchShape(shape, "protected") 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 (watchShapeStop 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. capsReadFilter() { resetCaps(); injectedSetItems = [ { "@graph": "did:ng:o:protdoc", "@id": "1", v: "protected-item" }, { "@graph": "did:ng:o:pubdoc", "@id": "2", v: "public-item" }, { "@graph": "did:ng:o:ungoverned", "@id": "3", v: "ungoverned-item" }, ]; getCaps().open("did:ng:o:protdoc", "protected", "owner-O"); getCaps().makePublic("did:ng:o:pubdoc"); // as owner-O setCurrentUser("owner-O"); const ownerView = [...(libUseShape(null, null) as Iterable)].map((i) => i.v); // as a stranger setCurrentUser("stranger"); const strangerView = [...(libUseShape(null, null) as Iterable)].map((i) => i.v); resetCaps(); injectedSetItems = []; setCurrentUser(null); return { ownerView, strangerView }; }, capsDirectedGrant() { resetCaps(); injectedSetItems = [{ "@graph": "did:ng:o:sharedoc", "@id": "1", v: "shared-item" }]; getCaps().open("did:ng:o:sharedoc", "protected", "owner-O"); setCurrentUser("friend"); const before = [...(libUseShape(null, null) as Iterable)].length; getCaps().grantRead("did:ng:o:sharedoc", "friend"); const after = [...(libUseShape(null, null) as Iterable)].length; resetCaps(); injectedSetItems = []; setCurrentUser(null); return { before, after }; }, // ── 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(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. (async () => { try { await (realInit as any)( (event: any) => { session = event.session as BrokerSession; state.status = "connected"; sessionResolve(session); }, true, [], ); } catch (e: any) { state.status = "error"; state.error = String(e?.message ?? e); } })();