fix(dashboard): persist sign-in with an HttpOnly session cookie - #384
Conversation
|
Warning Review limit reached
Next review available in: 53 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (49)
WalkthroughThe dashboard now exchanges the master key for a server-side, TTL-limited HttpOnly session cookie. Backend authorization, rotation, persistence, startup invalidation, frontend state, tests, documentation, and generated assets were updated accordingly. ChangesDashboard session authentication
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Implements persistent admin-dashboard authentication by exchanging the master key once for a server-issued, HttpOnly session cookie, replacing the previous per-tab sessionStorage bearer-key approach. This improves operator UX (survives new tabs/restarts) while reducing browser-side exposure of the master key.
Changes:
- Backend: adds DB-backed dashboard sessions (
dashboard_sessions) plus/v1/auth/session(POST to mint, DELETE to revoke) and cookie-aware auth inapi/deps.py. - Backend: revokes sessions on master-key change across restarts and re-mints the rotating caller’s session during generated master-key rotation.
- Frontend: removes master-key storage/header auth, switches login/logout flows to session-cookie semantics, and updates tests/build artifacts accordingly.
Reviewed changes
Copilot reviewed 50 out of 52 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| web/src/pages/UsersPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/UsagePage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/ToolsGuardrailsPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/SettingsPage.tsx | Removes client-side master-key swapping on rotation; relies on server re-mint. |
| web/src/pages/SettingsPage.test.tsx | Updates tests to session marker + “no Authorization header” expectations. |
| web/src/pages/ProvidersPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/OverviewPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/ModelsPage.test.tsx | Removes apiClient master-key setup from page tests (cookie-auth model). |
| web/src/pages/KeysPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/BudgetsPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/AliasesPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/pages/ActivityPage.test.tsx | Removes master-key header setup from page tests (cookie-auth model). |
| web/src/components/UpdatePrompt.tsx | Updates copy to reflect cookie-based sign-in persistence. |
| web/src/components/UpdatePrompt.test.tsx | Removes master-key header setup from component tests (cookie-auth model). |
| web/src/components/PricingWarning.test.tsx | Removes master-key header setup from component tests (cookie-auth model). |
| web/src/components/Login.tsx | Switches login to POST /v1/auth/session, then marks authenticated state. |
| web/src/components/Login.test.tsx | Verifies key exchange flow and that the raw key is not stored client-side. |
| web/src/auth/AuthContext.tsx | Replaces stored master key with a non-secret localStorage session marker; logout revokes server session best-effort. |
| web/src/auth/AuthContext.test.tsx | Adds coverage for restoring marker state and logout revocation behavior. |
| web/src/App.test.tsx | Updates app auth setup from sessionStorage key to localStorage marker. |
| web/src/api/client.ts | Removes master-key header injection; adds createSession and deleteSession helpers and keeps 401/403 bounce behavior. |
| tests/unit/test_master_key_service.py | Updates unit tests to new is_valid_master_key symbol name. |
| tests/unit/test_deps_db_error.py | Updates unit tests to new is_valid_master_key symbol name. |
| tests/unit/test_dashboard_session.py | Adds unit tests covering mint, cookie auth, revocation, expiry, restart behavior, and rotation reminting. |
| src/gateway/static/dashboard/index.html | Updates committed dashboard entrypoint to new asset hash. |
| src/gateway/static/dashboard/assets/UsersPage-DjQ_32XA.js | Committed rebuilt dashboard bundle reflecting cookie-auth changes. |
| src/gateway/static/dashboard/assets/UsagePage-DU8IagMv.js | Committed rebuilt dashboard bundle reflecting cookie-auth changes. |
| src/gateway/static/dashboard/assets/ToolsGuardrailsPage-qp13etCg.js | Committed rebuilt dashboard bundle reflecting cookie-auth changes. |
| src/gateway/static/dashboard/assets/SettingsPage-C0JYm9ni.js | Removes old committed settings bundle artifact. |
| src/gateway/static/dashboard/assets/SettingsPage-BzPdd2gR.js | Adds new committed settings bundle artifact. |
| src/gateway/static/dashboard/assets/OverviewPage-DXIwdDWG.js | Committed rebuilt dashboard bundle reflecting cookie-auth changes. |
| src/gateway/static/dashboard/assets/ModelScopeControl-Bpbo36Ko.js | Committed rebuilt dashboard bundle reflecting rebuild/hash changes. |
| src/gateway/static/dashboard/assets/KeysPage-CbUCEimJ.js | Committed rebuilt dashboard bundle reflecting rebuild/hash changes. |
| src/gateway/static/dashboard/assets/AliasesPage-AOThQmDL.js | Committed rebuilt dashboard bundle reflecting rebuild/hash changes. |
| src/gateway/static/dashboard/assets/ActivityPage-Dm6r6wPy.js | Committed rebuilt dashboard bundle reflecting rebuild/hash changes. |
| src/gateway/services/dashboard_session_service.py | Adds session token mint/verify/revoke helpers, cookie helpers, and startup key-change revocation. |
| src/gateway/models/entities.py | Adds DashboardSession ORM entity for DB-backed dashboard sessions. |
| src/gateway/main.py | Hooks startup revocation on master-key change; excludes cookie-auth paths from OpenAPI security stamping. |
| src/gateway/core/config.py | Adds dashboard_session_ttl_hours config field for session TTL. |
| src/gateway/api/routes/settings.py | Updates master-key rotation to revoke all sessions and re-mint the caller’s session cookie when applicable. |
| src/gateway/api/routes/auth_session.py | Adds /v1/auth/session POST/DELETE endpoints for cookie-based dashboard sessions. |
| src/gateway/api/main.py | Registers the new auth-session router in standalone mode. |
| src/gateway/api/deps.py | Adds cookie-session authentication path and makes verify_master_key cookie-aware. |
| docs/public/otari.postman_collection.json | Adds Postman requests for session create/delete; updates rotation description. |
| docs/public/openapi.json | Adds OpenAPI schemas/paths for /v1/auth/session and updates rotation docs. |
| alembic/versions/a9c1e3b5d7f9_add_dashboard_sessions_table.py | Adds migration creating dashboard_sessions table + index. |
| AGENTS.md | Updates repository docs to describe cookie-based dashboard sessions and new endpoint. |
Files not reviewed (2)
- src/gateway/static/dashboard/assets/SettingsPage-BzPdd2gR.js: Generated file
- src/gateway/static/dashboard/assets/index-D1FfVwkg.js: Generated file
There was a problem hiding this comment.
Approved. Solid, well-tested implementation. Correcting two findings from my initial review below.
Correction (both withdrawn):
- My "
Securedropped behind a TLS-terminating proxy" concern was wrong.request_is_https()(dashboard_session_service.py) deliberately honorsX-Forwarded-Protoregardless of source, andcreate_sessionmints the cookie withsecure=request_is_https(request), so the PaaS/proxy case is handled, notrequest.url.scheme == "https"as I stated. - The related "HTTPS
Securetest gap" was also wrong:test_https_requests_get_a_secure_cookieandtest_forwarded_https_gets_a_secure_cookieboth cover it. The cross-PR #388 concern rested on these, so it's withdrawn too.
Remaining points (both minor, inline): the unauthenticated/unthrottled sign-in endpoint, and Path=/ extending the cookie to inference routes (the code owns both tradeoffs in docstrings).
Verified correct: token entropy/hashing (secrets.token_urlsafe(32) → SHA-256, PK lookup no meaningful timing oracle); the str | None return change on verify_master_key (only rotation binds it); rotation-via-cookie CAS preserves concurrent-rotation rejection; restart survival via revoke_sessions_on_master_key_change + re-mint. Lifecycle coverage (header-wins-over-cookie, cross-site rejection, server-side revocation, expiry, restart) is thorough.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/gateway/api/routes/auth_session.py (1)
33-37: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winBound
master_keyin the request schema.
master_keyhas no length limits, so an unbounded string can be POSTed to this pre-auth endpoint. As per coding guidelines,src/gateway/api/**/*.pyrequires validating request bodies with constrained Pydantic models, including bounds.🔒 Suggested bound
- master_key: str = Field(description="The gateway master key; verified once and never stored by the browser.") + master_key: str = Field( + min_length=1, + max_length=256, + description="The gateway master key; verified once and never stored by the browser.", + )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/gateway/api/routes/auth_session.py` around lines 33 - 37, Update the CreateSessionRequest Pydantic model to constrain master_key with an explicit maximum length appropriate for the gateway master key, while preserving its required string behavior and existing description.Source: Coding guidelines
tests/unit/test_dashboard_session.py (1)
1-24: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winNice, thorough coverage — one gap: hybrid-mode cookie rejection isn't tested.
_session_cookie_authenticatesindeps.pyexplicitly disables cookie auth whenconfig.is_hybrid_modeis true, but no test here builds a hybrid-mode config and confirms a valid cookie still gets a 401. Since this is a real security boundary the PR introduces (dashboard cookie must never work in hybrid mode), it'd be worth a quick test alongside the others.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_dashboard_session.py` around lines 1 - 24, Add a unit test in tests/unit/test_dashboard_session.py that builds a GatewayConfig with is_hybrid_mode enabled, establishes or supplies a valid SESSION_COOKIE_NAME cookie, and verifies the protected dashboard request returns 401. Keep the test focused on the _session_cookie_authenticates hybrid-mode rejection path and preserve the existing non-hybrid cookie authentication coverage.web/src/api/client.ts (1)
65-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
deleteSessionbypassingapiFetchis well-justified, but it makes the guideline's exception clause stale.The reasoning in the comment (avoid re-entering
unauthorizedHandlerduring a logout-triggered revoke) is solid — routing this throughapiFetchreally would risk a recursive bounce. That said, worth a documentation touch-up since the exception clause currently only names the (now-renamed)validateMasterKey.As per coding guidelines, "avoid direct fetch() for authenticated management requests except pre-auth validateMasterKey" — consider updating this clause to also cover
createSession/deleteSessionso future readers don't flag this as an unexplained deviation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/api/client.ts` around lines 65 - 81, Update the authenticated-request guideline documentation to explicitly exempt createSession and deleteSession, alongside validateMasterKey, from the direct fetch restriction. Keep the existing rationale for deleteSession: it must bypass apiFetch to avoid re-entering unauthorizedHandler during logout-triggered revocation.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/api/routes/auth_session.py`:
- Around line 57-65: Update the SQLAlchemyError handler around
create_dashboard_session to log the caught database exception with the module’s
existing logger and exc_info=True before rolling back and raising the generic
HTTPException. Preserve the client-facing “Database error” detail and from None
behavior, matching the logging approach used by delete_session.
- Line 66: Update the secure-cookie decision in the authentication session route
to use the trusted `request_is_https()` result only when forwarded protocol
headers are validated by trusted proxy evidence such as `X-Client-Tls: true` or
`X-Tls-Endpoint: https`; otherwise treat the request as non-HTTPS. Preserve
direct HTTPS detection and keep the existing session-cookie application flow.
In `@src/gateway/static/dashboard/assets/index-D1FfVwkg.js`:
- Line 2: The AuthProvider flow around Me, logout, and login must serialize
sign-out before sign-in: make logout retain authenticated state and return a
promise that resolves only after DELETE /v1/auth/session and local cleanup
complete, have the sign-in form await that promise before invoking login(), then
regenerate the dashboard bundle and add a regression test covering rapid
logout/login.
---
Nitpick comments:
In `@src/gateway/api/routes/auth_session.py`:
- Around line 33-37: Update the CreateSessionRequest Pydantic model to constrain
master_key with an explicit maximum length appropriate for the gateway master
key, while preserving its required string behavior and existing description.
In `@tests/unit/test_dashboard_session.py`:
- Around line 1-24: Add a unit test in tests/unit/test_dashboard_session.py that
builds a GatewayConfig with is_hybrid_mode enabled, establishes or supplies a
valid SESSION_COOKIE_NAME cookie, and verifies the protected dashboard request
returns 401. Keep the test focused on the _session_cookie_authenticates
hybrid-mode rejection path and preserve the existing non-hybrid cookie
authentication coverage.
In `@web/src/api/client.ts`:
- Around line 65-81: Update the authenticated-request guideline documentation to
explicitly exempt createSession and deleteSession, alongside validateMasterKey,
from the direct fetch restriction. Keep the existing rationale for
deleteSession: it must bypass apiFetch to avoid re-entering unauthorizedHandler
during logout-triggered revocation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5b00e056-b38f-497f-a9ce-fbc6c0d3f6d2
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (51)
AGENTS.mdalembic/versions/a9c1e3b5d7f9_add_dashboard_sessions_table.pydocs/public/otari.postman_collection.jsonsrc/gateway/api/deps.pysrc/gateway/api/main.pysrc/gateway/api/routes/auth_session.pysrc/gateway/api/routes/settings.pysrc/gateway/core/config.pysrc/gateway/main.pysrc/gateway/models/entities.pysrc/gateway/services/dashboard_session_service.pysrc/gateway/static/dashboard/assets/ActivityPage-Dm6r6wPy.jssrc/gateway/static/dashboard/assets/AliasesPage-AOThQmDL.jssrc/gateway/static/dashboard/assets/BudgetsPage-o3Sj5U5B.jssrc/gateway/static/dashboard/assets/KeysPage-CbUCEimJ.jssrc/gateway/static/dashboard/assets/ModelScopeControl-Bpbo36Ko.jssrc/gateway/static/dashboard/assets/ModelsPage-WLlH9ed9.jssrc/gateway/static/dashboard/assets/OverviewPage-DXIwdDWG.jssrc/gateway/static/dashboard/assets/ProvidersPage-Bz-mLWy0.jssrc/gateway/static/dashboard/assets/SettingsPage-BzPdd2gR.jssrc/gateway/static/dashboard/assets/SettingsPage-C0JYm9ni.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-qp13etCg.jssrc/gateway/static/dashboard/assets/UsagePage-DU8IagMv.jssrc/gateway/static/dashboard/assets/UsersPage-DjQ_32XA.jssrc/gateway/static/dashboard/assets/index-D1FfVwkg.jssrc/gateway/static/dashboard/assets/index-DkHDQ_jO.jssrc/gateway/static/dashboard/index.htmltests/unit/test_dashboard_session.pytests/unit/test_deps_db_error.pytests/unit/test_master_key_service.pyweb/src/App.test.tsxweb/src/api/client.tsweb/src/auth/AuthContext.test.tsxweb/src/auth/AuthContext.tsxweb/src/components/Login.test.tsxweb/src/components/Login.tsxweb/src/components/PricingWarning.test.tsxweb/src/components/UpdatePrompt.test.tsxweb/src/components/UpdatePrompt.tsxweb/src/pages/ActivityPage.test.tsxweb/src/pages/AliasesPage.test.tsxweb/src/pages/BudgetsPage.test.tsxweb/src/pages/KeysPage.test.tsxweb/src/pages/ModelsPage.test.tsxweb/src/pages/OverviewPage.test.tsxweb/src/pages/ProvidersPage.test.tsxweb/src/pages/SettingsPage.test.tsxweb/src/pages/SettingsPage.tsxweb/src/pages/ToolsGuardrailsPage.test.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsersPage.test.tsx
💤 Files with no reviewable changes (4)
- web/src/pages/ModelsPage.test.tsx
- src/gateway/static/dashboard/assets/SettingsPage-C0JYm9ni.js
- web/src/components/UpdatePrompt.test.tsx
- src/gateway/static/dashboard/assets/index-DkHDQ_jO.js
khaledosman
left a comment
There was a problem hiding this comment.
Two inline notes below (both minor). See the updated review summary for the correction to my earlier findings.
2d75fac to
f0cb4c4
Compare
The dashboard forced operators to re-paste the master key on every new tab, closed tab, or browser restart, because the raw key lived in per-tab sessionStorage. Signing in now exchanges the key once (POST /v1/auth/session) for a server-issued opaque session held in an HttpOnly cookie, which is both more convenient (survives tabs and restarts) and more secure (no JS-readable credential, TTL, server-side revocation). Backend: - dashboard_sessions table stores only SHA-256 token hashes, so every worker and replica accepts a session and revocation is global. - POST /v1/auth/session verifies the master key and sets the cookie (HttpOnly; SameSite=Strict; Secure mirrors the request scheme so plain-HTTP LAN deployments keep working); DELETE signs out. - The master-key auth dependencies accept the cookie only when a request carries no header credentials, so API clients are untouched. A Sec-Fetch-Site check backs up SameSite as CSRF defense in depth. - Master-key rotation revokes every session and re-mints the caller's cookie on the response, so the rotating tab stays signed in. - Session TTL is configurable via dashboard_session_ttl_hours (default 7 days). Frontend: - The raw key is sent once to mint the session and never stored; a non-secret localStorage marker keeps the signed-in state synchronous, and any 401 drops it exactly like a mid-session revocation. - Sign-out revokes the session server-side before clearing local state. Fixes #338 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two follow-ups from self-review of the session-cookie change: - A dashboard session only proves possession of the master key at mint time, so it must not outlive the key. The generated key already revoked sessions inline at rotation, but a configured key rotates by changing OTARI_MASTER_KEY and restarting, which no request handler observes; pre-rotation sessions survived until their TTL. Store a hash of the effective key in runtime_settings and revoke every session at startup when it changes (covering configured-key rotation and configured/generated regime switches). The rotation endpoint keeps the marker in step so its re-minted session survives restarts. - Regenerate the Postman collection from the updated OpenAPI spec (make postman), which the openapi-spec CI job checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raising a 500 on a DB failure during DELETE /v1/auth/session made FastAPI discard the injected response, so the cookie was never cleared while the frontend had already dropped its local marker: the browser kept a live session cookie the operator believed was gone. Sign-out now always clears the cookie and returns 204, logging the failed revocation; the unrevoked row dies on its TTL. Flagged by Copilot review on #384. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… flag Behind a TLS-terminating proxy, uvicorn only trusts X-Forwarded-Proto from loopback by default, so request.url.scheme reads "http" on typical PaaS ingress and the session cookie was minted without Secure despite an HTTPS browser leg. Decide the Secure attribute from the effective scheme (direct https, or the first X-Forwarded-Proto value). Honoring the header regardless of source is safe for this decision alone: a spoofed "https" over plain HTTP only denies the spoofer their own cookie. Also covers the Secure=true branch in tests (direct HTTPS and forwarded proto), and documents two review-noted asymmetries: DELETE skipping the Sec-Fetch-Site check (SameSite already covers it; worst case is a forced sign-out) and Path=/ (management routes live beside inference under /v1, and the cookie grants nothing beyond master authority). Addresses khaledosman's review on #384. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The create_session 500 branch discarded the SQLAlchemyError without a trace, unlike the parallel branch in delete_session; a sign-in outage would have been invisible in the logs. Flagged by CodeRabbit on #384. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f0cb4c4 to
7297756
Compare
Description
The dashboard forced operators to re-paste the master key on almost every open: the raw key lived in per-tab
sessionStorage, so a new tab, a closed tab, or a browser restart lost the session.This implements option B from #338, the server-issued session cookie. Signing in now sends the master key once to a new
POST /v1/auth/sessionendpoint, which verifies it and answers with an opaque session token in anHttpOnlycookie. The sign-in survives tabs and restarts, and the browser never stores a JS-readable credential, so this is more convenient and more secure than the status quo at the same time.Backend
dashboard_sessionstable (plus Alembic migration) storing only SHA-256 token hashes. DB-backed rather than in-memory so every worker and replica accepts a session and revocation is global, following the same reasoning as the alias/provider TTL refreshers.POST /v1/auth/sessionmints the session;DELETEsigns out (idempotent, revokes only the caller's own cookie session). Standalone-only, like the rest of the management API.HttpOnly,SameSite=Strict,Path=/,Max-Agefrom the newdashboard_session_ttl_hoursconfig field (default 168h).Securemirrors the request scheme instead of being hard-coded, so plain-HTTP LAN and localhost deployments still receive the cookie back; that is no worse than such deployments already sending the raw key in cleartext.api/deps.py:verify_master_keyandverify_api_key_or_master_keyaccept the cookie only when the request carries no header credentials at all, so API clients keep exactly the previous behavior, including failure modes.SameSite=Strictis the primary control (the dashboard and API are same-origin, and the gateway origin serves no user-generated content); aSec-Fetch-Sitecheck on the cookie path adds defense in depth.same-siteis deliberately rejected so a sibling-subdomain page cannot ride the cookie.Frontend
sessionStorage). A non-secretlocalStoragemarker (otari.dashboard.hasSession) keeps the signed-in state synchronous on load; if it is ever stale, the first 401 drops it and bounces to sign-in, the same path as any mid-session revocation.replaceMasterKeyplumbing is gone: the rotation response re-mints the cookie server-side, so the client no longer swaps credentials at all.Verified end-to-end against a live gateway: cookie mint, cookie-only management reads, wrong-key/no-credential/cross-site rejections, server-side sign-out revocation, header auth unaffected, and a session minted before a full gateway restart still signs in (the exact friction in the issue).
PR Type
Relevant issues
Fixes #338
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).Note on tests: lint, typecheck, the full unit suite (881 passed, including 8 new session tests), the web checks (typecheck, 199 tests, fresh committed bundle), and
make openapi-checkall ran locally. The integration suite needs Docker/testcontainers, which the sandbox does not have, so it is covered by CI on this PR.AI Usage
AI Model/Tool used:
Claude Code (Claude Fable 5)
Any additional AI details you'd like to share:
Implemented via the repo's fix-github-issue workflow with @njbrake choosing the approach (option B) and reviewing before push.
🤖 Generated with Claude Code
Summary
Benefits