capsule AI-native Unix-like composition layer

capsule.yaml

3,658 bytes · 102 lines · capsule://quake0day/[email protected] raw on github

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.