voyage/tests/fixtures/bakeoff-rich/brief.md
Kjell Tore Guttormsen f7c8aa45ab feat(voyage): S10 part B — NW2 full bake-off (rich fixture) → verdict POSITIVE
Run the full T2 §5 prose-vs-Workflow /trekreview bake-off (operator GO,
choice "a"): 3 runs/arm on a rich-finding JWT-auth fixture, resolving the
smoke's 0-finding limitation.

Deliverables:
- tests/fixtures/bakeoff-rich/ — JWT-auth brief + diff with 5 seeded blatant,
  brief-traceable issues (varied severity/rule_key, one dual-flaggable).
- scripts/bakeoff-armA-merge.mjs — Arm A (prose) validate (NW1) + triplet-dedup,
  matching Arm B's dedup exactly.
- scripts/bakeoff-fidelity.mjs — cross-arm + within-arm + granularity-ladder
  fidelity analysis over the structured arm outputs.
- docs/T2-bakeoff-results.md §Full run — the T2 §5 verdict.

Result (3 runs/arm, both arms ran the coordinator):
- Verdict fidelity EQUIVALENT — all 6 runs BLOCK, cross-arm verdict-match 1.0.
- Finding-set: substrate is fidelity-neutral. Cross-arm jaccard 0.41 (triplet)
  → 0.71 (file,rule_key) → 1.0 (file); cross-arm ≈ within-arm at every
  granularity. Issue coverage 5/5 in 6/6 runs. Low triplet jaccard is
  line-citation noise shared by both arms, not a substrate effect.
- Token +4.4% (Arm B vs A; <=+15%). Classifier interference 0 at 9-agent
  concurrency. JSON-robustness: Arm B schema-forced; Arm A 6/6 valid via NW1.
- VERDICT POSITIVE → S11 proceeds with opt-in --workflow flag.

Caveat (per plan posture): strict triplet-jaccard>=0.7 flag is 0/9, a
metric-calibration artifact (both arms sub-0.7 against themselves), not a
regression. Residual: F4 auto/bypass explicit-mode check (mode not settable
in-session).

Suite green (662/660 pass, 2 skip); plugin validate clean (modulo the
pre-existing root-CLAUDE.md warning). No production code changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
2026-06-18 15:44:24 +02:00

2.7 KiB

type brief_version slug task research_topics research_status brief_quality created
trekbrief 2.1 jwt-auth-refresh-rotation Add JWT authentication with refresh-token rotation to the API 0 complete ready 2026-06-18

JWT authentication with refresh-token rotation

Intent

Add stateless JWT authentication to the API: a /login endpoint that issues a short-lived access token plus a rotating refresh token, and a /refresh endpoint that rotates the refresh token on every use. Tokens are signed with a fixed RS256 key pair. This is the security boundary of the service, so the contract below is strict.

Plan reference (what the approved plan said to build)

Plan Step 4lib/handlers/login.mjs verifies the password with bcrypt.compare(password, user.passwordHash). The stored credential is a bcrypt hash; no plaintext comparison.

Plan Step 6lib/auth/jwt.mjs hard-codes the verification algorithm to ['RS256']. The algorithm is never read from the request.

Success Criteria

  • SC1POST /login with valid credentials returns 200 with both an accessToken and a refreshToken in the JSON body.
  • SC2POST /login with invalid credentials returns HTTP 401 (not 200) and no tokens. Invalid means the email is unknown OR the password does not match.
  • SC3 — Refresh-token rotation is covered by an automated test that exercises the concurrent-refresh race window (two refreshes presenting the same refresh token must not both succeed).
  • SC4 — Access and refresh tokens are signed and verified with RS256 only, using the server's fixed key pair.

Non-Goals

  • NG1 — Do NOT accept a caller-supplied signing/verification algorithm. The algorithm must never be read from the request (header, body, or query). A token claiming a different alg must be rejected.
  • NG2 — Do NOT add a user-registration / sign-up endpoint. Users are provisioned out of band.
  • NG3 — Do NOT add password-reset or email flows in this change.

Constraints

  • Node stdlib + the already-vendored jsonwebtoken and bcrypt; no new deps.
  • Every delivered code path that the SCs describe must have test coverage.
  • Errors from the refresh-token store (a network resource) must not crash the request handler — degrade to a 5xx, do not let the rejection bubble unhandled.

Assumptions

  • db.getUserByEmail(email) returns { id, email, passwordHash } or null.
  • A refreshStore with get/set/delete (async, may throw on backend outage) is injected.

NFRs

  • Constant-time password comparison via the bcrypt primitive (no hand-rolled comparison over plaintext-derived buffers).