Skip to content

feat: pluggable session storage, and a cross-subdomain sign-in hint - #153

Merged
sea-snake merged 1 commit into
feat/auth-prompt-hintfrom
feat/auth-delegation-storage
Aug 24, 2026
Merged

feat: pluggable session storage, and a cross-subdomain sign-in hint#153
sea-snake merged 1 commit into
feat/auth-prompt-hintfrom
feat/auth-delegation-storage

Conversation

@sea-snake

@sea-snake sea-snake commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The other half of AuthClient's persistence, 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 itself stays in localStorage, per origin, 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, and this is the part to read hardest. One 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.

Read the notification path next. Every change source — the storage event, the cookie watchers, and the Cookie Store API where it exists — routes through one snapshot comparison, so a single change notifies subscribers once rather than once per source.

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.

Tests: local-session-storage.test.ts and cookie-session-storage.test.ts, including that discard leaves the hint for the siblings while remove takes it.

@sea-snake
sea-snake requested a review from a team as a code owner August 23, 2026 10:51
Copilot AI lite review requested due to automatic review settings August 23, 2026 18:18
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch from 78e9443 to 27b1bdb Compare August 23, 2026 18:18
@sea-snake sea-snake changed the title feat: pluggable delegation storage, and a cross-subdomain sign-in hint feat: pluggable session storage, and a cross-subdomain sign-in hint Aug 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new synchronous, observable “session storage” abstraction for persisting a non-secret DelegationChain, with implementations for same-origin storage (localStorage) and cross-subdomain sign-in signaling via a hint cookie.

Changes:

  • Add SessionStorage / Session interfaces for synchronous session persistence and change subscriptions.
  • Implement LocalSessionStorage (localStorage-backed, same-tab + cross-tab notifications).
  • Implement CookieSessionStorage (composes local storage with a domain-scoped hint cookie + multi-source change detection) and add tests for both.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/client/local-session-storage.test.ts Adds coverage for localStorage round-tripping and notification behavior.
tests/client/cookie-session-storage.test.ts Adds coverage for cookie-gated session restore, hint parsing, and multi-source notifications.
src/client/session-storage.ts Defines the new Session and SessionStorage API contracts.
src/client/local-session-storage.ts Implements localStorage-backed session storage with subscriber notifications.
src/client/cookie-session-storage.ts Implements cross-subdomain hint cookie behavior and consolidated change detection.
src/client/index.ts Exports the new session storage types and implementations from the client entrypoint.
Suppressed comments (2)

src/client/cookie-session-storage.ts:116

  • seconds is computed with Math.floor, which can drop the hint cookie up to ~999ms before expiresAtMs. That makes get() treat the session as signed-out early (cookie absent) even though the delegation may still be valid for the remainder of the second.
    const hint = deriveHint(session);
    const seconds = hint === null ? 0 : Math.floor((hint.expiresAtMs - Date.now()) / 1000);
    if (hint === null || seconds <= 0) {

src/client/cookie-session-storage.ts:213

  • #snapshot() concatenates two arbitrary strings with a | delimiter. If either side ever contains |, different states can collapse to the same snapshot string and subscribers may miss a change. Use a structured encoding (e.g. JSON.stringify([..])) to avoid delimiter collisions.
  #snapshot(): string {
    return `${localStorage.getItem(this.key) ?? ''}|${readCookie(this.key) ?? ''}`;
  }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/client/cookie-session-storage.ts Outdated
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch 2 times, most recently from 6656c45 to 181d1c8 Compare August 23, 2026 23:00
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. It carries the account key alongside the chain because
the chain is rooted at the session's own key, not the account's, so the key is
the only record of who is signed in until something mints.

SessionStorage (get/set/remove/discard/subscribe over Session) holds only
non-secret material, which is what lets it be synchronous and observable where
identity storage cannot be. LocalSessionStorage keeps it in localStorage, per
origin, and notifies other tabs through the storage event; every change source
routes through one snapshot comparison, so a single change notifies subscribers
once rather than once per source.

toBase64/fromBase64 move out of auth-client into base64.ts, since the store has
to encode the account key and both callers want the same pair.

remove() and discard() exist because ending a session and finding out about one
are different acts. remove() retracts anything the store publishes beyond itself,
which is what signing out means; discard() drops only this store's copy, so a
reader sharing the same session is not told its own has gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake force-pushed the feat/auth-identity-storage branch from 82bb670 to 9df96a7 Compare August 24, 2026 05:42
@sea-snake
sea-snake force-pushed the feat/auth-delegation-storage branch from 181d1c8 to b0b0c23 Compare August 24, 2026 05:42
Base automatically changed from feat/auth-identity-storage to feat/auth-prompt-hint August 24, 2026 05:42
@sea-snake
sea-snake merged commit b0b0c23 into feat/auth-prompt-hint Aug 24, 2026
4 checks passed
@sea-snake
sea-snake deleted the feat/auth-delegation-storage branch August 24, 2026 05:42
@sea-snake
sea-snake restored the feat/auth-delegation-storage branch August 24, 2026 05:45
@sea-snake

Copy link
Copy Markdown
Contributor Author

Not merged — nothing from this branch reached main, which is still at 3cf6063. GitHub marked this as merged during a stack restructure: when the branch below it was force-pushed, GitHub retargeted this PR's base onto a branch that already contained these commits, and treated that as a merge. Continued in #170.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants