From 88863183e11ab5e981762236e28bc36d91d72314 Mon Sep 17 00:00:00 2001 From: Arlind Ukshini Date: Thu, 23 Apr 2026 17:10:08 +0200 Subject: [PATCH] update docs: minimal env, WSL deploy, join tracking, rsync excludes - align auth docs with the simplified POST /auth/login flow - drop CODE_PEPPER / SMTP / MAIL_FROM / mail.js / request-code references - document the bifrost_joins table and bin/joins.js CLI - OPERATIONS.md: WSL setup, exclude data/.env/node_modules on promote rsync - INSTALL.md: 3-value /etc/fenja/env, drop SMTP prereq Co-Authored-By: Claude Opus 4.7 (1M context) --- CHECKLIST.md | 20 +++++------ CLAUDE.md | 7 ++-- INSTALL.md | 26 ++++++-------- OPERATIONS.md | 99 +++++++++++++++++++++++++++++++++++++++++++-------- PROJECT.md | 35 ++++++++---------- README.md | 51 +++++++++++++------------- 6 files changed, 147 insertions(+), 91 deletions(-) diff --git a/CHECKLIST.md b/CHECKLIST.md index 62d234e..a29ff13 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -9,7 +9,7 @@ Notation: run on the VPS unless marked `[local]` or `[browser]`. ## A. After any code change (minimum viable smoke test) - [ ] `sudo systemctl status fenja` → `active (running)` -- [ ] `sudo journalctl -u fenja -n 20` shows `[mail] SMTP relay reachable` and `[bifrost] listening`, no red errors +- [ ] `sudo journalctl -u fenja -n 20` shows `[bifrost] listening on 127.0.0.1:3000`, no red errors (there is no `[mail] SMTP relay reachable` line anymore — the mail stack was removed) - [ ] [local] `curl -I https://project-bifrost.fenja.ai/` → 200, with `X-Frame-Options: DENY` and `Content-Security-Policy` headers - [ ] [local] `curl -I https://project-bifrost.fenja.ai/timeline.js` → 302, `Location: /` - [ ] [browser, private window] Open `https://project-bifrost.fenja.ai/` → entrance page renders (not timeline) @@ -22,21 +22,19 @@ If all five pass, the site is up and the gate holds. Do section A, then: -- [ ] [browser, private] Enter invited email → receive code in inbox (not spam) within 60s -- [ ] [browser] Type code → redirected to `/` showing the timeline (not the entrance) +- [ ] [browser, private] Enter invited email → session cookie issued immediately, welcome step appears (no 6-digit code flow anymore) +- [ ] [browser] Click "Learn more" on the welcome step → redirected to `/timeline` showing the timeline page - [ ] [browser] Hard-refresh (Ctrl+Shift+R) → stays on the timeline (cookie persists) - [ ] [browser, new private window] Visit `/` → entrance appears (no cookie leak between sessions) -- [ ] [browser] Click logout → lands on entrance → visiting `/` stays on entrance +- [ ] [browser] Click logout → lands on entrance → visiting `/timeline` redirects to `/` - [ ] [browser] Visit `/timeline.js` or `/vendor/d3-array.min.js` directly while logged out → redirects to `/` -- [ ] DevTools → Application → Cookies: `fenja_session` shows `HttpOnly ✓`, `Secure ✓`, `SameSite=Lax` +- [ ] DevTools → Application → Cookies: `fenja_session` shows `HttpOnly ✓`, `Secure ✓` (prod only), `SameSite=Lax` -## C. After changes to the entrance form, code input, or email +## C. After changes to the entrance form or login endpoint -- [ ] Submit a non-invited address → still advances to code screen (enumeration protection intact) +- [ ] Submit a non-invited address → inline "not invited" message, no session issued - [ ] Submit a malformed email (`foo`, `foo@`, empty) → inline error appears, no request sent -- [ ] Type a wrong 6-digit code → "doesn't match" error, cells highlight red, can retry -- [ ] Type 5 wrong codes → get "too many attempts" message; requesting a new code resets the counter -- [ ] Request a code, wait 11 minutes, try to use it → rejected +- [ ] Submit > 30 login attempts from the same IP in an hour → rate-limit response (429) ## D. After changes to the timeline / protected pages @@ -54,7 +52,7 @@ Do section A with extra attention to: - [ ] CSP contains at minimum: `default-src 'self'`, `script-src 'self'` (no `unsafe-inline` on scripts), `frame-ancestors 'none'` - [ ] `sudo nginx -t` → "syntax is ok" and "test is successful" - [ ] [local] Open timeline in a browser → no red CSP violations in DevTools console -- [ ] `curl.exe -I https://project-bifrost.fenja.ai/auth/request-code -X POST` returns quickly (rate-limit zone is functioning) +- [ ] `curl.exe -X POST https://project-bifrost.fenja.ai/auth/login -H 'Content-Type: application/json' -d '{"email":"nobody@example.com"}'` returns quickly (rate-limit zone is functioning; expect 403 `not_invited` on a real email not on the list) ## F. After dependency or Node.js upgrades diff --git a/CLAUDE.md b/CLAUDE.md index efd9b62..5943d01 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ node bin/joins.js list # read join-CTA click log There is no test suite, linter, or build step. Verification is the checklist in `CHECKLIST.md`, primarily by walking the entrance → code → timeline flow in a browser. -For local dev, `.env` must set `CODE_PEPPER` (≥32 chars; `openssl rand -hex 32`) and SMTP credentials — the server hard-exits on boot without a valid pepper. `NODE_ENV=production` toggles the `Secure` cookie flag, so leave it unset locally (HTTP). +For local dev, `.env` needs only `PORT`, `PUBLIC_ORIGIN`, and `NODE_ENV` — see `.env.example`. Auth is email-only against the invite list; there is no 6-digit code flow and no SMTP relay anymore, so no mail/pepper secrets are required. `NODE_ENV=production` toggles the session cookie's `Secure` flag — set it in `/etc/fenja/env` on the VPS; leave it unset (or `development`) locally for HTTP. ## Architecture @@ -36,7 +36,7 @@ Single-process Express app bound to `127.0.0.1:3000`. Nginx is the only public i 4. `express.static(public)` — ungated assets 5. `requireAuth` **then** `express.static(protected)` — gating runs *before* the file is read off disk. Adding a file to `protected/` gates it automatically; adding to `public/` exposes it automatically. -**Auth flow** (see `src/auth.js`): email → 6-digit code (HMAC-SHA256 with `CODE_PEPPER`, 10-min TTL) → `/auth/verify-code` (constant-time compare, 5 wrong guesses nukes the code) → opaque 256-bit session ID stored in SQLite, set as `HttpOnly; Secure; SameSite=Lax` cookie. No JWTs; revocation is a DELETE. `/auth/request-code` always returns 200 regardless of invite status (email-enumeration defense). +**Auth flow** (see `src/auth.js`): email → `POST /auth/login` → the server checks the invite list; on hit it issues an opaque 256-bit session ID stored in SQLite and sets it as an `HttpOnly; Secure; SameSite=Lax` cookie (returns `{ok, firstName}`). On miss it returns `403 {error:"not_invited"}` — email enumeration is acceptable here by design (invite-list-only, preview content). `POST /auth/logout` deletes the session row. `GET /auth/me` returns `{email, firstName}` or 401. No one-time codes, no SMTP, no JWTs; revocation is a DELETE. **Storage** (`src/db.js`): `better-sqlite3` at `data/fenja.sqlite`, WAL mode, tables `invites` / `sessions` / `rate_limits` / `bifrost_joins`. Prepared statements exported as `q.*`. A `setInterval().unref()` sweeps expired rows every 5 minutes. @@ -50,8 +50,7 @@ These are from `PROJECT.md`. A change that breaks any of them is a security regr - Protected HTML must never be readable without a valid session cookie — `requireAuth` runs before `express.static(protected)`, don't reorder. - Session cookie: `HttpOnly`, `Secure` (prod), `SameSite=Lax`, opaque random 256-bit ID. -- Codes hashed with HMAC-SHA256 using `CODE_PEPPER`; comparisons via `crypto.timingSafeEqual`. Never change the pepper on a live server (invalidates pending codes). -- `/auth/request-code` always 200 past the format check (no enumeration). +- `/etc/fenja/env` on the VPS is intentionally minimal — only `PORT`, `PUBLIC_ORIGIN`, `NODE_ENV`. No pepper, no SMTP, no mail-from. The only env value with security impact is `NODE_ENV=production` (enables the `Secure` cookie flag). - No inline `