Files
ng-eventually/packages/sdk
Sylvain Duchesne f378c71739 docs: sept affirmations sur NextGraph requalifiées à la source
Lot F de la revue adverse. Chaque affirmation relue dans `nextgraph-rs` par symbole avant
d'être réécrite ; aucune ne s'est révélée exacte.

- `AddLink` / `RemoveLink` / `RepoLinkV0` étaient présentés comme **implémentés**. Leurs
  arms de vérificateur sont des `Ok(())`, là où celui d'`AddRepo` fait un vrai travail, et
  rien ne les construit. Le tableau dit désormais « déclaré, stubbé », et la conclusion qui
  en déduisait « le registre existe, seule la livraison manque » est corrigée : **les deux
  bouts** sont déclarés-et-stubbés.
- La table `inboxes` était dite « reconstruite vide à chaque session » — elle est
  repeuplée au chargement, et la clé privée d'inbox est persistée par repo. L'argument de
  sécurité qui s'appuyait dessus repose maintenant sur le bon motif : la table est **par
  vérificateur**, pas éphémère.
- La citation de « `doc_create` laisse `inbox: None` » pointait un constructeur réservé aux
  tests ; re-ciblée sur le chemin de production.
- `ExtObjectGet` était dit « le seul » primitif accessible à un non-membre et exigeant les
  clés : il y en a trois, et sa structure n'a aucun champ de clé.
- L'en-tête de `public-store.ts` était marqué **VERIFIED** alors qu'il repose sur un
  commentaire de doc, et la condition qu'il citait (« si les brokers pairs l'autorisent »)
  disparaissait de la conclusion. Requalifié en **pari**, condition rétablie.
- Deux sur-restrictions corrigées (ce qu'écrit le traitement d'un `ContactDetails`, et le
  prétendu « miroir 1:1 » de `NuriV0`, qui a dix champs).

**Sur la grammaire du ReadCap, une correction de MA correction.** J'avais écrit que la
forme `{target}:r:{cap}` était notre invention. Faux : le segment `r:` et son encodage sont
ceux d'amont, et l'auteur de NextGraph l'a énoncé. Ce qui est établi est plus étroit —
aucun parseur amont n'accepte aujourd'hui un NURI de repo qui le porte, et le segment est
produit comme valeur de champ. J'avais conclu d'une implémentation absente à ce que la
cible ferait, ce que la doctrine du projet interdit nommément. Seule « P1b remplace la
valeur, pas la forme » est corrigée, requalifiée en **pari**.

**Et la surface ne publie plus de type que personne n'utilise.** `export * from
"./model/types"` publiait huit types en bloc ; c'est une liste nommée de six. `ReadCap`
sort — aucune signature publiée ne le prend ni ne le rend, seules deux fonctions privées
de `inbox.ts` s'en servent — et `InboxScope` aussi. Un type n'est publié que si une
signature publiée l'utilise.

197 tests, 0 échec ; les trois typechecks propres ; `lint` sans erreur.
2026-08-10 12:09:20 +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();              // 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.
  • 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. 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