apiVersion: capsule.dev/v0.1
kind: Capsule
name: yingjieli-admin-auth
version: 1.0.0
type: subsystem
domain: yingjieli.site
maintainers:
- name: Quake
email: [email protected]
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.