fix: sessions that end when you end them (#112) #138

Merged
nalum merged 4 commits from fix/sessions into main 2026-08-18 16:42:41 +00:00
Owner

PR 1 of the #137 stack. Closes #112.

Four commits:

  1. feat(auth): bind session tokens to their refresh chain — ADR-0033. Session JWTs minted alongside a refresh chain carry a sid claim. The auth interceptor refuses the token once the chain has no live record, through a new nil-tolerated Config.LookupChain seam. Revoking a session now ends that device's access on its next request, not at token expiry.
  2. feat(user): mark the current session and hide idle-dead chains — Session.current (proto, additive) marks the chain the request rode. ListSessions skips chains already past the 90-day idle window: they can never exchange again, so listing them as signed-in devices was a lie. This is where the phantom session pile-up came from.
  3. fix(web) — the switch panel sent every PIN-set profile to the PIN pad even with no parked chain on the device, so the pad always failed into the password form and read as "my PIN doesn't work". It now routes to the password directly with honest copy. The sessions card shows a "This device" chip, and revoking the current session signs out cleanly on the spot.
  4. fix(android) — the same two fixes ported: parked-before-PIN routing on the switch sheet, current marker + clean sign-out in the sessions card.

Verified against the live deploy: sid present in the minted token, current: true on the calling chain, and a still-unexpired access token gets 401 on the very next request after its chain is revoked. make check, the hermetic integration suite, and the Android unit tests all pass.

🤖 Generated with Claude Code

PR 1 of the #137 stack. Closes #112. Four commits: 1. **feat(auth): bind session tokens to their refresh chain** — ADR-0033. Session JWTs minted alongside a refresh chain carry a `sid` claim. The auth interceptor refuses the token once the chain has no live record, through a new nil-tolerated `Config.LookupChain` seam. Revoking a session now ends that device's access on its next request, not at token expiry. 2. **feat(user): mark the current session and hide idle-dead chains** — `Session.current` (proto, additive) marks the chain the request rode. `ListSessions` skips chains already past the 90-day idle window: they can never exchange again, so listing them as signed-in devices was a lie. This is where the phantom session pile-up came from. 3. **fix(web)** — the switch panel sent every PIN-set profile to the PIN pad even with no parked chain on the device, so the pad always failed into the password form and read as "my PIN doesn't work". It now routes to the password directly with honest copy. The sessions card shows a "This device" chip, and revoking the current session signs out cleanly on the spot. 4. **fix(android)** — the same two fixes ported: parked-before-PIN routing on the switch sheet, current marker + clean sign-out in the sessions card. Verified against the live deploy: `sid` present in the minted token, `current: true` on the calling chain, and a still-unexpired access token gets 401 on the very next request after its chain is revoked. `make check`, the hermetic integration suite, and the Android unit tests all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Revoking a device session (RevokeSession, delete-all, logout) only
killed the refresh chain — the access JWT stayed valid until its
expiry, so 'signing a device out' did not actually end its session
(issue #112). Session tokens minted alongside a chain now carry a sid
claim naming the chain, and the auth interceptor refuses the token as
soon as the chain has no live record. The chain lookup is a Config
seam like LookupSubject, nil-tolerated so session-only tests need no
wiring; sid-less tokens (pre-change, or logins that did not remember
the device) behave exactly as before. ADR-0033 records the decision.
The settings session list showed every unrevoked chain with no hint of
which one is this device (issue #112): abandoned chains (a cleared
browser, a reinstalled app) sat listed for the full 90-day idle window
because idle expiry is only enforced when a token is presented, and the
member could not tell a phantom from the session they are using right
now. ListSessions now skips chains already past the idle window — they
can never exchange again, so listing them as signed-in devices was a
lie — and marks the requester's own chain via the sid claim (ADR-0033)
so clients can say 'this device'.
Two client halves of issue #112. The switch panel sent every PIN-set
profile to the PIN pad even when this device holds no parked chain for
them, so the pad always failed into the password form — reading as
'my PIN doesn't work'. It now routes straight to the password with the
honest notice, and the pad is reserved for profiles the device can
actually switch to. The sessions card marks the server's current
session as 'This device', and revoking it signs out cleanly on the
spot instead of leaving the page to die on its next request.
fix(android): honest session list and PIN routing on the switch sheet
Some checks failed
check / commits (pull_request) Successful in 6s
check / web (pull_request) Successful in 1m39s
check / go (pull_request) Successful in 2m46s
check / report (pull_request) Successful in 4s
android / build (pull_request) Successful in 5m11s
android / report (pull_request) Successful in 3s
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
check / go (push) Has been cancelled
check / commits (push) Has been cancelled
android / report (push) Has been cancelled
android / build (push) Has been cancelled
892c7b55bc
The Android half of issue #112, mirroring the web fix: the switch
sheet routed PIN-set profiles to the pad even when no parked chain
exists on this device (the pad then always failed into the password
form), the session list carried no 'this device' marker, and revoking
the current session left the app to die on its next request instead
of signing out cleanly.

Test report

Suite Tests Result Skipped
Unit 1370 ✅ pass 1
Integration 85 ✅ pass —

Coverage: 28.2%

Updated by the check workflow · commit 892c7b55bc

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1370 | ✅ pass | 1 | | Integration | 85 | ✅ pass | — | **Coverage:** 28.2% <sub>Updated by the check workflow · commit 892c7b55bc5640ac3ad0f4af242c76544a43eeb5</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 31 ✅ pass 0

Coverage: 2.4% of lines

Updated by the android workflow · commit 892c7b55bc

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 31 | ✅ pass | 0 | **Coverage:** 2.4% of lines <sub>Updated by the android workflow · commit 892c7b55bc5640ac3ad0f4af242c76544a43eeb5</sub>
nalum merged commit 892c7b55bc into main 2026-08-18 16:42:41 +00:00
nalum deleted branch fix/sessions 2026-08-18 16:42:41 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!138
No description provided.