Files
ng-eventually/packages/polyfill
Sylvain Duchesne 98e46effc2 fix: le resultat d-une commande de page suit la meme regle que tout le reste
Balayage execute sur les deux proxys apres le correctif precedent : aucun membre
de la vue de collection ne rend plus la cible, mais une commande de page de
l-enveloppe qui rendrait la collection la rendait BRUTE. `nextPage` est declare
`() => void` et ne rend rien aujourd-hui, donc ce n-est pas un defaut vivant -
c-est la meme justification jamais executee, sur l-autre proxy, et c-est
exactement le motif qu-il fallait casser. Elle s-execute toujours ; seul son
retour est tenu a la regle.
2026-09-24 19:03:10 +02:00
..

@ng-eventually/polyfill

One entry point. Most of what it publishes has the same signature as the future SDK — ng, useShape, useShapeQuery, getObjects, insertObject, docs, inbox, storeRegistry (+ 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 ReadCap or 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, docs,
  // Polyfill-era — one call, and it is the whole of what goes away.
  configure,
} from "@ng-eventually/polyfill";

configure({
  ng: realNg, useShape: realUseShape, ormSubscription, getObjects, insertObject,
  initNg, sharedWallet,
});
await ensureIdentity();              // who I am (returned), connection work awaited
// A store is named by its reference — `doc_create` takes one, and the session is where
// upstream gets it. Omit it and the document goes to the private store, as upstream does.
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
await docs.sparqlUpdate(sid, `INSERT DATA { … }`, doc);
// Reading is by SHAPE, over the SDK's own scope. `did:ng:i` is the whole local dataset;
// naming documents in `graphs` reads exactly those. There is no "list my documents" call
// and no way to scope a read by store — neither exists upstream.
// It reads through the SDK's own shape query (`RdfOrmSubscription.getOrCreate`, passed to
// `configure` as `ormSubscription`), and hands back the shape's own objects: `@graph`,
// `@id` and one field per predicate the shape DECLARES.
const query = useShapeQuery(NoteShape, { graphs: "did:ng:i" });
// The ORM's one-shot read and its subscription-free write, at the SDK's own names and
// signatures. Import them from HERE, never from `@ng-org/orm`: one wallet hosts every
// identity, so the unwrapped read returns everybody's objects and the unwrapped write
// reaches anybody's document. Wrapped, the read comes back filtered by what you hold and
// the write is refused unless you own the document its `@graph` names.
const notes = await getObjects(NoteShape, { graphs: "did:ng:i" });
await insertObject(NoteShape, { "@id": "", "@graph": doc, title: "a note" });

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, docs } 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.
// A stranger holding only that reference reads it with ONE anchored query — the anchor
// scopes the read to that document's own graph, and the key is fetched from the public
// store on the way in. `readUnion([publicDoc])` did this until `1.0.0-dev.8`; it was
// this call in a loop, so the loop is yours now and the call is unchanged.
await docs.sparqlQuery(sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, publicDoc);

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
// ✓ validated at the door — `anchor` is a `NuriLike`, so nothing to narrow here
if (saved) await docs.sparqlQuery(sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, saved);