feat(auth): optional WEBTERM_TOKEN access-token gate (W5)
An OPTIONAL shared token so the app can be used off-LAN (via the relay/tunnel) more safely than "anyone who reaches the port gets a shell". Sits IN FRONT OF the existing Origin/CSRF model — never replacing it — and is fully inert when unset. - src/http/auth.ts (new, pure): constantTimeEqual hashes both inputs to sha256 (32 bytes) then crypto.timingSafeEqual — no `===`, no length side-channel, never throws. parseCookieHeader / cookieIsAuthed / buildSetCookie / isAuthEnabled. - WEBTERM_TOKEN in config: 16–512 URL/cookie-safe chars or the server refuses to start. - GET /?token=<t> or POST /auth (rate-limited 10/min) validates → sets HttpOnly; SameSite=Strict; Secure-when-https cookie; public/login.html (no <script>). - When enabled: the WS handshake (AFTER the Origin check, before handleUpgrade) + a central authGate over all remote HTTP require the cookie. Open: /login, /auth. Loopback bypass scoped to /hook* ONLY (tighter than the plan — gates the local browser too). - Unset ⇒ isAuthEnabled false ⇒ pure passthrough (LAN zero-config unchanged). Honest tradeoff (in code + CLAUDE.md + login page): a bar-raiser, NOT a TLS/Tailscale substitute — on bare ws:// the token is cleartext + replayable; only hardens the TLS-terminated relay path. Verified: typecheck + build:web clean, 2118 pass at --test-timeout=30000 (disabled-mode regression proven; only the known tmux flake red); auth.ts 100% line coverage.
This commit is contained in:
@@ -121,12 +121,15 @@ build sequence (dependencies noted).
|
||||
the split-grid watch board + statusLine gauges + per-quadrant inline approve; server side is a
|
||||
thin "sessions grouped by repo/worktree" endpoint. Cost is UI. **Effort: L.** Depends on Wave 4.
|
||||
|
||||
- [ ] **App-level access token** (leave-the-LAN bar-raiser)
|
||||
Smallest step to safely use the new relay/tunnel off-LAN: a constant-time-compared
|
||||
`WEBTERM_TOKEN` checked on the WS handshake (alongside Origin) + on state-changing routes,
|
||||
set as an httpOnly cookie after one-time `?token=`, disabled when unset (keeps LAN zero-config).
|
||||
**Honest tradeoff:** a bar-raiser, **not** a TLS/Tailscale substitute; needs constant-time
|
||||
compare + cookie flags + rate-limiting or it's security theater. **Effort: M.**
|
||||
- [x] **App-level access token** (leave-the-LAN bar-raiser) — shipped on `develop`
|
||||
A constant-time-compared (`crypto.timingSafeEqual` over SHA-256, fixed-length guard)
|
||||
`WEBTERM_TOKEN` checked on the WS handshake (alongside, not replacing, Origin) + a central
|
||||
gate over every remote HTTP route, set as an `HttpOnly; SameSite=Strict; Secure-when-https`
|
||||
cookie after one-time `GET /?token=` or `POST /auth` (rate-limited 10/min/IP), disabled when
|
||||
unset (keeps LAN zero-config byte-identical). Charset/length-validated at load; loopback
|
||||
`/hook*` ingest exempt. `src/http/auth.ts` + wiring in `src/server.ts`.
|
||||
**Honest tradeoff:** a bar-raiser, **not** a TLS/Tailscale substitute — on bare `ws://` the
|
||||
token is cleartext and replayable; only hardens the TLS-terminated relay/tunnel path. **Effort: M.**
|
||||
|
||||
- [ ] **Android Projects / Diff / Worktree screens** (client parity)
|
||||
Android's whole v0.6/v0.7 projects-git UI is SDK-gated/off in `settings.gradle.kts`.
|
||||
|
||||
Reference in New Issue
Block a user