Skip to content

feat: share a sign-in across sibling subdomains - #173

Open
sea-snake wants to merge 5 commits into
feat/auth-idle-boundfrom
feat/auth-cookie-session-storage
Open

feat: share a sign-in across sibling subdomains#173
sea-snake wants to merge 5 commits into
feat/auth-idle-boundfrom
feat/auth-cookie-session-storage

Conversation

@sea-snake

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

Copy link
Copy Markdown
Contributor

A cookie is the only thing that crosses between origins, so putting the state in one is what lets chat.example.com and hr.example.com share a sign-in, and what lets a sign-out on either end it for both.

  • It carries no chain and no key. A sibling reads who is signed in and until when, and asks the identity provider to re-issue for itself rather than treating what it read as proof.
  • Nothing raises an event when a cookie changes and no BroadcastChannel crosses origins, so a sibling's change is seen by looking: the Cookie Store API where the browser has it, and otherwise a re-check when the page is shown or the window regains focus.
  • A domain the browser would refuse is refused here instead, naming both sides. A malformed cookie belonging to something else is skipped rather than allowed to break every read.

This is where held earns its keep. A cookie cannot carry a per-origin fact — every sibling reads the same bytes — so the store composes a localStorage companion and reports held by comparing its principal against the cookie's. Comparing rather than counting matters because an expired record is kept on purpose: a sibling signing in as someone else then publishes a cookie this origin has no credential for, and asking only whether some local record exists would read that as held.

Two behaviour fixes the cookie makes reachable:

  • onSessionGone used to clear the state along with the credentials. Across origins that means retracting the record a sibling's ceremony just wrote, telling the sibling that did sign in that its session is gone and taking both down. It now discards this origin's claim and leaves the record standing. #hydrate gets the same correction: credentials rooted at an account the state no longer names are dropped, not ended, for the same reason.
  • getIdentity() refuses rather than handing back an anonymous identity when the record names an account this origin holds nothing for. Anonymous there is the dangerous answer: calls would go out unauthenticated while the record says someone is signed in. SessionNotHeldError is where a silent re-issue belongs.

Read hardest: CookieStateStorage.get() composing held, and the getIdentity() guard.

@sea-snake
sea-snake requested a review from a team as a code owner August 24, 2026 05:47
@sea-snake
sea-snake requested a lite review from Copilot August 24, 2026 05:51

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

Adds a new CookieSessionStorage implementation to let sibling subdomains share sign-in awareness via a domain-scoped hint cookie, while keeping the actual delegation chain per-origin in localStorage.

Changes:

  • Introduces CookieSessionStorage that composes LocalSessionStorage and maintains a cross-subdomain hint cookie (principal + expiry).
  • Exports the new storage and types from the client entrypoint.
  • Adds a dedicated test suite covering cookie gating, hint derivation, and cross-source subscription notifications.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/client/cookie-session-storage.test.ts New tests validating hint semantics, cookie gating behavior, and subscription notifications.
src/client/index.ts Re-exports CookieSessionStorage and related types from the public client API.
src/client/cookie-session-storage.ts New SessionStorage implementation that adds a domain-scoped hint cookie and unified change notifications.
src/client/auth-client.ts Documentation updates referencing CookieSessionStorage as an option for sibling subdomains.

💡 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
Comment thread src/client/cookie-session-storage.ts Outdated
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from c52e0de to 01a9f71 Compare August 24, 2026 08:53
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 01a9f71 to 5c7b257 Compare August 24, 2026 09:06
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 5c7b257 to d1f8042 Compare August 24, 2026 10:05
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from d1f8042 to be8ea31 Compare August 24, 2026 10:39
@sea-snake
sea-snake changed the base branch from feat/auth-tab-sharing to main August 29, 2026 10:22
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 3467ddd to 2ee585f Compare August 29, 2026 10:23
@sea-snake
sea-snake changed the base branch from main to feat/auth-foreground-refresh August 29, 2026 10:23
@sea-snake
sea-snake changed the base branch from feat/auth-foreground-refresh to main August 29, 2026 10:24
@sea-snake
sea-snake changed the base branch from main to feat/auth-foreground-refresh August 29, 2026 10:24
@sea-snake
sea-snake requested a lite review from Copilot August 29, 2026 10:26

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread src/client/cookie-state-storage.ts
Comment thread src/client/auth-client.ts
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 2ee585f to c91b392 Compare August 29, 2026 10:36
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from c91b392 to 43f0068 Compare August 29, 2026 10:53
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 43f0068 to f631600 Compare August 29, 2026 11:27
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from f631600 to aed2de1 Compare August 29, 2026 11:59
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from c1bbf80 to 35893ba Compare August 31, 2026 13:00
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 35893ba to 8a35fa3 Compare August 31, 2026 13:13
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 8a35fa3 to 5e20f0e Compare August 31, 2026 13:24
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 5e20f0e to f1025fa Compare August 31, 2026 13:53
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from f1025fa to 216e2db Compare August 31, 2026 14:10
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 216e2db to 6a5d59b Compare August 31, 2026 14:21
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 6a5d59b to 11faac6 Compare August 31, 2026 15:11
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 11faac6 to b3b1190 Compare August 31, 2026 15:50
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from b3b1190 to 72fb666 Compare August 31, 2026 16:09
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 72fb666 to 4d599f2 Compare August 31, 2026 17:07
A cookie is the only thing that crosses between origins, so putting the
state in one is what lets `chat.example.com` and `hr.example.com` share
a sign-in and what lets a sign-out on either end it for both: the record
becomes the domain's rather than this origin's, and removing it is what
tells a sibling the sign-in is over.

It carries no chain and no key. A sibling reads who is signed in and
until when, and asks the identity provider to re-issue for itself rather
than treating what it read as proof.

Nothing raises an event when a cookie changes and no channel crosses
origins, so a sibling's change is seen by looking: the Cookie Store API
where the browser has it, and otherwise a re-check when the page is
shown or the window regains focus, which is when the user is about to
act on the answer.

A domain the browser would refuse is refused here instead, naming both
sides. The silent version writes nothing and reads as a sign-in that
ended the moment it began.

Credentials the state no longer names are dropped on the next load,
which is how a sibling signing in as someone else reaches this origin.
@sea-snake
sea-snake force-pushed the feat/auth-cookie-session-storage branch from 4d599f2 to 7b54787 Compare August 31, 2026 17:56
@sea-snake
sea-snake changed the base branch from feat/auth-foreground-refresh to feat/auth-idle-bound August 31, 2026 22:55
@sea-snake
sea-snake changed the base branch from feat/auth-idle-bound to main August 31, 2026 22:55
@sea-snake
sea-snake changed the base branch from main to feat/auth-idle-bound August 31, 2026 22:56
Its record is the only one that reaches past a single origin, so its siblings
are the ones that arrive holding no credential and need the provider to have
kept the session. The other two are read by the origin that wrote them, and
asking a provider to persist a sign-in for them would buy nothing.
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