144 lines
7.7 KiB
Markdown
144 lines
7.7 KiB
Markdown
# @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`, `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`](../../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.
|
|
|
|
```ts
|
|
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, 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`](../../examples/notebook) for an application written against it,
|
|
which the e2e suite drives.
|
|
|
|
## How a document is reached — the three acts, and no others
|
|
|
|
```ts
|
|
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`](../../docs/readcap-and-nuri-model.md) § 0.
|
|
|
|
## The types carry that invariant
|
|
|
|
`Nuri` and `ReadCap` are **template literal types**, not `string` aliases:
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
const saved = localStorage.getItem("doc"); // string | null
|
|
if (saved) await readUnion([saved]); // ✓ validated at the door
|
|
```
|