Keepalive via refresh-session; GET pings measured insufficient

The endurance test refuted the sliding-window model I committed earlier.
A keepalive doing only GET /api/v3/me succeeded at t+0/30/60/90 and was
still rejected by t+120 — consistent with the session ending ~2h after
LOGIN (t+107), and inconsistent with 2h after the last request, which
would have been t+210.

This is a live-vs-source divergence, not a misreading: both the current
JwtWhitelistAdapter and the legacy Feathers ensureTokenIsWhitelisted
re-set the Valkey TTL on every authenticated request, so the source
reads as a sliding window. The instance does not behave that way.

So the keepalive now calls POST /authentication/refresh-session, the
endpoint behind the UI's "Sitzung verlängern" button, which a separate
100s test showed does hold the reported budget at 7200s. It is the only
non-GET request in the server: no body, touches only our own session,
cannot read or modify user data, and is not exposed as a tool, so no
model-driven call can ever be a POST. It logs the returned budget, which
makes a failing extension visible before the session is lost.

Whether this is sufficient is NOT established. Two mechanisms still fit:
an idle TTL that reads fail to refresh (keepalive works), or an absolute
cap/revocation anchored at login — e.g. the IDP's back-channel logout,
which clears every token for the account rather than one. Added
scripts/session-diagnose.mjs to settle it: it logs the budget every 10
min, so a decaying series indicates the former and an abrupt 401 at
7200s the latter. Docs state the open question rather than asserting a
mechanism.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 15:46:54 +02:00
parent d657ece436
commit 60ca4d3eba
11 changed files with 333 additions and 124 deletions

View File

@@ -50,20 +50,21 @@ npm run probe # verifies the token and API against the live instan
Then either deploy it as a remote connector, or point Claude Code at
`dist/bin/stdio.js`. Both paths are in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
Getting `TSC_JWT_COOKIE` takes four clicks in DevTools. With the server running
it lasts up to 30 days; left idle for two hours it dies regardless — see
[docs/AUTH.md](docs/AUTH.md).
Getting `TSC_JWT_COOKIE` takes four clicks in DevTools. Expect to redo it
often until the session question in [docs/AUTH.md](docs/AUTH.md) is settled —
sessions have been observed ending ~2 h after login.
## Design decisions
**Bearer token plus a keepalive.** The instance's `jwt` cookie works verbatim
as `Authorization: Bearer` — no cookie jar, no `connect.sid`. But the token has
two clocks: a 30-day `exp` claim, and a **2-hour idle timeout** held in a
server-side whitelist that *every* authenticated request resets. Only the first
is visible in the token, which makes "valid for 30 days" an easy and wrong
conclusion. So the server pings `GET /api/v3/me` every 30 minutes to hold the
window open. See [docs/AUTH.md](docs/AUTH.md) — this one cost a day-old token to
pin down.
as `Authorization: Bearer` — no cookie jar, no `connect.sid`. But the token's
`exp` claim (30 days) is not its lifetime: the session ends about **two hours
after login**, and measurement showed that ordinary API reads do *not* extend
it, despite the upstream source saying they should. So the server calls
`refresh-session` every 30 minutes — the one non-GET request here, and not
exposed as a tool. Whether that is enough is still being measured; see
[docs/AUTH.md](docs/AUTH.md), which has the endurance test and the open
question.
**Read-only by construction.** Every method on the API client is a `GET`,
including `api_get`. The endpoint is internet-facing by necessity (Claude's
@@ -104,6 +105,7 @@ npm run dev # watch mode, runs src/ directly
npm test # unit tests, no network
npm run probe # check assumptions against the live instance
npm run smoke # full end-to-end: real server, real client, real data
npm run session-diagnose # instrument what actually ends the session (~2.5h)
npm run typecheck
```