Files
ng-eventually/packages/sdk
Sylvain Duchesne b5f05472d9 fix: second tour adverse — mes correctifs avaient quatre trous, dont un qui les annulait
J'ai relancé un adversaire sur les correctifs du commit précédent, comme la règle
l'exige. Il en a trouvé quatre. Le premier annulait la garde que je venais d'écrire.

**Le registre de propriété était écrivable depuis la surface publiée.** `assertMayWrite`
lit la propriété dans l'index de store de l'appelant — et `caps.open` marquait les
documents de STRUCTURE (les trois stores, les inbox) comme « créés par moi ». Donc un
porteur pouvait, par le `docs.sparqlUpdate` publié, insérer `contains "<n'importe quel
document>"` dans son propre index et s'en déclarer propriétaire. Démontré : Bob écrit dans
le document protégé d'Alice, et détourne l'inbox d'un de ses documents — exactement le
vecteur que le commit précédent prétendait fermer. `open` classe désormais sans marquer :
un document de structure n'est possédé par personne au sens de la paternité, donc les deux
moitiés de la garde répondent non, ce qui est correct.

**`inbox.post` acceptait n'importe quel NURI.** Déplacer `depositInto` hors de la surface
ne suffisait pas : `post` atteint la même porte, qui saute les deux gardes par
conception. Bob, ne détenant rien, écrivait quatre triplets dans le document d'Alice. En
amont la confusion est impossible — `InboxPost` scelle vers une CLÉ d'inbox et le broker
route par `inboxes: PubKey → RepoId` ; adresser un document n'est pas refusé, c'est
inexprimable. Le shim tient maintenant un index des inbox, l'équivalent émulé de ce que
le broker sait par construction, et `post` refuse ce qui n'en est pas une.

**Le filtre de lecture fuyait encore par les clés dunder.** `DeepSignalSet` expose la
collection brute sur `__raw__` / `__meta__` : `[...view]` rendait zéro élément pendant que
`view.__raw__` rendait le Set complet, tous utilisateurs confondus. Mon en-tête affirmait
qu'« une propriété simple ne porte aucun élément » — faux pour ce type.

**Et il cassait des membres légitimes** : ma liste blanche couvrait la moitié des
helpers d'itération, si bien que `toArray`, `reduce`, `first`, `take`, `drop`, `flatMap`
levaient sur les données du porteur lui-même. Tous filtrés désormais ; le refus ne vaut
que pour l'inconnu.

**Deux tests réparés à la source plutôt qu'en affaiblissant les gardes.** Le faux
`doc_create` de `inbox.test.ts` rendait une CONSTANTE — tous les documents créés étaient
le même NURI, donc la garde de propriété n'avait rien à distinguer et deux tests lisaient
l'inbox d'Alice sous l'identité de Bob sans que rien ne proteste. Et le harnais e2e
utilisait un document ordinaire comme inbox.

Enfin, mon propre cache d'inbox a reproduit la faute que la revue avait relevée ailleurs :
un mémo qui survit à sa session. Rattaché à `resetRegistryCache`.

189 tests unitaires (six régressions de plus), e2e 40/40 et applicatif 10/10 — après un
échec réseau non reproductible, relancé sans modification.
2026-08-07 14:26:32 +02:00
..

@ng-eventually/sdk

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 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 per-document encryption lands (P1b).

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/sdk";

configure({ ng: realNg, useShape: realUseShape, getSession, sharedWallet });
await ensureIdentity();   // resolves who I am, and waits for the connection work
const doc = await storeRegistry.createEntityDoc(me, "protected");
await docs.sparqlUpdate(sid, `INSERT DATA { … }`, doc);
const subjects = await readUnion(await storeRegistry.listMyEntityDocs(me, "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.
  • Inboxpost, 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/sdk";

// 1. CREATE — you hold its cap, with nothing to declare.
const doc = await storeRegistry.createEntityDoc(me, "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(me, "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