12eba6eea6
Un audit de la surface contre la source amont en a trouvé cinq ; voici les
quatre mécaniques. La cinquième — l'adresse d'inbox, qui traverse sept symboles
— relève du dessin et reste ouverte.
L'identifiant de session bloquait. Amont le déclare string | number
(sdk/js/web/src/index.ts:16) et le binding désérialise un u64 ; nous exigions
une chaîne. Une application ne pouvait donc pas passer la valeur que le SDK
venait de lui remettre. Élargi à ce qu'amont déclare, sur toute la chaîne, et
jamais converti : une chaîne échoue pour de vrai (Deserialization error of
session_id JsValue("1"), observé).
sparqlUpdate annonçait Promise<void> alors qu'il relayait DÉJÀ les commits.
C'était donc un mensonge de typage, pas un comportement — et la doublure de test
qui rendait undefined, un état que le vrai broker ne produit jamais, est ce qui
l'a laissé sans contradicteur.
ng était publié en Record<string, any>, ce qui perdait les 88 membres typés
d'amont — 88, pas 77 : le chiffre de notre propre documentation était faux.
Et materialize, second nom publié de read, sans appelant ni contrepartie amont,
est retiré.
docs/api-contract.md qualifiait docs.* de passthrough « 1:1 ». C'était faux sur
les deux premiers points. Corrigé, pas complété : un document qui se déclare
vérifié et qui ment est pire qu'un document absent, parce qu'on cesse d'aller
voir.
Une déviation assumée : amont type le retour en any, interdit ici ; on rend
unknown, comme sparqlQuery le fait déjà pour le même any amont.
151 lines
7.0 KiB
TypeScript
151 lines
7.0 KiB
TypeScript
/**
|
|
* The wallet session, held by the PACKAGE — never assembled by an application.
|
|
*
|
|
* ── Why this is not the application's business ────────────────────────────
|
|
* Upstream a session is RETURNED: `session_start` hands one back, and `init()`'s callback
|
|
* delivers `{ status: "loggedin", session }` (`@ng-org/web` `dist/ngweb.js:124`, VERIFIED —
|
|
* the callback is invoked once, with the `Session` the broker just opened). Nowhere does an
|
|
* application build a session out of raw fields.
|
|
*
|
|
* It did here, and only here: every consumer had to wrap `init()` in a promise, catch that
|
|
* event, and hand the library back a thunk unwrapping `session_id` / `private_store_id` /
|
|
* `protected_store_id` / `public_store_id`. Identical plumbing in every application, with
|
|
* nothing to migrate to — and it went wrong twice in the reference integration alone (a
|
|
* deadlock, and a share that silently reached nobody). So the package captures the event
|
|
* itself (`surface/lifecycle.ts`) and keeps the session here.
|
|
*
|
|
* ── What survives migration ───────────────────────────────────────────────
|
|
* NO COUNTERPART as a module: upstream nothing relays the session, because the SDK holds
|
|
* the one the wallet opened. What survives is the application-side gesture this removes —
|
|
* which is none at all.
|
|
*/
|
|
|
|
import type { RegistrySession } from "./account-registry";
|
|
|
|
/**
|
|
* The session as it stands, and a promise for the first one to arrive.
|
|
*
|
|
* Both, because the two questions differ: a caller after the fact wants the CURRENT session
|
|
* (a reconnection opens a new one, and reads must route through it), while a caller during
|
|
* startup has to wait for the first. Answering the first question with a settled promise
|
|
* would pin the very first session forever.
|
|
*/
|
|
let current: RegistrySession | null = null;
|
|
let announce!: (s: RegistrySession) => void;
|
|
let arrival = openArrival();
|
|
/**
|
|
* Has `init()` been called — is a session on its way at all?
|
|
*
|
|
* {@link sharedWalletSession} WAITS, and waiting is only right while a session can still
|
|
* arrive. Before `init()` has been delegated to, nothing in the system will ever open one:
|
|
* the wait is unbounded and answerless, which is the worst shape a failure can take — no
|
|
* error, no timeout, the application simply stops where it awaited. The polyfill's `init()`
|
|
* notes itself here as it goes through (`surface/lifecycle.ts`), and that one fact separates
|
|
* "not yet" from "never" with no timeout, no race and no guessing.
|
|
*/
|
|
let expected = false;
|
|
|
|
function openArrival(): Promise<RegistrySession> {
|
|
return new Promise<RegistrySession>((resolve) => {
|
|
announce = resolve;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The session this package holds — the current one, or the first to arrive.
|
|
*
|
|
* It WAITS rather than refusing: before `init()` has been delegated to, no session can
|
|
* exist and nothing else will make one. A thunk that threw there is what shipped the
|
|
* silent-abandon defect (`emulated-verifier/connect.ts` swallows the throw, the run
|
|
* abandons, and the caller that joins it resolves having restored nothing).
|
|
*/
|
|
export function sharedWalletSession(): Promise<RegistrySession> {
|
|
return current !== null ? Promise.resolve(current) : arrival;
|
|
}
|
|
|
|
/**
|
|
* Note that `init()` has been called — a session is on its way. See {@link expected}.
|
|
*
|
|
* @internal Called by the polyfill's `init()`, and by nothing else: it is a statement about
|
|
* that call having happened, not a switch anyone may set.
|
|
*/
|
|
export function expectSession(): void {
|
|
expected = true;
|
|
}
|
|
|
|
/**
|
|
* Can a session still arrive through this holder — because one is already here, or because
|
|
* `init()` has been called and will deliver one?
|
|
*
|
|
* A caller that awaits when this answers `false` waits forever. It is deliberately not a
|
|
* refusal by itself: the holder's job is to answer, and who should refuse is whoever is
|
|
* about to wait ({@link ./access-gate}.ensureIdentity).
|
|
*/
|
|
export function sessionIsComing(): boolean {
|
|
return current !== null || expected;
|
|
}
|
|
|
|
/**
|
|
* Read a lifecycle event, and keep the session if it carries one.
|
|
*
|
|
* Answers whether it did, so a caller can tell "the session landed" from "some other
|
|
* event went by" without reaching in. Every event that is not a `loggedin` carrying a
|
|
* session is ignored — the callback is a general lifecycle channel, and inventing a
|
|
* session out of a partial event would be worse than having none.
|
|
*
|
|
* ── The session id is RELAYED, never rebuilt — and that is load-bearing ────
|
|
* Upstream declares it `string | number` (`Session`, `index.d.ts:266`) and the broker
|
|
* returns a NUMBER; the chain below here now types it the same way and hands it to `ng.*`,
|
|
* whose binding takes it as-is. So this reads the field and passes it on untouched. It is
|
|
* not a detail: normalizing it to a string was written here first, and the applicative e2e
|
|
* refused every call in the batch with `Deserialization error of session_id JsValue("1")`
|
|
* — the wasm side deserializes the id by its own type, and a stringified number is not it.
|
|
*
|
|
* The chain used to narrow it to `string`, which made the value the SDK hands an application
|
|
* unpassable through this library. Widened to upstream's own declared type on 2026-08-14, at
|
|
* the capture and along every hop that relays it; the id is still only ever RELAYED.
|
|
*/
|
|
export function captureSession(event: unknown): boolean {
|
|
if (typeof event !== "object" || event === null) return false;
|
|
const { status, session } = event as { status?: unknown; session?: unknown };
|
|
if (status !== "loggedin") return false;
|
|
if (typeof session !== "object" || session === null) return false;
|
|
const {
|
|
session_id: sessionId,
|
|
private_store_id: privateStoreId,
|
|
protected_store_id: protectedStoreId,
|
|
public_store_id: publicStoreId,
|
|
} = session as {
|
|
session_id?: string | number;
|
|
private_store_id?: string;
|
|
protected_store_id?: string;
|
|
public_store_id?: string;
|
|
};
|
|
// An event missing either anchor is not a session; the id is checked for PRESENCE only,
|
|
// since its runtime type is the broker's to choose and ours to relay.
|
|
if (sessionId === undefined || sessionId === null) return false;
|
|
if (typeof privateStoreId !== "string") return false;
|
|
current = {
|
|
sessionId,
|
|
privateStoreId,
|
|
...(typeof protectedStoreId === "string" ? { protectedStoreId } : {}),
|
|
...(typeof publicStoreId === "string" ? { publicStoreId } : {}),
|
|
};
|
|
announce(current);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Forget the captured session (a fresh `configure()`, or a test).
|
|
*
|
|
* The pending promise is REPLACED rather than left resolved: a suite that reset and then
|
|
* awaited again must wait for the next session, not be handed the previous one's.
|
|
*/
|
|
export function resetSharedWalletSession(): void {
|
|
current = null;
|
|
arrival = openArrival();
|
|
// Including the expectation: it was set by an `init()` on the config being discarded, and
|
|
// a suite that reset and then awaited must be told the truth about the NEXT one.
|
|
expected = false;
|
|
}
|