fix: régler l'identité n'a plus le droit de réclamer une session
Le partage était cassé : Bob n'ouvrait pas le document qu'Alice venait de lui partager, sans erreur, juste « (illisible) ». Régression introduite en scindant ensureIdentity(). Chaîne observée aux sondes, pas déduite : settleIdentity() appelait setCurrentUser, qui déclenche startConnect(), qui va chercher la session via le thunk getSession de l'application. Or l'exemple appelle init() DEPUIS L'EXÉCUTEUR qui construit sessionReady — le thunk ne peut donc pas répondre, par construction. Il lève, resolveAccount rend null, l'exécution est abandonnée sans restauration ni drainage, mais s'est déjà enregistrée « en vol ». Le ensureIdentity() suivant rejoint cette exécution morte et se résout sans avoir rien fait. Avant la scission, rien n'appelait setCurrentUser pendant l'évaluation du module : la session existait, l'exécution était saine, et la rejoindre était sans danger. C'était bien une affaire de moment. bootstrap.ts scinde le setter : adoptCurrentUser enregistre qui agit, setCurrentUser reste « enregistrer + connecter » pour tous les autres appelants. La moitié sans session ne réclame donc plus de session, et se connecter redevient l'affaire du seul ensureIdentity(), attendu, là où une session existe. Ce que ça bloque, tracé avant de livrer : une application qui appellerait init() sans jamais appeler ensureIdentity() n'aurait plus de restauration en arrière- plan. Aucun appelant de ce genre n'existe, et avant la scission init() était un passthrough nu qui ne déclenchait rien — c'est une répartition rétablie, pas un comportement retiré. Reste connu, non corrigé : connectedUser mémorise toujours une exécution abandonnée. Le piège est documenté sur setCurrentUser.
This commit is contained in:
@@ -56,10 +56,10 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
adoptCurrentUser,
|
||||
getConfig,
|
||||
getCurrentUser,
|
||||
getStoreRegistryDeps,
|
||||
setCurrentUser,
|
||||
} from "./bootstrap";
|
||||
import { connectedUser } from "../emulated-verifier/connect";
|
||||
import type { PrincipalId } from "../model/types";
|
||||
@@ -268,9 +268,20 @@ let settling: Promise<PrincipalId> | null = null;
|
||||
* Split, the order stops being an instruction a caller can get wrong: `init()` awaits THIS
|
||||
* half (`surface/lifecycle.ts`), which completes with no session in existence.
|
||||
*
|
||||
* So nothing reachable from here may await `getSession()`. `setCurrentUser` is safe on
|
||||
* that count by construction — it FIRES the connection work without awaiting it
|
||||
* (`bootstrap.ts`), which is the property that keeps this half session-free.
|
||||
* So nothing reachable from here may CALL `getSession()` — not merely "may not await it".
|
||||
* That distinction is the one this half got wrong and shipped on. `setCurrentUser` looked
|
||||
* safe because it fires the connection work without awaiting it; but firing still RUNS it,
|
||||
* and its first line asks `resolveAccount`, which awaits the consumer's session thunk. The
|
||||
* thunk cannot answer here — settling runs before `init()` has been delegated to, and the
|
||||
* reference application builds its `sessionReady` promise around that very `init()` call,
|
||||
* so at that instant the promise does not exist yet. The thunk threw, `resolveAccount`
|
||||
* answered null, the run abandoned before restoring a single cap, and the `connectedUser()`
|
||||
* that {@link ensureIdentity} awaits JOINED that abandoned run rather than doing the work
|
||||
* (`emulated-verifier/connect.ts:56`, `:84`). Silently: a user simply could not read what
|
||||
* had been shared with it.
|
||||
*
|
||||
* Hence {@link adoptCurrentUser} below, which records who is acting and stops there. The
|
||||
* connecting is {@link ensureIdentity}'s, where it is AWAITED and where a session exists.
|
||||
*
|
||||
* ── One barrier, however many callers ────────────────────────────────────
|
||||
* Settling now has TWO entry points — an application's `init()` and its
|
||||
@@ -316,7 +327,7 @@ async function resolveIdentity(): Promise<PrincipalId> {
|
||||
// Even though `storedIdentity()` just read it: what it read may have come from THIS
|
||||
// partition's storage, which the round-trip does not carry. The address bar does.
|
||||
rememberIdentity(known);
|
||||
setCurrentUser(known);
|
||||
adoptCurrentUser(known);
|
||||
return known;
|
||||
}
|
||||
|
||||
@@ -339,7 +350,7 @@ async function resolveIdentity(): Promise<PrincipalId> {
|
||||
const chosen = await askForIdentity(cfg);
|
||||
const normalized = normalizeIdentity(chosen);
|
||||
rememberIdentity(normalized);
|
||||
setCurrentUser(normalized);
|
||||
adoptCurrentUser(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -379,15 +390,20 @@ export async function ensureIdentity(): Promise<PrincipalId> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the connection work `setCurrentUser` fires — restoring what others shared
|
||||
* with this user, draining its inboxes — before this call resolves.
|
||||
* Do the connection work — restoring what others shared with this user, draining its
|
||||
* inboxes — and do not resolve until it has actually run.
|
||||
*
|
||||
* **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
|
||||
* **Not a convenience: a correctness fix, found by the applicative e2e.** An application
|
||||
* that rendered without it 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).
|
||||
*
|
||||
* It is now the ONLY thing that connects the settled identity, and that is deliberate.
|
||||
* Settling used to fire this work too, in the background, before a session could exist —
|
||||
* so the run poisoned itself and this await joined it instead of doing the job. One
|
||||
* connection, started where the session is reachable and awaited by whoever asked to sign
|
||||
* in, is the shape that cannot go wrong (see {@link settleIdentity}).
|
||||
*
|
||||
* 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
|
||||
|
||||
@@ -209,24 +209,55 @@ export function resetStoreRegistry(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current identity id — who the SDK is reading/writing as. In the target
|
||||
* this is the wallet user established at wallet-import time; here the consumer
|
||||
* relays that id through this call so the read filter and the inbox `from` know
|
||||
* who is acting. Passing `null` clears it (no identity yet, e.g. during startup).
|
||||
* Record who is acting — and touch nothing else. Answers whether it CHANGED, which is
|
||||
* what decides whether there is any connecting to do.
|
||||
*
|
||||
* Split out of {@link setCurrentUser} so that the two things it did — naming the identity
|
||||
* and reaching for the session on its behalf — can be asked for separately. Only the
|
||||
* session-FREE half of signing in uses this one ({@link ./access-gate}.settleIdentity),
|
||||
* and that is the whole of why it exists: see the warning on {@link setCurrentUser}.
|
||||
*
|
||||
* @internal Never published. A consumer names its identity through the access gate.
|
||||
*/
|
||||
export function setCurrentUser(id: PrincipalId | null): void {
|
||||
export function adoptCurrentUser(id: PrincipalId | null): boolean {
|
||||
const changed = currentUser !== id;
|
||||
currentUser = id;
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current identity id — who the SDK is reading/writing as — **and connect it**.
|
||||
* In the target this is the wallet user established at wallet-import time; here the
|
||||
* consumer relays that id through this call so the read filter and the inbox `from` know
|
||||
* who is acting. Passing `null` clears it (no identity yet, e.g. during startup).
|
||||
*
|
||||
* ── This call REACHES THE SESSION, and a caller must know it ──────────────
|
||||
* "Fire-and-forget" says nothing about whether the session is touched — only about who
|
||||
* waits. The fired work asks `resolveAccount` on its first line, which awaits the
|
||||
* consumer's `getSession` thunk (`connect.ts` → `account-registry.ts:280`). So this
|
||||
* setter is unusable at any moment the session cannot yet answer, and calling it there is
|
||||
* not merely wasteful — it **poisons** the work: the thunk throws, `resolveAccount`
|
||||
* returns null, the run abandons before restoring anything, and it is that abandoned run
|
||||
* that the next `connectedUser()` JOINS instead of doing the work (`connect.ts:56`).
|
||||
* Nothing anywhere throws; a user simply cannot read what was shared with it.
|
||||
*
|
||||
* That is not hypothetical — it shipped. The reference application calls the polyfill's
|
||||
* `init()` from inside the executor that is still building its own `sessionReady`
|
||||
* promise, so the thunk could not answer, by construction. Whoever settles an identity
|
||||
* before a session can exist wants {@link adoptCurrentUser} and an awaited
|
||||
* `connectedUser()` later.
|
||||
*
|
||||
* Gated on the registry being configured, and that is not a test convenience: an
|
||||
* identity set before the registry is wired has nothing to restore and no inbox to
|
||||
* reach. The consumer's real sequence is `configureStoreRegistry` then `setCurrentUser`;
|
||||
* anything else can call `connectedUser()` explicitly.
|
||||
*/
|
||||
export function setCurrentUser(id: PrincipalId | null): void {
|
||||
const changed = adoptCurrentUser(id);
|
||||
// Connecting a user is what triggers inbox processing — the library's job, not
|
||||
// the app's. Fire-and-forget: this setter is synchronous and every consumer calls
|
||||
// it from synchronous code, so the work announces itself through the cap
|
||||
// registry's change signal instead of making callers await. See `connect.ts`.
|
||||
//
|
||||
// Gated on the registry being configured, and that is not a test convenience: an
|
||||
// identity set before the session resolves has nothing to restore and no inbox to
|
||||
// reach, so firing would be I/O that can only fail. The consumer's real sequence
|
||||
// is `configureStoreRegistry` then `setCurrentUser`; anything else can call
|
||||
// `connectedUser()` explicitly.
|
||||
if (changed && id !== null && registryDeps !== null) startConnect();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user