refactor(api): le bootstrap redescend de quatre appels à un

L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :

- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
  injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
  qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
  `getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
  passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
  sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
  exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
  identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
  le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
  commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
  sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
  le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
  la session EST la connexion — aucune application n'attend un second appel.

Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.

Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.

180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
This commit is contained in:
Sylvain Duchesne
2026-08-07 12:06:15 +02:00
parent b98fcaa77d
commit 0455a408b6
26 changed files with 205 additions and 114 deletions
+45 -41
View File
@@ -19,34 +19,36 @@ Per the design principle (`README.md` § *Design principle*): an absent implemen
## 1. Bootstrap and configuration
### Today — `@ng-eventually/sdk` (the POLYFILL-ERA block of `src/index.ts`; everything here is removed at migration)
### Today — `@ng-eventually/sdk`: **one call**
```ts
// all from shared-wallet/bootstrap.ts
// shared-wallet/bootstrap.ts
export interface EventuallyConfig {
ng: NgLike;
useShape: UseShapeLike;
sharedWallet?: SharedWalletConfig; // the gate's, § 2bis
ng: NgLike; // the REAL @ng-org/web ng
useShape: UseShapeLike; // the REAL @ng-org/orm useShape
getSession?: () => Promise<RegistrySession>; // the wallet session (a thunk)
normalizeId?: (id: string) => string;
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
sharedWallet?: SharedWalletConfig; // the gate's, § 2bis
currentUser?: PrincipalId;
debugAccessLog?: boolean;
init?: (...args: any[]) => any;
initNg?: (...args: any[]) => any;
}
export function configure(c: EventuallyConfig): void;
export interface StoreRegistryDeps {
getSession: () => Promise<RegistrySession>;
normalizeId?: (id: string) => string;
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
}
export function configureStoreRegistry(deps: StoreRegistryDeps): void;
// NOT published (2026-08-07, with the entry merge) — merging the two doors made
// publishing these a visible choice rather than an inherited one, and the choice is no:
// getConfig, getStoreRegistryDeps internal wiring; the surface reaches them by import
// resetConfig, resetStoreRegistry test resets; the suite reaches them the same way
```
**The count is the contract here.** The agreed target was two polyfill-era calls, or one; it had drifted to four, and each extra one was a reason the LIBRARY has rather than a need an application has. Four became one on 2026-08-07:
| Was published | Where it went |
|---|---|
| `configureStoreRegistry` + `StoreRegistryDeps` | folded into `configure` — two bootstrap calls existed because the library has two internals, which is not a reason a caller should pay |
| `setCurrentUser` | the access gate sets the identity (§ 2bis). An application naming its own identity is the gesture that INVERTS the model; it must not have a published call to reach for |
| `connectedUser` | `ensureIdentity` awaits it. Upstream, opening the session IS the connection — no application awaits a second call |
| `getConfig`, `getStoreRegistryDeps`, `resetConfig`, `resetStoreRegistry` | internal wiring and test resets, reached by their internal path (2026-08-07, with the entry merge) |
So an application's whole bootstrap is `configure({ … })` plus `await ensureIdentity()` — and the second of those keeps its call site after migration.
### Target
**NO COUNTERPART, by design.** The whole subject is the polyfill bootstrap: it exists to inject the real SDK without a hard import (build-alias safety). At migration the consumer initializes the real SDK directly, with the two calls in § 2, and `configure` / `configureStoreRegistry` are deleted (`docs/migration-guide.md` § 7). Nothing in the target takes an "injected `ng`".
@@ -548,16 +550,19 @@ export async function openDocumentInbox(doc: Nuri): Promise<Nuri>;
### Today
```ts
// PUBLISHED — the POLYFILL-ERA block of `src/index.ts`, both with no counterpart.
export function setCurrentUser(id: PrincipalId | null): void; // shared-wallet/bootstrap.ts
export async function connectedUser(): Promise<void>; // emulated-verifier/connect.ts
// NOT published (removed 2026-08-05) — identity persistence is the application's job
// upstream too, and asking the library who you signed in is a shared-wallet convenience:
// shared-wallet/virtual-users.ts IdentityStore, browserIdentityStore,
// VirtualUserStorage, ACCOUNT_STORAGE_KEY
// shared-wallet/bootstrap.ts getCurrentUser
// The access gate persists what IT needs (§ 2bis); nothing else has to be exposed.
// PUBLISHED: nothing. Identity is established by `ensureIdentity()` (§ 2bis) and the
// connection is awaited inside it.
//
// NOT published, and each removal is a gesture an application no longer performs:
// setCurrentUser (2026-08-07) naming one's own identity — the step that inverts the
// model. The gate does it; the e2e harness, which plays
// several identities on one page, reaches it internally.
// connectedUser (2026-08-07) awaited inside `ensureIdentity`; upstream, opening the
// session IS the connection.
// getCurrentUser (2026-08-05) an application knows who it signed in.
// IdentityStore, browserIdentityStore, VirtualUserStorage, ACCOUNT_STORAGE_KEY
// (2026-08-05) persisting an identity is the application's job
// upstream too; the gate persists what IT needs.
```
### Target
@@ -573,8 +578,8 @@ declare function user_disconnect(user_id: string): Promise<void>;
```
- `IdentityStore` / `browserIdentityStore` (the persisted identity id) — **NO COUNTERPART**; they exist only because every virtual user shares one wallet, and they are no longer published at all. Removed at migration (`docs/migration-guide.md` § 5).
- `setCurrentUser`**NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet. `getCurrentUser` was its read side and is gone from the surface: an application knows who it signed in.
- `connectedUser()` — the awaitable form of what the target does **automatically**: the recipient's verifier processes its inbox as messages arrive/at connection (`Verifier::inbox`, `engine/verifier/src/verifier.rs:1674`). VERIFIED at level 1 that no consumer call is needed upstream; the polyfill fires it from `setCurrentUser` for the same reason. A consumer should treat it as "await a deterministic start" (tests), not as an operation the future SDK will name.
- `setCurrentUser`**NO COUNTERPART**; the relay of an identity the broker cannot see. Disappears with the shared wallet, and is no longer published: the gate is the only caller an application needs.
- `connectedUser()` (internal since 2026-08-07) — the awaitable form of what the target does **automatically**: the recipient's verifier processes its inbox as messages arrive/at connection (`Verifier::inbox`, `engine/verifier/src/verifier.rs:1674`). VERIFIED at level 1 that no consumer call is needed upstream; the polyfill fires it from `setCurrentUser` for the same reason. A consumer should treat it as "await a deterministic start" (tests), not as an operation the future SDK will name.
---
@@ -625,21 +630,20 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
### `@ng-eventually/sdk` — `src/index.ts` (the only entry since 2026-08-07)
```text
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, RegistrySession, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, StoreRegistryDeps, UnionSubject, Unsubscribe, UseShapeLike, configure, configureStoreRegistry, connectedUser, docChangeType, ensureIdentity, init, initNg, ng, readUnion, setCurrentUser, subscribeDoc, subscribeDocs, useShape, watchShape
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, RegistrySession, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
docs: depositInto, docCreate, sparqlQuery, sparqlUpdate
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
```
**Of these, four are POLYFILL-ERA and have no target counterpart**`configure`,
`configureStoreRegistry`, `setCurrentUser`, `connectedUser` (plus the types
`EventuallyConfig`, `StoreRegistryDeps`, `RegistrySession`). They are the deletion list,
and `src/index.ts` groups them under a heading that says so. `ensureIdentity` is a fifth
in substance — the shared-wallet gate — but the *call site* survives (§ 2bis).
**Of these, exactly ONE is polyfill-era with no target counterpart**`configure` (plus
the types `EventuallyConfig`, `RegistrySession`). It is the deletion list, and
`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 (§ 2bis).
Six symbols the previous `/polyfill` entry published are gone from the surface entirely:
`getConfig` and `getStoreRegistryDeps` (internal wiring, reached through
`shared-wallet/bootstrap`), `resetConfig` / `resetStoreRegistry` / `resetCaps` (test
resets, reached by their internal path), and the direct `share` re-export — `inbox.share`
was always the same function, and publishing it twice blurred the boundary it was meant
to mark.
Nine symbols published before 2026-08-07 are gone from the surface: `configureStoreRegistry`
and `StoreRegistryDeps` (folded into `configure`), `setCurrentUser` and `connectedUser`
(§ 1), `getConfig` / `getStoreRegistryDeps` (internal wiring), `resetConfig` /
`resetStoreRegistry` / `resetCaps` (test resets), and the direct `share` re-export —
`inbox.share` was always the same function, and publishing it twice blurred the boundary
it was meant to mark.
+2 -2
View File
@@ -4,7 +4,7 @@
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/sdk/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.``src/index.ts` and `./polyfill``src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07; its polyfill-era symbols — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` and their types — now sit in a marked block of the single entry, and the machinery accessors and test resets it used to publish are no longer published at all.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07, and the polyfill-era block was then cut to ONE published call, `configure`. `configureStoreRegistry`, `setCurrentUser` and `connectedUser` became internal the same day — folded, replaced by the gate, and awaited inside it respectively; the machinery accessors and test resets are internal too. All four are inventoried below.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
**Labels** are those of `docs/api-contract.md`: **PASSTHROUGH (level 3/2, VERIFIED)**, **LEVEL-1 SHAPE (model VERIFIED, JS surface ASSUMED)**, **ASSUMPTION**, **NO COUNTERPART**. Level numbers per `README.md` § *The three references*: 3 = JS ORM, 2 = wasm binding (`@ng-org/web`), 1 = Rust engine. One label recurs here that the surface contract rarely needs: **NO COUNTERPART, shared-wallet machinery** — the code below the emulation's floor, which the target has no image of because the target has no shared wallet. Per the design principle, an absent implementation is never treated as evidence about the future.
@@ -139,7 +139,7 @@ The polyfill of capability-based read access: a Proxy view over the reactive set
export function startConnect(): void;
```
Fire-and-forget wrapper over the published `connectedUser()` (restore Links, then drain every inbox), called by `setCurrentUser` so inbox processing is the library's job, not the app's.
Fire-and-forget wrapper over `connectedUser()` (internal since 2026-08-07, awaited inside `ensureIdentity`) (restore Links, then drain every inbox), called by `setCurrentUser` so inbox processing is the library's job, not the app's.
- **LEVEL-1 SHAPE for the timing, VERIFIED**: upstream the recipient's verifier processes inbox messages as they arrive, with no consumer call (`Verifier::inbox``process_inbox`, `engine/verifier/src/verifier.rs:1674-1690`); firing on connection is the emulation's equivalent moment. The restore-before-drain order is a lib choice; upstream "restore" does not exist as a step (applied caps are already in the User branch replay).
- `startConnect` itself disappears at migration; the automatic-processing behaviour it fabricates is native.
+3 -3
View File
@@ -142,7 +142,7 @@ public/protected/private stores — on top of one shared wallet.
`listMyEntityDocs(id, scope)` (its own account, bounded — no cross-account fan-out).
- **Generic by construction.** The registry knows only the three native scopes,
zero application entity kind. The consumer application maps its entities to a scope
and injects the session + identity-id normalization via `configureStoreRegistry({
and injects the session + identity-id normalization through `configure({
getSession, normalizeId })` (`shared-wallet/bootstrap.ts`, published by the entry).
The `store≠document` two axes materialize here directly: the registry moves along
@@ -238,7 +238,7 @@ store-id:
Both resolve the native store ids from the injected session
(`RegistrySession.protectedStoreId` / `publicStoreId`, alongside the existing
`privateStoreId` anchor). The consumer application hands the whole session to the
lib at the one injection point (`configureStoreRegistry({ getSession })`) — that is
lib at the one injection point (`configure({ getSession })`) — that is
wiring, not placement logic; everything else in the consumer application speaks only
in scopes. If the session omits `protectedStoreId`, the non-private scopes fall back
to the private store rather than emit a broken NURI.
@@ -361,7 +361,7 @@ another name.
### Sharing, publication, and the recipient
- **`setCurrentUser(id)` (`shared-wallet/bootstrap.ts`)** — the SDK's "current identity" call.
- **`setCurrentUser(id)` (`shared-wallet/bootstrap.ts`, INTERNAL since 2026-08-07 — `ensureIdentity` is what an application calls)** — the SDK's "current identity" call.
It selects *whose* caps are consulted, lazily, so the delivered subset always
reflects the identity in effect at read time.
- **`inbox.share(doc, toUser)`** — the one sharing act the lib exposes. Recipients
+1 -1
View File
@@ -23,7 +23,7 @@ The four journeys the suite runs, and what each proves:
| Bob leaves a message on Alice's note, and only Alice reads it | A depositor FINDS the address from the note itself; depositing grants no reading |
| Each actor's list holds their own notes | The boundary, seen from the only place that matters: the screen |
It has also found three defects of its own, each an application-side one the harness could not see: `connectedUser()` had to be awaited at sign-in (a note just shared with you reads as unreadable otherwise), a stale answer stayed on screen beside a fresh question, and changing the scope did not refresh the list.
It has also found three defects of its own, each one the harness could not see. The first turned out to be a LIBRARY defect rather than an application one: the connection work had to be awaited at sign-in, or a note just shared with you read as unreadable — so `ensureIdentity` now awaits it, and `connectedUser` left the published surface. The other two were the application's: a stale answer stayed on screen beside a fresh question, and changing the scope did not refresh the list.
## Running it
+17 -20
View File
@@ -36,13 +36,10 @@ import {
readUnion,
storeRegistry,
subscribeDoc,
connectedUser,
type Nuri,
type Scope,
// Polyfill-era — these three go away, and they are the whole of what goes away.
// Polyfill-era — ONE call, and it is the whole of what goes away.
configure,
configureStoreRegistry,
setCurrentUser,
} from "@ng-eventually/sdk";
import { ng as realNg, init as realInit } from "@ng-org/web";
@@ -57,11 +54,11 @@ interface Note {
body: string;
}
// --- bootstrap: the two polyfill-era calls ---------------------------------
// --- bootstrap: ONE polyfill-era call --------------------------------------
//
// Everything else an application calls is SDK surface, preserved at migration. These
// two are the scaffolding: `configure` becomes inert (the app will import the real SDK)
// and the identity will come from the wallet instead of a barrier.
// Everything else an application calls is SDK surface, preserved at migration. This one
// is the scaffolding, and at migration it goes: the app imports the real SDK, and the
// identity comes from the wallet instead of a barrier.
let session: { session_id: string } | null = null;
const sessionReady = new Promise<{ session_id: string }>((resolve) => {
@@ -81,9 +78,6 @@ configure({
fileUrl: "/shared-wallet.ngw",
password: (globalThis as { __NOTEBOOK_WALLET_PASSWORD__?: string }).__NOTEBOOK_WALLET_PASSWORD__ ?? "",
},
});
configureStoreRegistry({
getSession: async () => {
const s = session ?? (await sessionReady);
return {
@@ -190,18 +184,18 @@ function currentIdentity(): string {
* Sign in. The library shows its access barrier when it needs one; the day the wallet
* supplies the identity, this resolves silently and nothing here changes.
*
* The `connectedUser()` await is not optional decoration, and the applicative e2e is
* what found that out: setting an identity FIRES the connection work restoring what
* others shared with you, draining your inboxes but does not wait for it. Render
* before it lands and a note someone just shared reads as unreadable, which looks like
* a permission problem and is a timing one. At migration this becomes the session
* opening, and the await stays exactly where it is.
* One await, and it covers everything: the gate resolves the identity AND waits for the
* connection work it fires (restoring what others shared with you, draining your
* inboxes). The application used to have to await that second part itself the
* applicative e2e is what found it out, because a note someone had just shared read as
* unreadable, which looks like a permission problem and is a timing one. The library
* absorbed it: upstream, opening the session IS the connection, and no application
* awaits a second call.
*/
async function signIn(): Promise<void> {
await ensureIdentity();
await sessionReady;
identity = readIdentityBack();
await connectedUser();
}
/** The library owns the identity; the app asks for it rather than remembering it. */
@@ -280,5 +274,8 @@ async function main(): Promise<void> {
void main();
// The e2e suite drives this app through the DOM. It exposes nothing else: a test that
// needed a back door would be testing something an application cannot do.
(globalThis as { __notebook?: unknown }).__notebook = { watchNote, setCurrentUser };
// needed a back door would be testing something an application cannot do. `watchNote`
// is here because reactivity has no visible surface in this UI yet — not as an escape
// hatch, and it takes no identity: switching user means reloading with another `?ng-id=`,
// exactly as switching upstream means opening another wallet.
(globalThis as { __notebook?: unknown }).__notebook = { watchNote };
+9 -9
View File
@@ -5,10 +5,11 @@ One entry point. Most of what it publishes has the same signature as the future
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.
**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
@@ -27,13 +28,12 @@ Per-symbol, with the target signature and an epistemic label on every claim:
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,
// Polyfill-era — one call, and it is the whole of what goes away.
configure,
} from "@ng-eventually/sdk";
configure({ ng: realNg, useShape: realUseShape, sharedWallet });
configureStoreRegistry({ getSession });
await ensureIdentity(); // who am I (shared wallet)
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"));
+11 -4
View File
@@ -17,9 +17,6 @@
import { ng as realNg, init as realInit } from "@ng-org/web";
import {
configure,
configureStoreRegistry,
setCurrentUser,
connectedUser,
docs,
subscribeDoc,
subscribeDocs,
@@ -33,7 +30,17 @@ import {
// application must not — but through the internal path, never the published entry.
// `storeRegistry` above is the app-facing slice; these are the shim internals.
import * as registryInternals from "../src/shared-wallet/account-registry";
import { getCaps, getCurrentUser, resetCaps } from "../src/shared-wallet/bootstrap";
// The harness plays SEVERAL identities on one page — something no application does, and
// the reason `setCurrentUser` / `configureStoreRegistry` are no longer published. It
// reaches them by their internal path, like the rest of its machinery.
import {
configureStoreRegistry,
setCurrentUser,
getCaps,
getCurrentUser,
resetCaps,
} from "../src/shared-wallet/bootstrap";
import { connectedUser } from "../src/emulated-verifier/connect";
import * as virtualUsers from "../src/shared-wallet/virtual-users";
import { ensureIdentity } from "@ng-eventually/sdk";
// The harness narrows for its OWN assertions; a consumer never has to (the entries take
+20 -8
View File
@@ -78,16 +78,28 @@ export type { NG } from "@ng-org/web";
* Inject the real SDK, and tell the library about the shared wallet. Upstream nothing
* is injected an application imports the SDK and opens its own wallet so this call
* is the shape of that absence. `docs/api-contract.md` § 1.
*
* **It is the ONLY call here**, and keeping it that way is the design target: an
* application's bootstrap should be one line to delete, not four.
*/
export { configure, configureStoreRegistry, setCurrentUser } from "./shared-wallet/bootstrap";
export type { EventuallyConfig, StoreRegistryDeps } from "./shared-wallet/bootstrap";
export { configure } from "./shared-wallet/bootstrap";
export type { EventuallyConfig } from "./shared-wallet/bootstrap";
export type { RegistrySession } from "./shared-wallet/account-registry";
/**
* Await the connection work `setCurrentUser` fires: restore what was shared with this
* user, and drain its inboxes. An application need not call it the work runs anyway
* but it may want to know it has finished. Upstream this is the session opening.
*/
export { connectedUser } from "./emulated-verifier/connect";
// --- what this block deliberately does NOT contain --------------------------
//
// Three calls were published here and removed on 2026-08-07, when the count had drifted
// to four against a target of two. Each removal is a thing an application no longer does:
//
// - `configureStoreRegistry` — folded into `configure`. Two bootstrap calls existed
// because the library has two internals, which is not a reason a caller should pay.
// - `setCurrentUser` — the access gate sets the identity (`ensureIdentity`, below).
// An application naming its own identity is the gesture that inverts the model, and
// it must not have a published call to reach for. The e2e harness plays several
// identities on one page and reaches it by its internal path, which is what a
// harness is allowed to do and an application is not.
// - `connectedUser` — `ensureIdentity` awaits it. Upstream, opening the session IS the
// connection; no application awaits a second call, so ours should not either.
// ── the access gate — polyfill-era in substance, one line in the app ────────
// One call before the app renders. It shows a technical barrier only while the shared
+23 -2
View File
@@ -45,6 +45,7 @@ import {
getStoreRegistryDeps,
setCurrentUser,
} from "./bootstrap";
import { connectedUser } from "../emulated-verifier/connect";
/**
* Normalize an identifier the SAME way the shim keys accounts on.
@@ -221,12 +222,12 @@ function askForIdentity(cfg: SharedWalletConfig): Promise<string> {
* the URL, and a plain reload finds it in storage.
*/
export async function ensureIdentity(): Promise<void> {
if (getCurrentUser() !== null) return;
if (getCurrentUser() !== null) return connected();
const known = storedIdentity();
if (known) {
setCurrentUser(known);
return;
return connected();
}
const cfg = getConfig().sharedWallet;
@@ -249,4 +250,24 @@ export async function ensureIdentity(): Promise<void> {
const normalized = normalizeIdentity(chosen);
rememberIdentity(normalized);
setCurrentUser(normalized);
return connected();
}
/**
* Wait for the connection work `setCurrentUser` fires restoring what others shared
* with this user, draining its inboxes before this call resolves.
*
* **Not a convenience: a correctness fix, found by the applicative e2e.** Setting an
* identity FIRES that work and does not wait for it. An application that rendered on
* `ensureIdentity()` alone could read a note someone had just shared with it as
* unreadable which looks like a permission problem and is a timing one, in the one
* place where the difference is invisible (nothing throws; a read is simply empty).
*
* Doing it here rather than exposing `connectedUser()` is the point: the awaited thing
* has NO counterpart upstream there, opening the session IS the connection, and no
* application awaits a second call. So the polyfill absorbs it, and an application's
* bootstrap keeps the shape it will still have after migration.
*/
async function connected(): Promise<void> {
await connectedUser();
}
+40 -8
View File
@@ -51,17 +51,40 @@ export interface StoreRegistryDeps {
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
}
/**
* Everything the polyfill needs, in ONE call.
*
* It used to take two `configure` for the SDK injection, `configureStoreRegistry` for
* the session because the two belonged to different internals. That is a reason the
* library has, not one an application should pay for: from a caller's side both are
* "here is what you need to run", and two bootstrap calls is one more thing to delete
* at migration than there needs to be. Merged 2026-08-07; the registry's own wiring
* function stays internal.
*/
export interface EventuallyConfig {
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
ng: NgLike;
/** The REAL `@ng-org/orm` `useShape`. */
useShape: UseShapeLike;
/**
* Resolve the wallet session. Shared-wallet only: upstream the session IS the user, so
* there is nothing to inject an application opens its wallet and the SDK knows.
* A thunk, so it may be given before the session exists.
*/
getSession?: () => Promise<RegistrySession>;
/** Normalize an identity id for shim keying. Default: trim. */
normalizeId?: (id: string) => string;
/**
* POINTER micro-guard budget see {@link StoreRegistryDeps.pointerGuard}. Left unset
* a single read, which keeps the synchronous unit fakes fast.
*/
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
/**
* The shared wallet this deployment hands out, and what the access gate needs to do
* it (`shared-wallet/access-gate.ts`). Absent no gate; the caller sets the identity
* itself. Disappears with the gate: upstream a user opens their own wallet.
*/
sharedWallet?: SharedWalletConfig;
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
ng: NgLike;
/** The REAL `@ng-org/orm` `useShape`. */
useShape: UseShapeLike;
/** Initial current user; may also be set later via {@link setCurrentUser}. */
currentUser?: PrincipalId;
/**
@@ -114,6 +137,16 @@ export function configure(c: EventuallyConfig): void {
cfg = c;
currentUser = c.currentUser ?? null;
setAccessLog(c.debugAccessLog ?? false);
// The session wiring is part of the same act — see {@link EventuallyConfig}. Omitted
// only by unit suites that never touch the registry; those get the same
// "must be configured" error they got before, from `getStoreRegistryDeps`.
if (c.getSession) {
configureStoreRegistry({
getSession: c.getSession,
...(c.normalizeId ? { normalizeId: c.normalizeId } : {}),
...(c.pointerGuard ? { pointerGuard: c.pointerGuard } : {}),
});
}
}
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */
@@ -130,10 +163,9 @@ export function resetConfig(): void {
}
/**
* Wire the storeRegistry's consumer-injected dependencies (session + identity-id
* normalization). Must be called before any storeRegistry.* use. Separate from
* {@link configure} because it's storeRegistry-specific and, like the shim,
* disappears at migration.
* Wire the storeRegistry's dependencies. INTERNAL since 2026-08-07: an application
* passes these to {@link configure}, which calls this. Still exported for the library's
* own suites, which wire the registry alone.
*/
export function configureStoreRegistry(deps: StoreRegistryDeps): void {
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,
+2 -1
View File
@@ -7,7 +7,8 @@
*/
import { getCurrentUser } from "../src/shared-wallet/bootstrap";
import { test, expect, afterEach } from "bun:test";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { ensureIdentity } from "../src/shared-wallet/access-gate";
+3 -1
View File
@@ -21,7 +21,9 @@
import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test";
import { setAccessLog, enabled, shortNuri } from "../src/shared-wallet/access-log";
import { docCreate, sparqlUpdate, sparqlQuery } from "../src/surface/docs";
import { configure, configureStoreRegistry, connectedUser, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { connectedUser } from "../src/emulated-verifier/connect";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
// ---------------------------------------------------------------------------
+2 -1
View File
@@ -26,7 +26,8 @@ import {
resetRegistryCache,
} from "../src/shared-wallet/account-registry";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
import { configure, configureStoreRegistry } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetOpenedRepos } from "../src/emulated-verifier/open-repo";
+2 -1
View File
@@ -23,7 +23,8 @@
import { describe, it, expect, mock, afterAll, beforeEach } from "bun:test";
import { ensureAccount, resolveWriteGraph, resetRegistryCache } from "../src/shared-wallet/account-registry";
import { resetOpenedRepos } from "../src/emulated-verifier/open-repo";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetInfrastructure } from "../src/emulated-verifier/reach";
+3 -1
View File
@@ -26,7 +26,9 @@ import {
} from "../src/shared-wallet/account-registry";
import { documentInboxAddress, openDocumentInbox } from "../src/emulated-verifier/branch-registers";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
import { configure, configureStoreRegistry, connectedUser, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { connectedUser } from "../src/emulated-verifier/connect";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { share } from "../src/surface/inbox";
import { post, postToDocument, read as readInbox } from "../src/surface/inbox";
+2 -1
View File
@@ -25,7 +25,8 @@ test("throws a clear error when configure() was not called", async () => {
});
// From here on, a fake real `ng` is injected via configure().
import { configure, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps } from "../src/shared-wallet/bootstrap";
function fakeNg() {
+2 -1
View File
@@ -2,7 +2,8 @@ import { test, expect, mock, beforeEach, afterAll } from "bun:test";
import { post, read, materialize, watch } from "../src/surface/inbox";
import { userInbox, resetRegistryCache } from "../src/shared-wallet/account-registry";
import type { Deposit } from "../src/surface/inbox";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
+2 -1
View File
@@ -21,7 +21,8 @@ import { test, expect, mock, afterAll } from "bun:test";
import { createEntityDoc, resetRegistryCache, userInbox, listMyEntityDocs } from "../src/shared-wallet/account-registry";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
import type { Nuri, ReadCap } from "../src/model/types";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { share } from "../src/surface/inbox";
import { read as readInbox } from "../src/surface/inbox";
+2 -1
View File
@@ -1,7 +1,8 @@
import { getCaps } from "../src/shared-wallet/bootstrap";
import { test, expect, mock, afterEach } from "bun:test";
import { makeNg } from "../src/surface/ng-proxy";
import { configure, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig } from "../src/shared-wallet/bootstrap";
// This suite injects a fake `ng` via configure() and declares WRITE caps —
+2 -1
View File
@@ -20,7 +20,8 @@
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test";
import { ensureRepoOpen, ensureReposOpen, resetOpenedRepos } from "../src/emulated-verifier/open-repo";
import { readUnion } from "../src/surface/read-model";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetInfrastructure } from "../src/emulated-verifier/reach";
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
+2 -1
View File
@@ -9,7 +9,8 @@ import { test, expect, mock, afterEach } from "bun:test";
import { exposeReadCap, fetchReadCap, resetPublicStoreFetches } from "../src/emulated-verifier/public-store";
import { mintCap } from "../src/emulated-verifier/caps";
import { getCaps } from "../src/shared-wallet/bootstrap";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import type { Nuri } from "../src/model/types";
+2 -1
View File
@@ -14,7 +14,8 @@ import { test, expect, mock, afterAll } from "bun:test";
import { sparqlQuery, sparqlUpdate, depositInto } from "../src/surface/docs";
import { createEntityDoc, resetRegistryCache, userInbox } from "../src/shared-wallet/account-registry";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { mayReach, mustNotAttempt } from "../src/emulated-verifier/reach";
import { hasReadCap } from "../src/model/nuri";
+2 -1
View File
@@ -2,7 +2,8 @@ import { getCaps } from "../src/shared-wallet/bootstrap";
import { test, expect, mock, afterAll } from "bun:test";
import { readUnion } from "../src/surface/read-model";
import type { Nuri } from "../src/model/types";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps } from "../src/shared-wallet/bootstrap";
// The cap registry is process-wide, so each inject() starts from an empty one:
+2 -1
View File
@@ -10,7 +10,8 @@ import {
resetRegistryCache,
} from "../src/shared-wallet/account-registry";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
import { configure, configureStoreRegistry } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
// This suite injects a fake `ng` via configure(); bun runs test files in a
+2 -1
View File
@@ -1,6 +1,7 @@
import { test, expect, mock, afterAll } from "bun:test";
import { subscribeDoc, subscribeDocs } from "../src/surface/subscribe";
import { configure, configureStoreRegistry } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import type { RegistrySession } from "../src/shared-wallet/account-registry";
+2 -1
View File
@@ -25,7 +25,8 @@
import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test";
import { watchShape } from "../src/surface/watch-shape";
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import { resetRegistryCache, createEntityDoc } from "../src/shared-wallet/account-registry";
import { resetOpenedRepos, setOpenTimeoutForTests, getSyncState } from "../src/emulated-verifier/open-repo";