Suite du balayage commencé dans la doc : 20 occurrences de P1a/P1b dans les commentaires, les titres de tests et le README. Les phrases ont été récrites, pas substituées : « the breach P1a opened » devient « the breach that cap-surface opened », et « labelled P1b's » ne survivait pas à un nom plus long. Un lecteur qui n'a jamais entendu ni l'un ni l'autre doit comprendre la phrase. L'avertissement de déploiement du README garde sa force et gagne un nom : « "anonymous" or "private" until cap-enforcement lands per-document encryption. » Reste une occurrence dans e2e/polyfill-entry.ts, qui part avec le lot e2e.
@ng-eventually/polyfill
One entry point. Most of what it publishes has the same signature as the future SDK —
ng, useShape, watchShape, docs, inbox, storeRegistry, readUnion (+ types) —
and is a drop-in for @ng-org/web / @ng-org/orm: as NextGraph matures it resolves to
the real SDK (build alias removed) with no code change.
One call does not, and it is the whole of what you will delete: configure. It
exists because one shared wallet hosts every user; upstream, an application imports the
SDK and each user opens their own wallet. src/index.ts groups it under a heading that
says so. (ensureIdentity is a second in substance — the shared-wallet gate — but its
call site survives: an application still awaits a session before it renders.)
(There were two entry points until 2026-08-07, . and ./polyfill, and the second one
WAS that list. One door is easier to import from and says less — hence the grouping, and
hence docs/api-contract.md, whose export inventory a test keeps honest.)
Per-symbol, with the target signature and an epistemic label on every claim:
docs/api-contract.md.
Reading is key possession, and the isolation here is still fake. The cap surface has the shape of the real model — you hold a document's
ReadCapor you do not read it, and there is no authorization list anywhere — but nothing is encrypted yet and the stand-in key is a constant. Nothing this library does may be described as "anonymous" or "private" until cap-enforcement lands per-document encryption.
import {
// SDK-shaped — the real SDK replaces these in place.
ensureIdentity, storeRegistry, inbox, readUnion, docs,
// Polyfill-era — one call, and it is the whole of what goes away.
configure,
} from "@ng-eventually/polyfill";
configure({ ng: realNg, useShape: realUseShape, getSession, sharedWallet });
await ensureIdentity(); // who I am (returned), connection work awaited
const doc = await storeRegistry.createEntityDoc("protected");
await docs.sparqlUpdate(sid, `INSERT DATA { … }`, doc);
const subjects = await readUnion(await storeRegistry.listMyEntityDocs("protected"));
Principle — the polyfill compensates, it never extends
Its only reason to exist is to bridge a NextGraph implementation gap. Every non-SDK surface must map to something NextGraph will provide natively, and must fall away at that point — no bespoke features, no observability, no convenience API that isn't strictly "NextGraph will do this later". The test for any proposed addition: does it compensate a real, exhibited gap? If not, it belongs in the consumer application. And a compensation whose gap is not actually exhibited on the target broker is dead weight, not defensive code.
Both halves are binding — the surface AND the implementation stay as close as possible to what NextGraph plans. The question to ask at every choice: would this make a caller learn something it has to UNLEARN at migration? If yes, it is a deviation, whatever it buys.
What the polyfill adds, each emulated now and native later:
- Shared-wallet identity — one wallet hosts every user, so the library fabricates
virtual users and confines every access to the connected one
(
emulated-verifier/reach.ts). Upstream, each user opens their own wallet. - Capability emulation — per-identity cap possession plus a read filter over it: you read the documents whose cap you hold. There is no authorization list, because the real model has none.
- Inbox —
post,postToDocument,share, and the recipient's processing. The model is verified (an inbox is a keypair on one repo); no JS surface exists yet.
Generic by construction: no application domain here. See
examples/notebook for an application written against it,
which the e2e suite drives.
How a document is reached — the three acts, and no others
import { storeRegistry, inbox, readUnion } from "@ng-eventually/polyfill";
// 1. CREATE — you hold its cap, with nothing to declare. No identity parameter: a
// session belongs to one user, exactly as the target's own `doc_create` assumes.
const doc = await storeRegistry.createEntityDoc("protected");
// 2. GIVE TO READ — name the document and the person. The key is looked up and
// sealed into a deposit; the recipient applies it by connecting, with nothing
// to call. Irreversible: there is no revoking a key already handed out.
await inbox.share(doc, "bob");
// 3. CIRCULATE THE REFERENCE — no call at all. Every reference this surface returns
// is BARE: it names the document and grants nothing. If the document sits in a
// PUBLIC store, the store serves its read cap to whoever asks, so the bare
// reference is enough to read it — and if it does not, the reference still names
// it and opens nothing.
const publicDoc = await storeRegistry.createEntityDoc("public");
// …put `publicDoc` in a QR code, a message, another document. Nothing else to do.
await readUnion([publicDoc]); // a stranger holding only this reads it
The invariant behind all three: you never derive a cap from a bare reference. You
look it up in what you hold, you were given it, or a public store served it. A
did:ng:o:… without :r: names a document and opens nothing — which is what makes
confidentiality composable: a widely circulated document may point at a restricted
one, and following the reference gets you a name, not a key. See
docs/readcap-and-nuri-model.md § 0.
The types carry that invariant
Nuri and ReadCap are template literal types, not string aliases:
type Nuri = `did:ng:${string}`
type ReadCap = `did:ng:${string}:r:${string}`
They are still strings — assignable to string, JSON-serializable, no wrapper — but
the distinction is checked. A ReadCap goes wherever a Nuri is expected (a cap is
a NURI with the key inside); the reverse does not compile.
Permissive in, precise out. Public entries take NuriLike (Nuri | string) and
validate at the door, so a value coming from storage, a URL or a form needs no
narrowing and no cast on your side; what they return is a precise Nuri. The
runtime checks stay regardless — a JavaScript caller never meets the compiler.
const saved = localStorage.getItem("doc"); // string | null
if (saved) await readUnion([saved]); // ✓ validated at the door