refactor: renommer client → sdk, et fusionner les deux portes en une

Deux mouvements de surface, aucun changement de comportement.

**`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.**
« client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est
tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans
`docs/source-layout-by-fate.md` et le tableau des paquets du README.

**Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs —
`configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types
— vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`.

Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on
importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule
porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de
`docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc
lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par
`test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de
vocabulaire sur les noms publiés.

**Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le
choix visible plutôt qu'hérité :

- `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par
  `shared-wallet/bootstrap` ;
- `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes
  par leur chemin interne, ce qui est leur raison d'être ;
- le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux
  fois brouillait la frontière qu'elle servait à marquer.

Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` /
`hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au
permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait
`capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README
de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire.

179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le
broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
This commit is contained in:
Sylvain Duchesne
2026-08-07 11:05:19 +02:00
parent 0832338201
commit 0eb25286c8
85 changed files with 423 additions and 413 deletions
+123
View File
@@ -0,0 +1,123 @@
# @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.
**Four calls do not, and they are the whole of what you will delete:** `configure`,
`configureStoreRegistry`, `setCurrentUser`, `connectedUser`. They exist because one
shared wallet hosts every user; upstream, an application imports the SDK and each user
opens their own wallet. `src/index.ts` groups them under a heading that says so.
*(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 per-document encryption lands (P1b).
```ts
import {
// SDK-shaped — the real SDK replaces these in place.
ensureIdentity, storeRegistry, inbox, readUnion, docs,
// Polyfill-era — these go away, and they are the whole of what goes away.
configure, configureStoreRegistry, setCurrentUser,
} from "@ng-eventually/sdk";
configure({ ng: realNg, useShape: realUseShape, sharedWallet });
configureStoreRegistry({ getSession });
await ensureIdentity(); // who am I (shared wallet)
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.
- **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/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`](../../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
```