feat: le polyfill possède la session et la normalisation des identités

Pour démarrer, une application devait écrire une promesse autour du callback
d'init(), attraper l'événement loggedin, puis fournir un thunk getSession qui
dépiaute session_id et les trois identifiants de store dans notre forme. Plus un
normalizeId. C'est précisément la plomberie que ce paquet existe pour absorber :
chaque application la réécrirait à l'identique, et c'est elle qui a produit deux
défauts aujourd'hui — un blocage et un partage cassé en silence.

En amont, une session est RENDUE ; une application n'en assemble jamais une à
partir de champs bruts. Et les identités virtuelles sont une invention du
polyfill, donc leur normalisation lui appartient.

Le wrapper init() enveloppe désormais le callback de l'appelant : il capture
l'événement, en dérive la session, puis appelle le callback avec le même
événement. Le paquet n'appelle jamais init de sa propre initiative — il
l'enveloppe. Sans callback, il capture quand même.

getSession et normalizeId quittent la surface publiée. Le chemin d'injection
reste pour les harnais, mais inatteignable depuis l'entrée : vérifié par un
import à l'exécution et par un configure() refusé à la compilation.

Défaut trouvé et corrigé en route : le broker envoie session_id en NOMBRE, et le
convertir en chaîne faisait refuser tous les appels par le binding wasm. La
valeur ne fait que transiter, elle est relayée telle quelle. Reste que toute la
chaîne la type string — inexactitude antérieure à ce commit, à traiter à part.

Une application écrit maintenant : configure({ ng, useShape, init, sharedWallet }).
This commit is contained in:
Sylvain Duchesne
2026-08-12 17:39:12 +02:00
parent 7a4d9b492f
commit cc8a95d303
20 changed files with 501 additions and 141 deletions
@@ -24,17 +24,43 @@ import { resetPublicStoreFetches } from "../emulated-verifier/public-store";
import { setAccessLog } from "./access-log";
import { inspectOutbox } from "./outbox-log";
import { startConnect } from "../emulated-verifier/connect";
import { resetSharedWalletSession, sharedWalletSession } from "./session";
/**
* Consumer-injected dependencies of the storeRegistry (polyfill-era). The
* registry itself is generic (it knows only native scopes); the consumer wires
* up how to reach the shared-wallet session and how to normalize an identity id
* used as the shim key. Removed at migration along with the whole shim.
* How an identity id becomes the key a virtual user is filed under.
*
* The package's rule, not a consumer's choice (2026-08-12): `@Alice`, `alice ` and `ALICE`
* are ONE person's space. It has to be one rule, because the identifier arrives from three
* places — typed at the barrier, read back from the URL after the broker round-trip, read
* from storage — and any of them keying differently silently opens a SECOND space whose
* documents the first one cannot see.
*
* **NO COUNTERPART.** Upstream there is nothing to normalize: a wallet holds one user, and
* `session_start` takes the id the wallet gives. This exists only because one wallet here
* hosts everybody, and it disappears with them.
*/
export function normalizeIdentityId(id: string): string {
return id.trim().replace(/^@/, "").toLowerCase();
}
/**
* Dependencies of the storeRegistry (polyfill-era) — INTERNAL, and no longer an
* application's business.
*
* The registry itself is generic (it knows only native scopes); these two say how to reach
* the shared-wallet session and how to key an identity in the shim. An application used to
* supply both through {@link EventuallyConfig}; the package owns them now (2026-08-12), and
* this interface is the substitution path its OWN suites use — the unit fakes need a
* synchronous session, and the e2e harness holds one the broker gave it directly.
*
* Internal is carried by the module, not by a comment: nothing here is re-exported from
* `src/index.ts`, so the published entry cannot reach it. Removed at migration with the
* whole shim.
*/
export interface StoreRegistryDeps {
/** Resolve the current shared-wallet session (id + private-store anchor). */
getSession: () => Promise<RegistrySession>;
/** Normalize an identity id for shim keying. Default: trim (identity-ish). */
/** Normalize an identity id for shim keying. Default: {@link normalizeIdentityId}. */
normalizeId?: (id: string) => string;
/**
* POINTER micro-guard budget. The account records now live in a subscribable
@@ -60,20 +86,29 @@ export interface StoreRegistryDeps {
* "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.
*
* ── Two fields left on 2026-08-12, and what left with them ────────────────
* `getSession` and `normalizeId` were published, and both made an application build
* something the target never asks it for:
*
* - `getSession` — upstream a session is RETURNED (`init()`'s callback delivers it,
* `session_start` hands one back). An application that has to ASSEMBLE one out of
* `session_id` / `private_store_id` / … is coding against a shape with no successor.
* The package captures the event instead ({@link ../surface/lifecycle}.init) and holds
* the session ({@link ./session}).
* - `normalizeId` — the identities it normalizes are this package's own invention (one
* wallet, many virtual users). There is nothing upstream to normalize, so there was
* nothing for a consumer to decide; see {@link normalizeIdentityId}.
*
* Every consumer wrote the same plumbing for both, and the reference integration's copy
* carried two defects at once. Both remain substitutable through {@link StoreRegistryDeps},
* which the published entry cannot reach.
*/
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.
@@ -138,16 +173,14 @@ export function configure(c: EventuallyConfig): void {
// skip the barrier on a top-level page, which is the one thing it exists to prevent.
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 } : {}),
});
}
// Wire the registry onto what the PACKAGE owns. Unconditional since 2026-08-12: there is
// no longer anything for a caller to supply here, so there is no longer a case where
// configuring the library leaves the registry half-wired. A suite that needs its own
// session or key rule calls `configureStoreRegistry` AFTER this, and overrides it.
configureStoreRegistry({
getSession: sharedWalletSession,
...(c.pointerGuard ? { pointerGuard: c.pointerGuard } : {}),
});
}
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */
@@ -157,10 +190,13 @@ export function getConfig(): EventuallyConfig {
}
/** Reset the injected config back to un-configured (mainly for tests, so a
* suite that calls configure() can restore the not-configured guard state). */
* suite that calls configure() can restore the not-configured guard state).
* The captured session goes with it: it arrived through the config's `init`, so leaving
* it behind would hand the next `configure()` the previous one's session. */
export function resetConfig(): void {
cfg = null;
currentUser = null;
resetSharedWalletSession();
}
/**
@@ -189,7 +225,7 @@ export function configureStoreRegistry(deps: StoreRegistryDeps): void {
};
registryDeps = {
getSession,
normalizeId: deps.normalizeId ?? ((id: string) => id.trim()),
normalizeId: deps.normalizeId ?? normalizeIdentityId,
// Default: single read (no re-read). Only the real-broker consumers (app + e2e)
// opt into the bounded pointer micro-guard; unit fakes stay synchronous.
pointerGuard: deps.pointerGuard ?? { attempts: 1 },