apiVersion: capsule.dev/v0.1 kind: Capsule name: yingjieli-admin-auth version: 1.0.0 type: subsystem domain: yingjieli.site maintainers: - name: Quake email: quake0day@gmail.com purpose: summary: | Single source of truth for "is this request the site admin?". Implements a password login that yields an HMAC-signed session cookie (7-day TTL), plus per-IP brute-force rate limiting via Cloudflare KV. owns: - POST /api/auth (login), DELETE /api/auth (logout), GET /api/auth (status) - the yl_admin HttpOnly cookie format and TTL - HMAC session token construction and verification - per-IP rate limit (5 attempts / 5 min) backed by KV - the isAuthed(request, env) helper that every other capsule must use does_not_own: - the admin's *identity* beyond "knows the shared password" (single-user system) - what an authed admin is allowed to do (other capsules enforce their own write gates) - user-facing login UI (lives in yingjieli-admin-ui) interfaces: provides: - kind: http_api name: auth-login entrypoint: src/api/auth.js description: POST /api/auth — exchange password for session cookie. - kind: http_api name: auth-logout entrypoint: src/api/auth.js description: DELETE /api/auth — clear session cookie. - kind: http_api name: auth-status entrypoint: src/api/auth.js description: "GET /api/auth → { authenticated: bool }." - kind: library name: auth-helpers entrypoint: src/_lib/auth.js description: | isAuthed, createSession, verifySession, setSessionCookie, clearSessionCookie, json, unauthorized, checkPasswordRateLimit. requires: - kind: env name: ADMIN_PASSWORD description: Shared admin password. REQUIRED; POST /api/auth 500s without it. - kind: env name: SESSION_SECRET description: HMAC signing key for session tokens. Falls back to ADMIN_PASSWORD if unset. - kind: env name: YL_DATA description: KV namespace; used for per-IP rate-limit counters. dependencies: capsules: [] runtime: - node: ">=18" - cloudflare-pages: "*" agent: summary_for_ai: | Single source of truth for admin auth. Other capsules MUST import isAuthed() from _lib/auth.js — they must never decode the cookie themselves and must never re-implement HMAC verification. avoid: - Decoding the yl_admin cookie outside this capsule. - Storing the password or hash anywhere in code or KV (env var only). - Removing the Secure / HttpOnly / SameSite=Strict cookie flags. verification: health_checks: - id: lib-auth-syntax command: node --check src/_lib/auth.js - id: api-auth-syntax command: node --check src/api/auth.js functional_tests: - id: roundtrip-session-token command: | node --input-type=module -e "import('./src/_lib/auth.js').then(async m=>{const env={SESSION_SECRET:'test-secret'};const t=await m.createSession(env);const ok=await m.verifySession(t,env);if(!ok){console.error('verify failed');process.exit(1)}console.log('ok')})" proves: - createSession() output verifies cleanly via verifySession() under the same secret. invariants: - A revoked / expired session must never authenticate a subsequent request. - ADMIN_PASSWORD must never appear in any response body or log line. - Cookie is always Secure + HttpOnly + SameSite=Strict. x-reconstruct: install: install.json notes: | Pure passthrough capsule — install.json copies src/* unchanged to the site's functions/ tree. No data injection or templating needed.