Repository navigation
fix(auth-client): keep the redirect flow batched and its derivation origin stable - #124
Merged
Merged
Conversation
…rigin stable Bumps @icp-sdk/signer to ^5.6.0 and fixes two UrlTransport redirect-flow issues that only surface with real network/keygen timing — e2e mocks missed them, but a deployed 1-click SSO + attributes flow hit both: - Batch the delegation with a concurrently issued request (e.g. from requestAttributes) into one redirect. signIn now starts session-key acquisition before opening the channel and acquires the key inside a memoize producer, so the transport's batch flush is held across the async keygen + storage write instead of firing while only the faster request is buffered — which split the flow into two redirects, the second of which failed. - Preserve the derivation origin across the redirect. The return load reconstructs the client from a query-less callback URL, so options.derivationOrigin is gone; journaling it via memoize (now usable in the constructor, since 5.6.0's memoize returns synchronously for a sync producer) replays the original value so a re-issued request still carries it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…origin through it `AuthClient.memoize` now mirrors the transport's shape — synchronous for a sync producer, a promise for an async one (awaiting stays safe either way). That lets the constructor read a memoized value synchronously, so the derivation-origin journaling drops its `this.#urlTransport ? … : …` ternary and becomes a single `this.memoize(...)` call — window mode is handled by memoize's own passthrough. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the auth client’s redirect-mode behavior to keep concurrently-issued requests batched into a single redirect and to persist derivationOrigin across the redirect replay, aligned with the updated @icp-sdk/signer behavior.
Changes:
- Bump
@icp-sdk/signerfrom^5.5.0to^5.6.0(lockfile updated accordingly). - Adjust
AuthClientredirect flow to (a) hold batching across async session-key acquisition and (b) journalderivationOriginso it replays after returning to a query-less callback URL. - Update redirect-flow tests and the fake URL transport to reflect polymorphic
memoizebehavior and new journal slots.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/client/auth-client.ts |
Reworks redirect flow timing + journals derivation origin; updates memoize API shape. |
tests/client/fake-url-transport.ts |
Updates fake transport memoize to be polymorphic (sync/async) like the real transport. |
tests/client/auth-client-redirect.test.ts |
Adds derivation-origin-survives-redirect coverage; updates journal shape assertions. |
package.json |
Bumps @icp-sdk/signer dependency range to ^5.6.0. |
pnpm-lock.yaml |
Locks @icp-sdk/signer to 5.6.0 and updates snapshot entries. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
MRmarioruci
approved these changes
Jul 24, 2026
…andled rejection signIn starts session-key acquisition before awaiting openChannel. If openChannel (or another await before it is consumed) throws first, sessionKeyPromise is never awaited, so a later acquisition failure would surface as an unhandled rejection while background keygen/storage work continued. Attach a no-op rejection handler when it is started; the await still observes and propagates a rejection when reached. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sea-snake
added a commit
that referenced
this pull request
Aug 4, 2026
Closes #120. ## Root cause The error reported in #120 — `InvalidStateError: Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing` — means the connection was already closed by the time a storage operation ran. Nothing in this library (or in `idb`) ever calls `db.close()`, so the closure comes from the browser: Chrome is known to force-close IndexedDB connections out from under pages (see e.g. [Dexie #613](dexie/Dexie.js#613), [localForage #581](localForage/localForage#581)), and constant full-page reload churn on Vite localhost dev servers makes this far more likely than on a deployed asset canister. `IdbKeyVal` cached one connection for its whole lifetime with no recovery path, so a single force-close turned every subsequent storage call into this error. ## Changes - **`IdbKeyVal` now survives force-closed connections** (`src/client/db.ts`): the connection promise is dropped via `openDB`'s `terminated` hook when the browser reports an abnormal close, and — since Chrome does not always fire that event — an `InvalidStateError` thrown by an operation also drops the dead connection, reopens, and retries the operation once. A failed open is never cached. - **`IdbStorage._db` memoizes the in-flight creation** (`src/client/storage.ts`): concurrent first accesses previously each started their own `IdbKeyVal.create()`, opening duplicate connections that leaked (the fix suggested in #120). This became internally reachable with the 8.0.0 redirect flow, which touches storage concurrently with session hydration. - **`signIn()`/`signOut()` wait for session hydration** (`src/client/auth-client.ts`): both now await the memoized `#init()` before touching storage, so their writes/deletes cannot interleave with the constructor's restore reads. In `signIn()` the await is placed after `openChannel()` — which must stay the first await so a popup opens in the same tick as the user's click — and after the eager session-key acquisition start, preserving the same-tick `transport.memoize` batching hold from #124. ## Tests - `tests/client/db-recovery.test.ts`: closes the underlying connection out from under `IdbKeyVal` (via a pass-through `openDB` wrapper) and verifies reads/writes transparently reopen and succeed. This reproduces the retry path; the `terminated` event path is not reproducible under `fake-indexeddb`, which never force-closes. - `tests/client/storage.test.ts`: verifies concurrent first accesses share a single `IdbKeyVal.create()` call, and that a failed open is not cached. `pnpm exec vitest run` (92 tests), `pnpm codestyle:check`, and `pnpm build` all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps
@icp-sdk/signerto^5.6.0and fixes twoUrlTransportredirect-flow issues. Both only surface with real network / keygen timing, so the e2e mocks passed while a deployed 1-click SSO + attributes flow hit both.1. Batch the delegation with a concurrent request
Promise.all([signIn(), requestAttributes()])should produce one redirect carrying[delegation, attributes]. It instead split into two — and the second (a loneicrc34_delegation) failed on the provider.Cause: the transport coalesces concurrently-issued requests by flushing on a
setTimeout(0)macrotask once nomemoizeproducer is in flight.requestAttributesbuffers its request after a quick nonce memoize, butsignInonly buffers its delegation afteropenChanneland an IndexedDB key restore/generate — work the flush didn't wait for. So the flush fired with only the attributes request buffered.Fix:
signInnow (a) starts session-key acquisition beforeopenChannel(so the memoize registers in the same tick), and (b) acquires the key inside amemoizeproducer, capturing it in a closure so there's no post-producer storage read. The in-flight count stays raised across the keygen, holding the flush until the delegation is buffered too.2. Preserve the derivation origin across the redirect
The return load reconstructs
AuthClientfrom a query-less callback URL, sooptions.derivationOrigin(like the identity provider) is gone. A re-issued request would drop it.Fix: journal it —
derivationOrigin: this.#urlTransport.memoize(() => options.derivationOrigin?.toString()). This relies on 5.6.0'smemoizereturning synchronously for a sync producer, so it's usable directly in the constructor. On replay the original value comes back from the journal. The memoize is unconditional (calling it only when set would desync the call-order journal between the fresh and return loads). The window flow is unaffected — it passesderivationOrigindirectly.Tests
tests/client/auth-client-redirect.test.ts: added a derivation-origin-survives-redirect test; updated the journal-shape assertions for the new slots. The fake transport now mirrors 5.6.0's polymorphicmemoize. Full suite green (81),tsc+ biome clean.Requires
@icp-sdk/signer@5.6.0(the polymorphicmemoize+ redirect-target persistence).