diff --git a/CLAUDE.md b/CLAUDE.md index 0ed9cce..9ba8051 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ Single-process Express app bound to `127.0.0.1:3000`. Nginx is the only public i `bifrost_joins` logs every click of the final "Join Project Bifrost" CTA — one row per click (auto-increment `id`, `email`, `clicked_at`, `session_id`). Writes come from `POST /api/bifrost-join` (behind `requireAuth`); reads come from `bin/joins.js`. See OPERATIONS.md for admin usage. -**Hidden admin page** at `/fenjaops` (deliberately obscure URL, not `/admin`) — gated by `requireAuth` + `requireAdmin` (`is_admin` column on `invites`). Non-admins get a plain 404 so the URL's existence isn't leaked. Files live in `admin/` at the repo root (outside `public/` and `protected/` so only the explicit route reaches them). Admins can create **non-admin** invites from the page (`POST /api/fenjaops/invites` → stores `is_admin=0`, audit trail records the acting admin's email in `invited_by`). Promotion to admin and removal of invites stay **CLI-only** via `bin/invite.js admin add|remove|list` — a web session compromise cannot escalate the invite list. Internal code keeps the word "admin" (middleware, files, CLI); only the public URL is obscured. +**Hidden admin page** at `/fenjaops` (deliberately obscure URL, not `/admin`) — gated by `requireAuth` + `requireAdmin` (`is_admin` column on `invites`). Non-admins get a plain 404 so the URL's existence isn't leaked. Files live in `admin/` at the repo root (outside `public/` and `protected/` so only the explicit route reaches them). Admins can **create** non-admin invites (`POST /api/fenjaops/invites`, stores `is_admin=0`, audit trail records the acting admin in `invited_by`) and **remove** non-admin invites from the page (`DELETE /api/fenjaops/invites/:email`; rejects removing admins or oneself, also kills any active sessions for the deleted email). Admin promotion / demotion stays CLI-only (`bin/invite.js admin add|remove|list`) so a web session compromise cannot escalate or lock everyone out. Internal code keeps the word "admin" (middleware, files, CLI); only the public URL is obscured. **Rate limiting** (`src/middleware.js`) is a SQLite-backed sliding window keyed per-IP — 5 code requests/hour, 20 verify attempts/hour. Nginx adds another layer via a `limit_req_zone` declared in `/etc/nginx/nginx.conf`. diff --git a/PROJECT.md b/PROJECT.md index 7a9481d..bba7038 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -103,7 +103,7 @@ These things define the security model. Breaking any of them is a regression eve - **No inline `