refactor: per-profile session layer + multi-host TLS trust (all-profiles phase 0+1) - #338
Open
pliablepixels wants to merge 29 commits into
Open
refactor: per-profile session layer + multi-host TLS trust (all-profiles phase 0+1)#338pliablepixels wants to merge 29 commits into
pliablepixels wants to merge 29 commits into
Conversation
Approved design for the virtual All Servers profile: per-profile session layer (big-bang migration), two-tier preferences, auto-routed actions, multi-fingerprint TLS trust, four implementation phases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…override) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auth store moves from one global token slice to slices keyed by ProfileId, with per-profile single-flight gates for login/refresh/ proactive-login/401-recovery. api/store-gates.ts builds gates scoped to one profile; createStoreApiClient now requires a profileId. All 28 consumers of the old flat useAuthStore selectors thread the current profile's id (useAuthSlice for components/hooks, getAuthSlice for non-React code); useFreshAccessToken/useTokenRefresh resolve it internally via useCurrentProfile so none of their ~13 combined callers needed changes. Persistence moves to one JSON refresh-token map in secure storage keyed by profile id; a legacy single-slice persisted blob is discarded on read rather than converted. ProfileForm's connection-test flow (and services/discovery.ts, which it drives) now mints/threads a real ProfileId before the profile is saved, instead of relying on the single global auth slice. This incidentally fixes a latent bug: testing a second server while already authenticated to a profile could attach that profile's access token to the untested server's probe request. Per-profile slices mean the probe now reads an empty slice for the pre-save id, so nothing leaks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes five review findings against the per-profile auth slice refactor:
- Critical: wire markSessionActive into every legacy client-creation call
site (profile-initialization.ts, stores/profile.ts) so
getFreshAccessToken's hasActiveSession guard is ever true before Task 8
builds the real per-profile session registry. Proactive refresh was
dead in the running app without this.
- ProfileForm's connection-test flow now logs out its minted profile id
on every failure/cancel exit, so a login that succeeds mid-flow but is
later abandoned (duplicate name, cancelled) doesn't leak a persisted
refresh token under an orphaned id.
- deleteProfile/deleteAllProfiles now call logout/logoutAll so a deleted
profile's auth slice and persisted refresh token don't outlive it.
- logout(profileId, { keepGates: true }) for the three internal call
sites inside refreshAccessToken/recoverFromAuthFailure's own failure
paths: a full gate reset while their own single-flight gate was still
open let a concurrent caller slip past it and start a duplicate
refresh/login attempt.
- Discovery's anonymous pre-profile default moves off ALL_PROFILES_ID to
a distinct PROBE_PROFILE_ID, excluded from persistence, so a probe that
ends up authenticating can never persist a slice under the aggregate
identity Phase 2 reads.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folds components/ui/secure-image.tsx's direct getApiClient() use into this commit per plan. This is the last of the 11 api/ modules; every production getApiClient() reference is now gone except client.ts's own definition (Task 8 removes the singleton).
#337) api/client.ts loses the module apiClient var, getApiClient, setApiClient, resetApiClient, and registerApiClientResetHook; api/client-ready.ts is deleted (auth already guards on hasSession). stores/profile.ts's addProfile/updateProfile/deleteProfile/deleteAllProfiles/switchProfile replace their setApiClient/resetApiClient/markSessionActive stopgaps with getSession/dropSession/dropAllSessions, making getSession the live path. switchProfile no longer logs out the outgoing profile or resets a client - sessions persist per profile across a switch. deleteProfile/updateProfile drop the affected profile's session; deleteAllProfiles drops them all. services/profile-initialization.ts and the discovery/ProfileForm/Profiles probe flows drop their now-redundant setApiClient calls (they already build and use a client directly). getSession also rejects PROBE_PROFILE_ID, like ALL_PROFILES_ID. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lib/profile/profile-settings.ts's getExcludedMonitorIds/getExcludedMonitorIdSet take a profileId instead of reading currentProfileId internally: a background poller or assistant tool fetching events/monitors for a non-current profile must filter against that profile's exclusion list, not whichever is active in the UI. api/monitors.ts's getMonitors and api/events.ts's getEvents/ getAdjacentEvent gain a profileId parameter threaded to the gate; every caller (queries, the assistant's read-only tools, eventPoller, object-labels) now passes the profileId it already has in scope alongside its client. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#337) Native trust-map lookups used platform-reported hosts as-is while the JS side always lowercases and strips IPv6 brackets from new URL().hostname; a case or bracket mismatch silently degraded a PINNED host to accept-any. Normalize hosts at every native lookup/store site (iOS, Android) and make the JS side's normalization explicit. dropAllSessions now also resets every profile's pending auth gates, mirroring dropSession, so a stale in-flight login/refresh/recovery never resolves against a torn-down session.
) Final review (PR #338): maintainer ruling that union-of-profiles TLS trust intentionally fails open across all hosts once any profile enables self-signed trust (preserves multi-server setups with sibling stream hosts). Also flags that Phase 2 must fix reLoginFor before aggregate readers go live - it currently ignores its profileId argument and re-logs-in the current profile instead.
9 tasks
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.
Phases 0 and 1 of #337 (All Profiles mode). Foundation only: no user-visible change.
Phase 0 — TLS trust becomes a host→fingerprint map. SSLTrust plugin (iOS/Android/JS) stores per-host fingerprints so multiple self-signed servers can be trusted concurrently. TOFU semantics preserved per host; pre-save test-connection trusts the in-flight draft profile via a candidate override.
Phase 1 — per-profile session layer. New
services/sessions.tsregistry (one session per profile: client + timezone); auth store refactored to per-profile token slices with per-profile single-flight dedup; all 11api/*modules take an explicitclientfirst param; thegetApiClient/setApiClientsingleton is deleted;switchProfileno longer logs out the outgoing profile; Sessions architecture contract added to AGENTS.project.md with enforcement gates.Intentional fixes beyond pure refactor (found during review, documented in commit bodies):
Verification:
npm run gatesexit 0 (3290+ unit tests, build, 3 blocking lints, ratchet baseline unchanged at 213); full e2e suite 136 passed with ZERO e2e file edits — the zero-behavior-change acceptance proof. One pre-existing flake (monitor-detail pan, passed on retry).Pending manual verification: two-server self-signed TLS on device (device e2e is manual-only); maintainer will supply two live profiles — credentials never stored.
Design spec:
docs/superpowers/specs/2026-08-02-all-profiles-design.md. Plan:docs/superpowers/plans/2026-08-02-all-profiles-phase0-phase1.md.Claude assisting @pliablepixels
🤖 Generated with Claude Code