docs: foundational specs (PRD, architecture, design system, database, security, code style, testing) + AGENTS operational rulebook
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
# SECURITY — anwarpy.ai
|
||||
|
||||
> Threat model: a solo-writer content site. The assets are the content, the member trust (ADR-0005), and the pipeline key. Not a fintech app — but every rule below is load-bearing.
|
||||
|
||||
## 1. Authentication
|
||||
|
||||
- **Payload auth** (email + password). Signup writes `verification_token` + `emailVerified` flag at creation (ADR-0005 trust model); verification/resend routes: `/api/auth/verify-email`, `/api/auth/resend-verification`. Forgot/reset via Payload's built-in flow.
|
||||
- **Session seam**: `src/lib/viewer.ts` `getViewer()` is the only place components read identity. If auth provider changes (anticipated: Google OAuth or Clerk — owner decision 2026-10-11), it changes behind this seam; callers keep consuming `Viewer`.
|
||||
- **No password rules theater**: one live hint (strength guidance) at signup; no confirm-password field (owner decision, recorded).
|
||||
- **Lockout**: login attempts on `/api/users` are rate-limited by `src/middleware.ts`; the 429 message stays honest («too many attempts» fidelity — step 14 evidence). No CAPTCHA (owner decision, recorded 2026-10-10).
|
||||
- Verification emails go through **Cloudflare Email REST** (ADR-0003, never SMTP). Transport down → the UI/API says so in Arabic («خدمة البريد غير مربوطة بعد.»); it does not pretend success.
|
||||
|
||||
## 2. Environment variables & secrets
|
||||
|
||||
| Var | Scope | Rule |
|
||||
|---|---|---|
|
||||
| `DATABASE_URI` / `DB_PASSWORD` | server | compose builds the URI; never commit, never echo |
|
||||
| `PAYLOAD_SECRET` | server | signing/encryption; rotate = invalidate sessions |
|
||||
| `AGENT_API_KEY` | server | pipeline auth; header `x-agent-key` |
|
||||
| `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `MAIL_FROM` | server | mail transport |
|
||||
| `LAUNCH_MODE` | server | launch policy (SECURITY-relevant: gates paid flows) |
|
||||
| `NEXT_PUBLIC_SITE_URL` | public | canonical/og origin |
|
||||
| `STRIPE_SECRET_KEY`, `STRIPE_PRICE_ID`, `STRIPE_WEBHOOK_SECRET` | server | Phase 3 |
|
||||
| `SEED_TEST_USERS` | dev only | never set in prod |
|
||||
|
||||
Rules:
|
||||
- `.env*` files are gitignored and **excluded from rsync deploys**; prod secrets live only in `/root/anwarpi/.env.production`.
|
||||
- Never print secret values into transcripts, docs, videos, or evidence. Known incident: 8 chars of prod `DB_PASSWORD` leaked in a session transcript on 2026-10-10 → **owner to rotate**; rotation is the standing remedy for any partial leak.
|
||||
- Read secrets at runtime via `process.env` inside server modules only; never in `'use client'` code (only `NEXT_PUBLIC_*` there).
|
||||
|
||||
## 3. RBAC & content access
|
||||
|
||||
- Two tiers: `role: 'admin'` (Ahmed, `/admin` + agent pipeline ownership) and everyone else (member/public). No roles table beyond that until a real need.
|
||||
- **Content ACL is the crown jewel** (ARCHITECTURE §3): `Posts.content` field-level read rule — public iff `status=published && (access=free || freeEarlyAccess())`. VIP previews (H2 list, dek) are the only VIP fields served to logged-out users when billing is on.
|
||||
- Files: `GET /api/files/[id]` enforces **per-file access** before streaming; the download gate and the article page must agree (one decision, ARCHITECTURE §3).
|
||||
- Admin UI access = Payload's built-in `admin` gate; never hand-roll admin pages on the frontend.
|
||||
|
||||
## 4. Input handling & pipeline hardening
|
||||
|
||||
- **`pickFields`** on the agent API strips privileged fields (`access`, `readingMinutes`, `status` except explicit `publish: true` opt-in on POST; PUT cannot set `status`).
|
||||
- **`scanForSecrets` / `scanAgentPayload`** (`src/lib/secret-scan.ts`): POST and PUT scan the *real text* (Lexical → `lexicalToText`, or the raw string) for AWS AKIA keys, PEM blocks, `sk-` API keys, GitHub tokens (`ghp_`…), generic credential shapes → **422 with pattern NAMES only**. Never echo the matched text, never log it.
|
||||
- String `content` → 400 (Lexical JSON required); no silent placeholder substitution.
|
||||
- Comment bodies: textarea, moderated (pending until approved), author forced server-side from the session — client-supplied `author`/`authorName` identity is ignored/overwritten (ADR-0002). Display names are a snapshot, not a live join, so renamed users can't retroactively forge old comments.
|
||||
- All user-facing error strings are fixed Arabic sentences from the route handlers — errors never reflect raw input back (no echo = reflected-input hygiene).
|
||||
|
||||
## 5. Transport & external calls
|
||||
|
||||
- Everything behind Traefik with TLS on anwarpy.ai; www → apex 308.
|
||||
- Outbound: Cloudflare Email REST with scoped token; Stripe webhook verifies signature (`STRIPE_WEBHOOK_SECRET`) before trusting the body; agent API requires the key header (Authorization Bearer → 401).
|
||||
- The db container publishes **no ports**; prod DB access for maintenance = SSH tunnel (AGENTS.md rulebook recipe), never a public 5432.
|
||||
|
||||
## 6. Operational security
|
||||
|
||||
- Backups: `web/scripts/backup-db.sh` → gzipped `pg_dump`; rehearsed restore 2026-10-10 (temp DB, count check, drop). Backup artifacts stay on the VPS `backups/` dir, never in git, never in the deploy rsync.
|
||||
- Test rows (smoke signups, notify rows) use `@example.com` addresses and are **deleted after verification** — prod counts (`users=6`, `launch_notifications=0`) are the post-cleanup ground truth.
|
||||
- Media/files volumes are excluded from rsync (`no --delete` protects them).
|
||||
- Incident rule: any suspected leak (secret in transcript/log/URL) → rotate the credential first, then investigate. Recording the incident in docs is required; pretending it didn't happen is not.
|
||||
|
||||
## 7. What NOT to add (recorded decisions)
|
||||
|
||||
- No CAPTCHA, no aggressive rate limits beyond login lockout (owner, 2026-10-10).
|
||||
- No WAF/Cloudflare proxy changes without owner approval — the domain's DNS/proxy is Ahmed's console.
|
||||
- No secrets in client bundles; no `NEXT_PUBLIC_` for anything except the site URL.
|
||||
- No new auth provider without routing it through `viewer.ts` (see §1).
|
||||
Reference in New Issue
Block a user