Fix session lifetime: 2h sliding idle timeout, not 30 days

The JWT's exp claim says 30 days, and I took that as the session
lifetime. It is only an outer ceiling. The server also keeps a per-token
whitelist entry in Valkey (jwt:{accountId}:{jti}) whose TTL is
JWT_TIMEOUT_SECONDS — 7200s on this instance — and JwtStrategy.validate
re-sets it on every authenticated request. Two hours idle and the token
is rejected with 29 days still on exp.

Proven, not inferred: the token from yesterday returned 401 at 13.8h old.
The live instance publishes the values unauthenticated at
GET /api/v3/config/public — JWT_TIMEOUT_SECONDS 7200,
JWT_SHOW_TIMEOUT_WARNING_SECONDS 3600, the latter being exactly the
one-hour UI prompt that prompted this investigation.

refresh-session turns out not to be special: it extends through the same
guard as any other route, and uniquely only in returning the remaining
TTL. So the keepalive uses GET /api/v3/me instead, and the server stays
GET-only; the one POST in the repo is in scripts/probe.mjs, where it
reports the idle budget.

JWT_EXTENDED_TIMEOUT_SECONDS (~1 month) exists in the config schema but
is vestigial: privateDevice has no references in the current NestJS
source, and generateJwtAndAddToWhitelist never overrides the TTL.

Also fixes a real breakage this surfaced: TypeScript parameter
properties are rejected by Node's type stripping, so `npm run dev` and
`npm test` both failed on any file reaching them. Rewritten as explicit
fields, and noted in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 13:07:22 +02:00
parent 35125b7683
commit d657ece436
14 changed files with 408 additions and 57 deletions

View File

@@ -59,7 +59,7 @@ the property that makes that acceptable. Do not add a write tool without the
user explicitly asking for one and understanding this.
**Never log or echo secrets.** `TSC_JWT_COOKIE` grants full read access to the
account for 30 days; `MCP_AUTH_TOKEN` guards the endpoint. Neither belongs in
account; `MCP_AUTH_TOKEN` guards the endpoint. Neither belongs in
logs, error messages, or tool output. `.env` is git-ignored — keep it that way.
**Live behaviour beats upstream source.** The clones in `vendor/` track `main`
@@ -84,11 +84,18 @@ These cost real time to discover; `docs/API.md` has the full list with evidence.
its own OpenAPI document. It is not in the main `docs-json`.
- Legacy lesson responses return ids as `{buffer:{data:[...]}}`; use
`normalizeObjectId`.
- **The JWT dies after 2h idle, not 30 days.** `exp` is a hard ceiling; the
real limit is a Valkey whitelist entry (`JWT_TIMEOUT_SECONDS`, live value at
`GET /api/v3/config/public`) that every authenticated request re-sets.
`src/keepalive.ts` holds it open. Do not "simplify" it away.
## Conventions
- Imports use `.ts` extensions; `rewriteRelativeImportExtensions` makes `tsc`
emit `.js`. This lets `node --watch src/bin/http.ts` run the tree directly.
- **No TypeScript parameter properties** (`constructor(private readonly x: T)`).
Node's type stripping rejects them, which breaks `npm run dev` and `npm test`.
Declare the field and assign it in the constructor body instead.
- Tabs for indentation, single quotes, trailing commas.
- Comments explain *why* — an API quirk, a security property, a trade-off — not
what the line does. Several such comments encode findings that are expensive
@@ -108,5 +115,5 @@ These cost real time to discover; `docs/API.md` has the full list with evidence.
## Environment
`.env` holds `TSC_URL`, `TSC_JWT_COOKIE`, `MCP_AUTH_TOKEN`. See `.env.example`
for the full set and `docs/AUTH.md` for refreshing the JWT — it expires every 30
days, and `npm run probe` reports the days remaining.
for the full set and `docs/AUTH.md` for refreshing the JWT. `npm run probe`
reports both clocks: days until hard expiry and seconds of idle budget left.