Skip to content

refactor: per-profile session layer + multi-host TLS trust (all-profiles phase 0+1) - #338

Open
pliablepixels wants to merge 29 commits into
mainfrom
feat/all-profiles-sessions
Open

refactor: per-profile session layer + multi-host TLS trust (all-profiles phase 0+1)#338
pliablepixels wants to merge 29 commits into
mainfrom
feat/all-profiles-sessions

Conversation

@pliablepixels

Copy link
Copy Markdown
Member

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.ts registry (one session per profile: client + timezone); auth store refactored to per-profile token slices with per-profile single-flight dedup; all 11 api/* modules take an explicit client first param; the getApiClient/setApiClient singleton is deleted; switchProfile no 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):

  • add-profile discovery no longer attaches an existing profile's token to the new server
  • failed refresh no longer clears single-flight gates mid-race (prevented N duplicate login POSTs)
  • deleted profiles now drop their persisted refresh tokens
  • excluded-monitors filtering keys on the profile being queried, not the UI-active one

Verification: npm run gates exit 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

pliablepixels and others added 26 commits August 2, 2026 13:59
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>
@pliablepixels pliablepixels added the refactor Behavior-preserving; gates prove equivalence label Aug 2, 2026
pliablepixels and others added 3 commits August 2, 2026 18:49
…#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.
Records the accepted trust-scope decision from the PR #338 final review
in the durable contract, not only the design spec. Refs #337.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Behavior-preserving; gates prove equivalence

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant