Skip to content

feat(auth): slide session expiry so active members stay signed in - #30

Merged
johannesjo merged 4 commits into
mainfrom
claude/reauthentication-frequency-o54jpv
Jul 25, 2026
Merged

feat(auth): slide session expiry so active members stay signed in#30
johannesjo merged 4 commits into
mainfrom
claude/reauthentication-frequency-o54jpv

Conversation

@johannesjo

Copy link
Copy Markdown
Contributor

Sessions had a fixed 7-day lifetime with no renewal, so even a member who
used their Space every day was pushed back through an emailed login code
every week.

Make expires_at an idle window instead of a fixed lifetime: a request
landing in the second half of the window pushes expiry out to a full TTL,
and the window itself widens to 30 days. Renewing only past the halfway
mark keeps authentication read-only in the common case — at most one write
per session per half-window rather than one per request, which is what the
fixed lifetime was buying.

A copied token still dies 30 days after its last use, and logout, leaving a
Space, and the 10-session cap are all unchanged. No backfill is needed:
every live session sits below the halfway mark of the new window, so each
slides out to the full 30 days on its owner's next request.

Terms §5 and Privacy §16 restated accordingly.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01QLS7Awz5uAiN7RKog2YYCu

claude added 4 commits July 25, 2026 17:49
Sessions had a fixed 7-day lifetime with no renewal, so even a member who
used their Space every day was pushed back through an emailed login code
every week.

Make expires_at an idle window instead of a fixed lifetime: a request
landing in the second half of the window pushes expiry out to a full TTL,
and the window itself widens to 30 days. Renewing only past the halfway
mark keeps authentication read-only in the common case — at most one write
per session per half-window rather than one per request, which is what the
fixed lifetime was buying.

A copied token still dies 30 days after its last use, and logout, leaving a
Space, and the 10-session cap are all unchanged. No backfill is needed:
every live session sits below the halfway mark of the new window, so each
slides out to the full 30 days on its owner's next request.

Terms §5 and Privacy §16 restated accordingly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLS7Awz5uAiN7RKog2YYCu
Review of the sliding-session change caught a defect it introduced. Node
does not clamp a setTimeout delay above 2^31-1 ms (~24.8 days): it warns
and fires after 1ms. addClient schedules the session-expiry disconnect at
the session's full remaining lifetime, which was always safe under the old
7-day TTL and never is under a 30-day one.

Every stream opened with more than ~24.8 days left — i.e. every session
straight after sign-in or renewal — was therefore closed within
milliseconds. The web client resets its backoff on a successful open, so
this degraded into a ~1Hz reconnect loop that also fanned two presence
broadcasts per cycle to every other member of the Space.

Clamp the delay. A stream that stays open past the ceiling is disconnected
a little early and simply reconnects.

Also from review:

- Prune sessions over the per-member cap by expires_at rather than
  created_at. With sliding expiry the two diverged, so the cap retired a
  member's daily-driver device and kept a phone used once.
- Terms §5 and Privacy §16 stated the idle window as a floor. Renewal only
  fires past the halfway mark, so a session last used early in its window
  lapses sooner than a full TTL later; restate both as the ceiling they
  are, and correct the comment on MEMBER_SESSION_TTL_MS likewise.
- Terms §5 said an API token "follows the same rule": API tokens have a
  fixed 1-year lifetime that use does not extend. Say so.
- Add the server-side session record to the Privacy §7 retention list.
- Note the shifted meaning of the analytics "open sessions" figure, and
  refresh comments that still described a fixed 7-day lifetime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLS7Awz5uAiN7RKog2YYCu
Sliding expiry left no upper bound at all: a token someone keeps using
never expires, and there is no "sign out everywhere" control to fall back
on. That mattered less when the lifetime was a fixed 7 days — the old
scheme locked out a copied token on a fixed schedule whether or not the
member ever noticed the compromise. Restore that guarantee at a horizon
that doesn't reintroduce the original complaint: 90 days from issuance,
four emailed codes a year for a daily user against the previous 52.

Enforced on read via created_at, not merely by capping the renewal write.
A ceiling that holds only because no older code path ever wrote the row is
not a bound worth relying on for revocation. The write is capped as well so
expires_at stays honest for the retention sweep and the live-session count,
and the new `next > expiresAt` guard stops a session pinned at its ceiling
from rewriting the same timestamp on every request — it sits permanently
inside the halfway window, so without the guard the cap would undo the
read-only-authentication property the threshold exists to protect.

No mass logout on deploy: every currently-live session was issued within
the last 7 days, so none is near the ceiling.

Terms §5 and Privacy §7/§16 now state both bounds.

This does not close the missing revoke-all-sessions path, which remains the
right fix for a compromise the member *does* notice, nor the proof-token
lateral movement it would only partly contain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLS7Awz5uAiN7RKog2YYCu
CI's `npm audit --audit-level=high` step went red on two advisories that
went public after main was last built — the failure predates this branch
and would hit main equally. Every other job was already green.

postcss is a straight `npm audit fix` (8.5.16 -> patched, lockfile only).

brace-expansion is the awkward one. The advisory covers every published
version up to 5.0.7 across all three major lines in the tree (1.1.16,
2.1.2, 5.0.7) and only 5.0.8 is patched, so nothing short of forcing 5.0.8
everywhere clears it. Doing that alone breaks eslint: @eslint/config-array
0.22.x pins minimatch 3.x, which throws constructing a Minimatch against
5.x. Pairing the override with @eslint/config-array ^0.23.5 — the first
release off that minimatch — removes the incompatible path, so the whole
dependent cascade (minimatch, eslintrc, eslint, workbox, vite-plugin-pwa)
clears at once.

`npm audit fix --force` was the alternative and is worse: it downgrades
vite-plugin-pwa to 1.2.0, a breaking change to the PWA build.

Note the gate was never protecting shipped code here — `npm audit
--omit=dev` reports zero high advisories. All of this is lint and build
tooling. Relaxing the gate to production deps would also have made CI
green, but that trades away real supply-chain coverage of the dev
toolchain, so it isn't done here.

Verified end to end: check (typecheck/lint/format), web 247, server 242,
e2e 203, production build including the workbox chain the override
touches, and drizzle migrations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLS7Awz5uAiN7RKog2YYCu
@johannesjo
johannesjo merged commit d73a3d2 into main Jul 25, 2026
5 checks passed
@johannesjo
johannesjo deleted the claude/reauthentication-frequency-o54jpv branch July 25, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants