fix: l'identifiant est dans l'URL avant qu'init() ne l'emporte

@ng-org/web fait déjà la redirection vers le broker — même hôte, même forme,
même test de cadre (dist/ngweb.js). Le polyfill n'a donc rien à réimplémenter
là : ce qui lui revient, c'est la seule chose qu'init() ne peut pas faire, à
savoir mettre ?ng-id= dans window.location.href avant qu'il ne le lise.

rememberIdentity() s'exécute désormais sur les trois chemins — identité posée
par l'appelant, venue du stockage, ou saisie à la barrière. Le cas du stockage
était le silencieux : le paramètre restait absent, l'iframe lisait une identité
vide et provisionnait un second espace virtuel, sans erreur.

L'écriture dans le stockage et celle dans la barre d'adresse sont deux try
indépendants : un stockage qui refuse d'écrire ne doit pas emporter avec lui le
paramètre, qui est le seul à franchir la frontière de partition.

Le contrat retire l'obligation « être ouverte via la redirection du broker » :
elle n'a jamais été celle de l'application. Ni broker, ni iframe, ni redirection
n'y sont plus nommés.
This commit is contained in:
Sylvain Duchesne
2026-08-11 11:59:57 +02:00
parent 9c487b59f3
commit fc3c129bd3
3 changed files with 238 additions and 8 deletions
@@ -37,6 +37,21 @@
*
* Getting this wrong does not fail loudly: the iframe reads an empty identity, provisions
* a second virtual user, and the returning user silently lands in an empty space.
*
* ── The hand-over to the broker is `init()`'s, and stays there ────────────
* The round-trip is a redirect to NextGraph's login, which opens the wallet and loads the
* application back INSIDE an iframe it hosts. **`ng.init()` already performs it** — it
* compares `window.self` to `window.top` and, when top-level, navigates to its own
* redirect URL built from `window.location.href` (`@ng-org/web`, `dist/ngweb.js`). The
* level below covers the need, so this module implements NOTHING of it: doubling it would
* be code to delete at migration that diverges in the meantime (it did, briefly — a
* `sessionStorage` loop guard here made the hand-over not happen where upstream's does).
*
* What upstream cannot do is the ORDER, because it does not know this parameter exists:
* 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.
*/
import {
@@ -113,16 +128,35 @@ function storedIdentity(): string | null {
}
}
/** Put the identifier where the round-trip can find it, then remember it locally. */
/**
* Put the identifier where the round-trip can find it, and remember it locally.
*
* Called on EVERY path that settles an identity — typed at the gate, read from storage,
* or already set by the caller — and that is the whole point. `init()` hands the broker
* `window.location.href` as it finds it, so a path that settles an identity without
* writing the param sends the round-trip off without it: the iframe reads no identity,
* provisions a second virtual user, and the returning user lands in an empty space with
* no error anywhere. The identity coming from storage is the case that bites, since
* storage is precisely what does NOT cross the partition.
*
* Two independent effects, deliberately not one `try`: the address bar is what crosses
* the frontier and storage is only same-partition convenience, so a storage that refuses
* writes (private mode, quota) must not cost the round-trip its parameter.
*/
function rememberIdentity(id: string): void {
try {
globalThis.localStorage?.setItem(STORAGE_KEY, id);
} catch {
// Same-partition convenience only: a reload will ask again, the round-trip still works.
}
try {
const url = new URL(globalThis.location!.href);
url.searchParams.set(URL_PARAM, id);
globalThis.history?.replaceState(null, "", url.toString());
} catch {
// Nothing to do: without the param the round-trip loses the identity and the gate
// will ask again, which is the safe failure.
// No location, or a document forbidden to rewrite its URL (sandboxed): the round-trip
// loses the identity and the gate asks again, which is the safe failure. Nothing here
// can substitute for it — the URL `init()` hands over is the browser's, not ours.
}
}
@@ -222,6 +256,13 @@ function askForIdentity(cfg: SharedWalletConfig): Promise<string> {
* 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.
*
* **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.
*
* **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
@@ -233,12 +274,16 @@ function askForIdentity(cfg: SharedWalletConfig): Promise<string> {
export async function ensureIdentity(): Promise<PrincipalId> {
const already = getCurrentUser();
if (already !== null) {
rememberIdentity(already);
await connected();
return already;
}
const known = storedIdentity();
if (known) {
// 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);
await connected();
return known;