NEC-152: A refresh reply lost to the network signs the vault out: no grace window for the rotated-out token #211

Merged
nectenda-agent merged 4 commits from worktree-nec-152-a-refresh-reply-lost-to-the-network-sign into main 2026-09-29 14:43:48 +01:00
Collaborator

Task: NEC-152 — https://projectron.nerchure.com/tasks/160

A refresh whose reply is lost to the network no longer signs the vault out. For 10 minutes after a rotation (REFRESH_GRACE_SECONDS), the rotated-out token is answered with a new one when it comes from the session's own install id. So that a stolen token stays detectable, every displaced token hash is now kept for 30 days (RETIRED_REFRESH_TOKEN_DAYS, pruned hourly and when the session ends), and its return revokes the session. This also closes an older gap on main, where refreshing a stolen token twice pushed the owner out silently. The reuse log now dates a retired token's return correctly and marks it retired: true. Ride-along NEC-153: a sign-out with a pushed-out token now ends the session. Two fresh-context reviews shaped this; the retention and table design were the user's decisions.

Spec: docs/changes/NEC-152-refresh-grace-window/spec.md

Also fixes NEC-153: Signing out with a refresh token two rotations back revokes nothing

Changelog

Losing the network in the middle of a sign-in refresh no longer signs your vault out.

Task: NEC-152 — https://projectron.nerchure.com/tasks/160 A refresh whose reply is lost to the network no longer signs the vault out. For 10 minutes after a rotation (REFRESH_GRACE_SECONDS), the rotated-out token is answered with a new one when it comes from the session's own install id. So that a stolen token stays detectable, every displaced token hash is now kept for 30 days (RETIRED_REFRESH_TOKEN_DAYS, pruned hourly and when the session ends), and its return revokes the session. This also closes an older gap on main, where refreshing a stolen token twice pushed the owner out silently. The reuse log now dates a retired token's return correctly and marks it retired: true. Ride-along NEC-153: a sign-out with a pushed-out token now ends the session. Two fresh-context reviews shaped this; the retention and table design were the user's decisions. Spec: `docs/changes/NEC-152-refresh-grace-window/spec.md` Also fixes NEC-153: Signing out with a refresh token two rotations back revokes nothing ## Changelog Losing the network in the middle of a sign-in refresh no longer signs your vault out.
A rotated-out refresh token presented within ten minutes of its rotation,
from the session's own install id, is a reply lost to the network rather
than theft. It is now answered with a new token. The lost token moves
into the previous slot, and the window stays anchored to the rotation,
so a replay from elsewhere or after the window still revokes the session.

REFRESH_GRACE_SECONDS sets the window; 0 restores the old behaviour.

Task: NEC-152
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gj4XQjqZgapxUHWKa9Pk53
Review found that grace plus one ordinary refresh left the other holder's
token two rotations back, where it read as unknown: that holder was signed
out while the session lived on. The same was already true of a stolen
current token refreshed twice. Every token a rotation pushes out of the
previous slot is now kept, as a hash, in retired_refresh_tokens, and its
return revokes the session. Rows are pruned hourly after 30 days
(RETIRED_REFRESH_TOKEN_DAYS), or at once when their session is over.

Task: NEC-152
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gj4XQjqZgapxUHWKa9Pk53
The reuse log line reported the gap from the session's latest rotation,
so a thief refreshing just before the owner came back made a weeks-old
token look like the harmless double-present. It now counts from when the
token left the previous slot, and says `retired: true`; the runbook reads
it accordingly.

Task: NEC-152
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gj4XQjqZgapxUHWKa9Pk53
Let a sign-out with a pushed-out refresh token end the session
All checks were successful
Release note / release-note (pull_request) Successful in 13s
CI / build (pull_request) Successful in 4m55s
CI / e2e (pull_request) Successful in 5m24s
CI / promote (pull_request) Has been skipped
42faac7252
`/auth/logout` matched only the current and previous tokens. Once a thief
had refreshed twice, the owner's sign-out answered 200 and revoked
nothing. It now also matches a retired token.

Task: NEC-152
Also-fixes: NEC-153
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gj4XQjqZgapxUHWKa9Pk53
cruelacid force-pushed worktree-nec-152-a-refresh-reply-lost-to-the-network-sign from 42faac7252
All checks were successful
Release note / release-note (pull_request) Successful in 13s
CI / build (pull_request) Successful in 4m55s
CI / e2e (pull_request) Successful in 5m24s
CI / promote (pull_request) Has been skipped
to f73afaa09d
All checks were successful
Release note / release-note (pull_request) Successful in 11s
CI / build (pull_request) Successful in 4m49s
CI / e2e (pull_request) Successful in 5m23s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 5m5s
CI / e2e (push) Successful in 5m22s
CI / promote (push) Successful in 30s
2026-09-29 14:37:49 +01:00
Compare
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
Nectenda/nectenda!211
No description provided.