feat: let AuthClient take identity and session storage - #149
Conversation
Two optional constructor options that shape the authorize URL, for silent re-authentication against Internet Identity: - `prompt: 'none'` asks II to answer from a delegation it already holds and return without rendering anything (or fail with interaction_required), rather than running a ceremony. Omitting it (or 'login') signs in normally. - `hint` is the Principal to re-issue for, so II can pick which stored delegation a prompt=none request resolves to. Both are II-specific extensions inspired by OpenID Connect's prompt/login_hint, so they travel as query params on the authorize URL rather than in the ICRC request. Baking them into the URL at construction means one client per authorize intent; clients share storage, so whichever flow resolves populates the session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors AuthClient session persistence into two swappable layers: an identity storage responsible for key generation/persistence (algorithm-owned by the store), and a delegation storage responsible for synchronously storing/observing DelegationChain updates (including cross-tab and optional cross-subdomain signaling).
Changes:
- Introduces
AuthClientIdentityStorage(IdbStoragedefault, plusLocalStorageEd25519 fallback) andAuthClientDelegationStorage(SyncLocalStoragedefault, plusSyncCookieStoragehint-cookie variant). - Updates
AuthClientto use the new storages, addssubscribe()/dispose(), and makesisAuthenticated()synchronous by reading delegation expiry directly. - Adds/updates tests for the new storage implementations and updated
AuthClientbehavior (including redirect flow).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/client/sync-local-storage.test.ts | Adds unit tests for delegation localStorage round-trip and storage-event notifications. |
| tests/client/sync-cookie-storage.test.ts | Adds unit tests for hint-cookie gating, principal mismatch behavior, and notification triggers. |
| tests/client/storage.test.ts | Updates tests to cover identity-store semantics (create/get/remove) for both IndexedDB and localStorage implementations. |
| tests/client/auth-client.test.ts | Updates AuthClient tests to use new storages; adds coverage for external-change subscription reconciliation. |
| tests/client/auth-client-redirect.test.ts | Updates redirect-flow tests to validate memoized identity persistence across loads without pending-key machinery. |
| src/client/sync-local-storage.ts | Implements synchronous delegation storage in localStorage with cross-tab change notifications. |
| src/client/sync-cookie-storage.ts | Implements cross-subdomain hint-cookie delegation storage layered on SyncLocalStorage. |
| src/client/storage.ts | Replaces generic storage with AuthClientIdentityStorage; implements IdbStorage (ECDSA) + LocalStorage (Ed25519). |
| src/client/index.ts | Exports new storage interfaces/implementations and delegation storage key. |
| src/client/delegation-storage.ts | Introduces AuthClientDelegationStorage interface + DELEGATION_STORAGE_KEY. |
| src/client/db.ts | Removes legacy localStorage migration/clearing and relocates DB_VERSION here. |
| src/client/auth-client.ts | Refactors AuthClient to use identity/delegation storages; adds subscribe/dispose and synchronous isAuthenticated(). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
1e4f7b3 to
7dc655f
Compare
Splits the sign-in keypair out of AuthClient's combined storage into its own swappable store, one class or interface per file. IdentityStorage (create/set/get/remove -> SignIdentity) owns the key algorithm rather than taking it as an option: IdbIdentityStorage keeps a non-extractable ECDSA key pair in IndexedDB, LocalIdentityStorage an Ed25519 key in localStorage for environments without CryptoKey. Switching algorithms means switching the implementation. create() only mints an identity and set() persists it, so a caller can decide whether a ceremony that never completed should be allowed to overwrite what is already stored. Nothing selects a store yet; AuthClient keeps its own storage until the wiring two PRs up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7dc655f to
1b35a95
Compare
78e9443 to
27b1bdb
Compare
1b35a95 to
14d5efe
Compare
27b1bdb to
6656c45
Compare
14d5efe to
20ce1e9
Compare
Adds the second half of AuthClient's persistence as its own swappable store, holding a Session rather than a bare delegation chain. A session is what an application is given at sign-in and what its access derives from, so the record is named for what it is. SessionStorage (get/set/remove/discard/subscribe over Session) holds only non-secret material. LocalSessionStorage keeps it in localStorage and notifies other tabs through the storage event. CookieSessionStorage composes that and adds a domain-scoped hint cookie carrying the signed-in principal and the session's expiry, so sibling subdomains of one domain can see that a session exists and notice a sign-out. The session stays in localStorage, per origin and too large for a cookie, so the cookie carries no key material and no chain; each subdomain re-issues its own. readHint() is what lets an application decide whether to acquire silently. remove() and discard() exist because ending a session and finding out about one are different acts. A session serves every sibling of a domain, so an origin that discovers the chain it held is stale has learned something about itself: a sibling may have signed in a moment ago and written a hint that is perfectly good. remove() takes the hint with it, which is what signing out means; discard() leaves it, so the sibling that signed in is not told its session has gone. A domain the browser would refuse is rejected in the constructor. Setting such a cookie fails silently, and the hint is what makes a stored session count as live, so a session would appear to be forgotten the moment it was written. Every change source, the storage event, the cookie watchers and the Cookie Store API, routes through one snapshot comparison, so a single change notifies subscribers once instead of once per source. AuthClient starts using these in the PR above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6656c45 to
181d1c8
Compare
AuthClient now takes identityStorage and sessionStorage instead of one combined store, which is what lets an application share sign-in state across sibling subdomains by handing it CookieSessionStorage. The client is key-type agnostic as a result: the keyType option, the pending-key machinery, key (de)serialization, StoredKey and the localStorage migration are all gone, because the choice of algorithm now belongs to the identity store. The window flow persists the key only once a delegation is obtained, so a cancelled or failed ceremony can no longer overwrite the key of a session that is still valid. The redirect flow has to persist on the outbound load, since the key must survive the navigation, so #hydrate and #reconcile verify that the stored key matches the delegation's leaf public key and discard the session on a mismatch rather than assembling an identity that signs with an unauthorized key. subscribe() and dispose() expose the stores' change events and re-derive the identity on an external sign-in or sign-out. isAuthenticated() is synchronous, reading the session's expiry directly. Each concrete store owns its own storage slot, so no slot-key constants are exported. BREAKING CHANGE: removes the keyType option and the AuthClientStorage and StoredKey interfaces, along with the IdbStorage, LocalStorage and KEY_STORAGE_* exports; renames the storage option to identityStorage and adds sessionStorage. Existing sessions are not migrated and users re-authenticate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
20ce1e9 to
98fbafe
Compare
181d1c8 to
b0b0c23
Compare
|
Closing: this change is not separable the way the stack assumed. Making |
AuthClienttakesidentityStorageandsessionStorageinstead of one combined store, which is what lets an application share sign-in state across sibling subdomains by handing itCookieSessionStorage. The client is key-type agnostic as a result: thekeyTypeoption, the pending-key machinery, key serialization,StoredKeyand thelocalStoragemigration are all gone, because the choice of algorithm belongs to the identity store.The window flow persists the key only once a delegation is obtained, so a cancelled or failed ceremony can no longer overwrite the key of a session that is still valid.
The redirect flow has to persist on the outbound load, since the key must survive the navigation. To stop that corrupting a live session,
#hydrateand#reconcilecheck that the stored key matches the delegation's leaf public key and discard the session on a mismatch, rather than assembling an identity that signs with a key the delegation does not authorize. Read that pair hardest.subscribe()anddispose()expose the stores' change events and re-derive the identity on an external sign-in or sign-out.isAuthenticated()is synchronous, reading the session's expiry directly.Breaking, and the commit carries the footer:
keyType,AuthClientStorage,StoredKey,IdbStorage,LocalStorageand theKEY_STORAGE_*exports are removed,storageis renamed toidentityStorage, andsessionStorageis added. Existing sessions are not migrated, so users re-authenticate.Tests:
auth-client.test.tsandauth-client-redirect.test.ts.