fix: régler l'identité ne demande pas de session, se connecter oui
init() de @ng-org/web redirige vers le broker en première instruction, dès qu'on est en tête. L'application appelait donc init() au chargement du module, la page partait, et ensureIdentity() ne s'exécutait jamais : la barrière n'apparaissait pas, ?ng-id= restait absent de l'URL remise au broker, et un primo-arrivant se retrouvait devant la page de connexion sans portefeuille et sans moyen d'en obtenir un — sans la moindre erreur. Appeler ensureIdentity() avant init() ne marchait pas non plus : il attend la session, que seul le callback d'init() résout. Cycle vérifié empiriquement. La cause n'était ni l'ordre ni la redirection, mais une confusion dans ensureIdentity() entre deux actes de nature différente — régler qui est l'utilisateur (barrière, URL, stockage : aucune session) et se connecter (session requise). settleIdentity() porte le premier ; le wrapper init() du polyfill l'attend avant de déléguer. L'invariant d'ordre est ainsi porté par la composition, pas par une consigne d'ordre d'appel que personne ne lit. Piège trouvé et épinglé en écrivant les tests : init() et ensureIdentity() dans le même tick montaient deux barrières, l'utilisateur répondait à l'une et l'autre ne se résolvait jamais. Le règlement en vol est désormais partagé.
This commit is contained in:
@@ -51,7 +51,8 @@
|
||||
* it hands over `window.location.href` AS IT FINDS IT. So the one thing this module owes
|
||||
* the round-trip is that `?ng-id=` is already in the address bar when `init()` reads it —
|
||||
* on every path, whatever settled the identity. Hence {@link rememberIdentity} on all
|
||||
* three, and hence `ensureIdentity()` before `init()` in an application's bootstrap.
|
||||
* three, and hence {@link settleIdentity}, which the polyfill's own `init()` awaits before
|
||||
* it delegates (`surface/lifecycle.ts`) — so no application has to know that order.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -245,37 +246,68 @@ function askForIdentity(cfg: SharedWalletConfig): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
/** The settling in flight, shared by every caller — see {@link settleIdentity}. */
|
||||
let settling: Promise<PrincipalId> | null = null;
|
||||
|
||||
/**
|
||||
* Ensure an identity is set for this session, showing the gate only if one is missing.
|
||||
* Settle WHO the user is — and touch nothing else.
|
||||
*
|
||||
* The application calls this once, before it renders. It does NOT pass an identifier:
|
||||
* naming one is the step that will disappear, so it must not appear in the signature —
|
||||
* the day the wallet supplies the identity, this resolves without showing anything and
|
||||
* the caller's code is unchanged.
|
||||
* Resolve the identifier (URL, then storage, then the barrier), normalize it, and leave it
|
||||
* where the round-trip will find it. It needs a URL, a storage and a DOM; it needs **no
|
||||
* session**, and that absence is the entire reason this half exists on its own.
|
||||
*
|
||||
* A returning user never sees the gate: the identifier survives the broker round-trip in
|
||||
* the URL, and a plain reload finds it in storage.
|
||||
* ── Why signing in had to be cut in two ───────────────────────────────────
|
||||
* The two things `ensureIdentity()` does have opposite needs. Settling needs the page;
|
||||
* connecting needs the SESSION, which only `init()`'s callback establishes. Fused, they
|
||||
* made the ordering unsolvable: the identifier must reach the address bar BEFORE `init()`
|
||||
* reads it, yet calling `ensureIdentity()` first hangs — the connection half awaits
|
||||
* `getSession()`, and the session is what `init()` is on its way to open
|
||||
* (`emulated-verifier/connect.ts:84` → `account-registry.ts:592` → `session()` →
|
||||
* the consumer's thunk → the promise `init()`'s callback resolves).
|
||||
*
|
||||
* **Call it before `init()`.** Whatever settled the identity — typed, stored, or set by
|
||||
* the caller — this leaves `?ng-id=` in the address bar, and `init()` hands the broker the
|
||||
* address bar as it finds it. The other order signs the user in and then sends the
|
||||
* round-trip off without the identifier, which fails silently (see
|
||||
* {@link rememberIdentity}). It cannot be enforced from inside `init()`: this call awaits
|
||||
* the connection work, which awaits the session, which `init()` is what establishes.
|
||||
* 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.
|
||||
*
|
||||
* **It RETURNS the identity it settled**, and that is not a convenience — it is the only
|
||||
* way an application can know who it is. Upstream the question does not arise: an app
|
||||
* passes `user_id` to `session_start(wallet_name, user_id)` (`@ng-org/web`), having got it
|
||||
* from the wallet it opened, so it holds its identity before the session exists. Here the
|
||||
* GATE chooses it, so the gate is what hands it back. Without this the example
|
||||
* application had to read the gate's own private storage key — a boundary no consumer
|
||||
* should be able to see, let alone depend on.
|
||||
* 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.
|
||||
*
|
||||
* ── One barrier, however many callers ────────────────────────────────────
|
||||
* Settling now has TWO entry points — an application's `init()` and its
|
||||
* `ensureIdentity()` — and an application calls both, in the same tick
|
||||
* (`examples/notebook/app.ts`). Un-shared, each would mount its own barrier: the user
|
||||
* answers whichever is on top, the other stays pending forever, and the `init()` waiting on
|
||||
* it never hands the page to the broker. So a second caller JOINS the settling in flight
|
||||
* instead of asking again — the same reason `connectedUser()` keeps its work in flight
|
||||
* (`emulated-verifier/connect.ts`).
|
||||
*
|
||||
* @internal Not published: an application calls {@link ensureIdentity}, which is this plus
|
||||
* the connection work. Publishing the halves would invite a caller to sequence them, which
|
||||
* is the obligation this split removes.
|
||||
*/
|
||||
export async function ensureIdentity(): Promise<PrincipalId> {
|
||||
export function settleIdentity(): Promise<PrincipalId> {
|
||||
if (settling !== null) return settling;
|
||||
const run = resolveIdentity();
|
||||
settling = run;
|
||||
// Cleared on BOTH outcomes: a failure must stay retryable — a barrier nobody has answered
|
||||
// is not a settled identity — and clearing it here is why nothing has to reset it.
|
||||
const done = (): void => {
|
||||
if (settling === run) settling = null;
|
||||
};
|
||||
run.then(done, done);
|
||||
return run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the identifier the three ways it can arrive — already set by the caller, read
|
||||
* from the page (URL, then storage), or typed at the barrier — and remember it on every
|
||||
* one of them ({@link rememberIdentity}), which is the path-independent part that the
|
||||
* round-trip depends on.
|
||||
*/
|
||||
async function resolveIdentity(): Promise<PrincipalId> {
|
||||
const already = getCurrentUser();
|
||||
if (already !== null) {
|
||||
rememberIdentity(already);
|
||||
await connected();
|
||||
return already;
|
||||
}
|
||||
|
||||
@@ -285,7 +317,6 @@ export async function ensureIdentity(): Promise<PrincipalId> {
|
||||
// partition's storage, which the round-trip does not carry. The address bar does.
|
||||
rememberIdentity(known);
|
||||
setCurrentUser(known);
|
||||
await connected();
|
||||
return known;
|
||||
}
|
||||
|
||||
@@ -309,10 +340,44 @@ export async function ensureIdentity(): Promise<PrincipalId> {
|
||||
const normalized = normalizeIdentity(chosen);
|
||||
rememberIdentity(normalized);
|
||||
setCurrentUser(normalized);
|
||||
await connected();
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure an identity is set for this session, showing the gate only if one is missing.
|
||||
*
|
||||
* The application calls this once, before it renders. It does NOT pass an identifier:
|
||||
* naming one is the step that will disappear, so it must not appear in the signature —
|
||||
* the day the wallet supplies the identity, this resolves without showing anything and
|
||||
* the caller's code is unchanged.
|
||||
*
|
||||
* A returning user never sees the gate: the identifier survives the broker round-trip in
|
||||
* the URL, and a plain reload finds it in storage.
|
||||
*
|
||||
* **It no longer has to be called before `init()`** — the order is structural now. Whatever
|
||||
* settles the identity leaves `?ng-id=` in the address bar, and `init()` hands the broker
|
||||
* the address bar as it finds it; settling after the hand-over sends the round-trip off
|
||||
* without the identifier, which fails silently (see {@link rememberIdentity}). That used to
|
||||
* be a rule an application had to follow, and following it hung — so the polyfill's `init()`
|
||||
* awaits {@link settleIdentity} itself. This call is safe in any position: after `init()` it
|
||||
* finds the identity already set, and alongside it — which is what an application's
|
||||
* bootstrap actually does — it JOINS the settling in flight rather than raising a second
|
||||
* barrier. Either way it goes on to the connection work, which is what it adds.
|
||||
*
|
||||
* **It RETURNS the identity it settled**, and that is not a convenience — it is the only
|
||||
* way an application can know who it is. Upstream the question does not arise: an app
|
||||
* passes `user_id` to `session_start(wallet_name, user_id)` (`@ng-org/web`), having got it
|
||||
* from the wallet it opened, so it holds its identity before the session exists. Here the
|
||||
* GATE chooses it, so the gate is what hands it back. Without this the example
|
||||
* application had to read the gate's own private storage key — a boundary no consumer
|
||||
* should be able to see, let alone depend on.
|
||||
*/
|
||||
export async function ensureIdentity(): Promise<PrincipalId> {
|
||||
const settled = await settleIdentity();
|
||||
await connected();
|
||||
return settled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the connection work `setCurrentUser` fires — restoring what others shared
|
||||
* with this user, draining its inboxes — before this call resolves.
|
||||
|
||||
Reference in New Issue
Block a user