- Resolve a guest JWT on the lowercased email. Accounts are stored lowercased,
so a mixed-case `email` claim slipped past the no-account gate and resolved an
account holder to a guest, and split the activity rows the seat count reads.
`has_any_account` now normalises its input too (index-friendly, not
`lower(email)`).
- Cap the auth-cache entry for a guest JWT at 5 minutes rather than the token's
`exp` (up to 24h). A guest JWT is revocable only by the workspace switch or by
rotating the key; the short entry makes a rotated or cleared key bite on
re-verification, and makes the day-keyed activity dedupe reachable across a
midnight (the second-day row was never written).
- Audit `users.login_guest` only when the upsert freshly inserts the row
(`xmax = 0`), decided atomically by the DB, so concurrent first requests and
separate API nodes emit it at most once a day.
- JWKS hardening: read the body with a 1MB cap instead of buffering any size;
an alg-less RSA key accepts the whole RSA family instead of being forced to
RS256; a failed fetch serves the last good keys (or a short negative entry) so
an unreachable issuer is hit at most once per 30s however much unauthenticated
`jwt_guest_` traffic arrives, and an unknown `kid` never triggers a fetch;
lower the fetch timeouts to 5s/10s.
- Settings copy: note that the JWKS should point at an issuer you control, since
neither `iss` nor `aud` is bound.
- Tests: a mixed-case account and an over-24h lifetime are refused; unit tests
pin `jwk_algorithms` (including the alg-less RSA family) and a JWK-derived key
verifying a real token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3