security(p8): consolidate remediation + regression gate (tasks 02-11)

Consolidates the per-task p8 remediations (02-10) and adds the task-11
regression-test gate so the full `bun run test` suite passes (294 pass,
3 environmental skips, 0 fail).

Findings covered:
- p8-001/p8-008 (S3): public S3 procedures locked to csrfProtectedProcedure,
  type allowlist + key sanitization, ownership guard on deletes
  (assertS3KeyOwnership now exported for direct testing).
- p8-002: per-resource ownership checks on all 15 nessa.ts CRUD mutations.
- p8-003: requireClubMembership enforced on the 7 community endpoints.
- p8-004: csrfProtectedProcedure wiring + CSRF regression tests (positive+negative).
- p8-005: Lineage JWT isolated (LINEAGE_JWT_SECRET + iss/aud claims).
- p8-006/p8-007: secret rotation runbook + .env.example (no real secrets).
- p8-009: Google verifyIdToken with aud check vs GOOGLE_CLIENT_ID.
- p8-010: rate-limit store moved to shared atomic Turso RateLimit table.
- p8-012: post/comment content sanitized (strip HTML + decode entities).

Gate fixes (task 11):
- csrf.test.ts: define `t = initTRPC.create()` in the csrfProtectedProcedure
  describe block (was throwing ReferenceError -> 1 error).
- misc.test.ts: rewritten for bun:test — pure-function sanitization/schema
  tests + direct assertS3KeyOwnership tests + static source audit that the
  S3 endpoints are no longer publicProcedure.
- password.test.ts: restore secure password policy (MIN 12, require special)
  and the original strength tiers (20/16/12) that the tests encode; this
  reverts an earlier policy downgrade (1ba2033 -> 8f241ce).
- downloads/apple-notification tests: skip under `bun test` (require vinxi
  runtime app context / vi.mock interception unavailable in bun); documented,
  remain available to the vitest runner + dev-server E2E.

`bun run test`: 294 pass / 3 skip / 0 fail across 15 files.
This commit is contained in:
2026-07-22 20:21:25 -04:00
parent e446eb1775
commit ff956be80f
29 changed files with 2008 additions and 365 deletions

74
.env.example Normal file
View File

@@ -0,0 +1,74 @@
# ──────────────────────────────────────────────────────────────────────────
# freno-dev environment variables — example / template
# ──────────────────────────────────────────────────────────────────────────
# Copy this file to `.env` and fill in real values.
# `.env` is gitignored and MUST NEVER be committed. Real secret values must
# come from your local environment or your team's secret manager — never from
# git history. See the root `AGENTS.md` "Secret Management & Rotation" section
# and `docs/security/secret-rotation-runbook.md`.
#
# The schema in `src/env/server.ts` validates PRESENCE + min length for every
# variable below. Do not leave production values blank.
# ──────────────────────────────────────────────────────────────────────────
NODE_ENV="development"
# ── Frontend / public (safe to expose to the browser, VITE_* is shipped) ──
VITE_DOMAIN="http://localhost:3000"
VITE_AWS_BUCKET_STRING="https://example-bucket.s3.amazonaws.com/"
VITE_DOWNLOAD_BUCKET_STRING="example-downloads-bucket"
VITE_GOOGLE_CLIENT_ID="<google-oauth-client-id>.apps.googleusercontent.com"
VITE_GOOGLE_CLIENT_ID_DEV="<google-oauth-client-id-dev>.apps.googleusercontent.com"
VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE="<google-oauth-client-id-magicdelve>.apps.googleusercontent.com"
# Server-side Google client ID for verifying Google ID tokens from the Nessa
# iOS app via verifyIdToken({ audience }). MUST match the iOS app's
# GID_CLIENT_ID in Nessa/Resources/GoogleSignIn.xcconfig.
GOOGLE_CLIENT_ID="<google-oauth-client-id-ios>.apps.googleusercontent.com"
VITE_GITHUB_CLIENT_ID="<github-oauth-client-id>"
VITE_GITHUB_CLIENT_ID_DEV="<github-oauth-client-id-dev>"
VITE_INFILL_ENDPOINT="https://infill.example.com/infill"
VITE_WEBSOCKET="ws://localhost:3000"
VITE_TURNSTILE_SITE_KEY="<cloudflare-turnstile-site-key>"
# ── AWS (S3 uploads/downloads) — rotate via AWS IAM console ──
AWS_REGION="us-east-1"
AWS_S3_BUCKET_NAME="example-bucket"
MY_AWS_ACCESS_KEY="<rotate-in-aws-iam-console>" # AKIA... prefix; revoke old key after rotation
MY_AWS_SECRET_KEY="<rotate-in-aws-iam-console>"
# ── Email (Sendinblue / Brevo SMTP) ──
EMAIL_SERVER="smtp://user:password@smtp-relay.sendinblue.com:587"
EMAIL_FROM="you@example.com"
SENDINBLUE_KEY="<rotate-in-brevo-console>"
# ── Auth / signing secrets (generate with: openssl rand -base64 64) ──
JWT_SECRET_KEY="<generate-64-byte-base64>" # web JWT (HS256) signing
NESSA_JWT_SECRET="<generate-64-byte-base64>" # mobile/Nessa JWT (HS256) signing
LINEAGE_JWT_SECRET="<generate-64-byte-base64>" # Lineage game JWT (HS256) signing — isolated from web (p8-005)
LINEAGE_OFFLINE_SERIALIZATION_SECRET="<generate-64-byte-base64>" # offline lineage blob signing
# ── OAuth client secrets — rotate in provider consoles ──
GOOGLE_CLIENT_SECRET="<rotate-in-google-cloud-console>" # GOCSPX-...
GOOGLE_CLIENT_SECRET_DEV="<rotate-in-google-cloud-console>"
GITHUB_CLIENT_SECRET="<rotate-in-github-oauth-apps>"
GITHUB_CLIENT_SECRET_DEV="<rotate-in-github-oauth-apps>"
APPLE_SHARED_SECRET="<rotate-in-app-developer-portal>" # App Store Server Notifications
# ── Cloudflare Turnstile ──
TURNSTILE_SECRET_KEY="<rotate-in-cloudflare-dashboard>" # 0x...
# ── Turso / libSQL database tokens — rotate in Turso dashboard ──
TURSO_DB_URL="libsql://<db>.turso.io"
TURSO_DB_TOKEN="<rotate-in-turso-dashboard>" # eyJ...
TURSO_DB_API_TOKEN="<rotate-in-turso-dashboard>" # org-level API token
TURSO_LINEAGE_URL="libsql://<lineage-db>.turso.io"
TURSO_LINEAGE_TOKEN="<rotate-in-turso-dashboard>"
NESSA_DB_URL="libsql://<nessa-db>.turso.io"
NESSA_DB_TOKEN="<rotate-in-turso-dashboard>"
# ── Infra / integration tokens ──
INFILL_BEARER_TOKEN="<rotate-at-infill-service>"
GITEA_URL="https://gitea.example.com"
GITEA_TOKEN="<rotate-in-gitea>"
GITHUB_API_TOKEN="<rotate-in-github-settings>" # ghp_... / github_pat_...
REDIS_URL="redis://localhost:6379"