customer-presentation/CLAUDE.md
Arlind Ukshini d5f578a581 update docs
2026-04-23 15:00:53 +02:00

5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Companion docs (read these first for non-trivial work)

  • PROJECT.md — architecture, auth flow, non-negotiable security properties, what is/isn't safe to change
  • INSTALL.md — one-time VPS setup
  • OPERATIONS.md — deploy, invite management, backups, troubleshooting
  • CHECKLIST.md — manual test matrix keyed by the area you touched (A through I). After any change, mentally walk the relevant section and call out which items the change plausibly affects.

Common commands

npm install                          # install deps
npm run dev                          # node --watch server.js, binds 127.0.0.1:3000
npm start                            # production start
node bin/invite.js add <email>       # invite (also: remove, list)
node bin/joins.js list               # read join-CTA click log
                                     # (also: summary, for <email>, stats)

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).

Architecture

Single-process Express app bound to 127.0.0.1:3000. Nginx is the only public ingress. ESM only ("type": "module").

Request routing lives in server.js and is deliberately ordered. Understanding this order is the key to the security model:

  1. Security headers + CSP (strict — script-src 'self', no inline scripts)
  2. /auth/* router (public)
  3. GET / dispatches to protected/index.html (timeline) if currentSession(req) is set, else public/entrance.html — the same URL serves different pages depending on cookie state
  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).

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.

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.

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.

Security invariants — do not violate without explicit approval

These are from PROJECT.md. A change that breaks any of them is a security regression even if nothing visibly breaks:

  • 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).
  • No inline <script> in any HTML — CSP is script-src 'self'. Put JS in a separate file with src="..." defer.
  • Node binds to 127.0.0.1 only; Nginx is the single ingress.
  • Secrets live in /etc/fenja/env on the VPS (not /opt/fenja/.env).

Safe-to-change vs. flag-before-touching

  • Safe: content/layout in public/ or protected/, the timeline (protected/timeline.js, data, visuals), copy, fonts/colors in protected/fenja/colors_and_type.css. Adding a new gated page = drop file in protected/ and it's automatically gated.
  • Flag in the change summary: anything in src/, server.js, deploy/, package.json, or .env.example. These touch operational surface area and need a human to redeploy/verify.

Conventions

  • ESM imports only, Node 20+.
  • File headers use the // ─── ... ─── comment banner style. Match it when editing existing files.
  • bin/invite.js and bin/joins.js are the admin CLIs — there is no web UI for either by design. invite.js manages the invite list; joins.js reads the CTA click log.