diff --git a/.env.example b/.env.example index fa6c60f..58ee57d 100644 --- a/.env.example +++ b/.env.example @@ -1,52 +1,86 @@ -CATALYST_WEBHOOK_URL= -DODOPAY_API_KEY= -DODOPAY_SANDBOX_MODE= -DODOPAY_WEBHOOK_SECRET= -MAILGUN_API_KEY= -MAILGUN_DOMAIN= -MAILGUN_FROM_EMAIL= -MAILGUN_FROM_NAME= -MAILGUN_HOST= -MONGODB_DB_NAME= -MONGODB_URI= -NEXTAUTH_SECRET= -NEXTAUTH_URL= -PAGESPEED_API_KEY= -UPSTASH_REDIS_REST_TOKEN= -UPSTASH_REDIS_REST_URL= -V2CHECKOUT_MERCHANT_CODE= -V2CHECKOUT_SANDBOX_MODE= -V2CHECKOUT_SECRET_KEY= -VITE_APP_URL= -VITE_ENABLE_ANALYTICS= -VITE_ENABLE_PERFORMANCE_MONITORING= -VITE_FIREBASE_API_KEY= -VITE_FIREBASE_APP_ID= -VITE_FIREBASE_AUTH_DOMAIN= -VITE_FIREBASE_MEASUREMENT_ID= -VITE_FIREBASE_MESSAGING_SENDER_ID= -VITE_FIREBASE_PROJECT_ID= -VITE_FIREBASE_STORAGE_BUCKET= - -# --- Phase 0 security hardening ------------------------------------------- -# GitHub webhook HMAC secret (sha256 signatures are verified over the raw body). -# Configure this in your GitHub repo webhook settings; leave empty to reject -# all GitHub webhook deliveries. +# CatalystLab environment template +# Copy to `.env` for local use: cp .env.example .env +# `.env` is gitignored. This file is tracked (see !.env.example in .gitignore). +# Leave secrets empty in this template. Production must set real values. + +# ============================================================================= +# Runtime +# ============================================================================= +# development | test | production (production is always default-deny for authz) +NODE_ENV=development +# HTTP listen address. Use 0.0.0.0 so the preview proxy can reach the server. +HOST=0.0.0.0 +PORT=3000 +# pino log level: fatal | error | warn | info | debug | trace +LOG_LEVEL=info + +# ============================================================================= +# Reverse proxy / client IP +# ============================================================================= +# Set to "true" ONLY when Express sits behind a trusted proxy (Cloud Run, nginx). +# When unset, X-Forwarded-For / CF-Connecting-IP are ignored for rate limits, +# client-log buckets, and telemetry geo hashing. +TRUST_PROXY= + +# ============================================================================= +# Auth / demo surfaces +# ============================================================================= +# Skip Firebase identity on GitHub/notifications/API-key demo routes. +# Honored ONLY when NODE_ENV=test (vitest). Ignored in development and production. +ALLOW_UNAUTH_DEMO= + +# Firebase Admin — server-side ID token verification (attachIdentity, state sync). +# Unset => requests with Bearer tokens fail closed (401); anonymous stay visitors. +# Inline JSON (raw or base64) takes precedence over a file path. +FIREBASE_SERVICE_ACCOUNT_JSON= +FIREBASE_SERVICE_ACCOUNT_PATH= +# Standard Google ADC path (used if FIREBASE_SERVICE_ACCOUNT_PATH is empty). +GOOGLE_APPLICATION_CREDENTIALS= + +# Comma-separated allowlist of cat_live_... keys accepted via the x-api-key header. +# Unset => API-key rate tier is disabled (fail closed). Never put keys in Authorization: Bearer. +VALID_API_KEYS= + +# ============================================================================= +# Webhooks (HMAC over the raw request body) +# ============================================================================= +# GitHub repo webhook secret. Empty => unsigned deliveries are rejected. GITHUB_WEBHOOK_SECRET= -# Webhook signing secrets for payment gateways (HMAC-SHA256 over raw body). -# Per-gateway value wins over the shared value; unset => deliveries rejected (503). +# Shared payment webhook secret. Per-gateway vars below override this. PAYMENTS_WEBHOOK_SECRET= PAYMENTS_WEBHOOK_SECRET_2CHECKOUT= PAYMENTS_WEBHOOK_SECRET_DODOPAY= -# Comma-separated allowlist of valid ``cat_live_...`` API keys. Unset => the -# API-key rate tier is disabled entirely (Phase 1 replaces this with hashed, -# persisted keys). -VALID_API_KEYS= +# ============================================================================= +# Payments (fail closed — missing credentials => 503, no fake checkout) +# ============================================================================= +V2CHECKOUT_MERCHANT_CODE= +V2CHECKOUT_SECRET_KEY= +# "false" = live 2Checkout URLs; any other value = sandbox +V2CHECKOUT_SANDBOX_MODE= -# Firebase Admin service account for server-side ID token verification -# (state sync auth). Either inline JSON (optionally base64) or a file path. -# Unset => /api/state/sync responds 401 to everyone (fail closed). -FIREBASE_SERVICE_ACCOUNT_JSON= -FIREBASE_SERVICE_ACCOUNT_PATH= +DODOPAY_API_KEY= +DODOPAY_WEBHOOK_SECRET= +# "false" = live Dodo URLs; any other value = sandbox +DODOPAY_SANDBOX_MODE= + +# ============================================================================= +# Rate limiting (optional distributed limiter) +# ============================================================================= +# When both are set, engine routes also consume an Upstash sliding window (120/min). +# Unset => in-memory limiter only (fine for single instance / tests). +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= + +# ============================================================================= +# Analytics / Mailgun (optional) +# ============================================================================= +MONGODB_URI= +MONGODB_DB_NAME= + +MAILGUN_API_KEY= +MAILGUN_DOMAIN= +MAILGUN_FROM_EMAIL= +MAILGUN_FROM_NAME= +MAILGUN_HOST= diff --git a/.gitignore b/.gitignore index c32df22..f670f74 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ dist/ .nuxt/ .output/ -# Miscellaneous +# Secrets — never commit real env files. The template (.env.example) is tracked. .env .env.* !.env.example @@ -48,273 +48,11 @@ Thumbs.db # Vercel .vercel -# Others -.cache -.tempdencies -node_modules/ - -# Testing -coverage/ - -# Production builds -build/ -dist/ -.next/ -.nuxt/ -.output/ - -# Miscellaneous -.env -.env.* -!.env.example - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# IDEs and editors -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Misc -.npm -.yarn -.pnp -.yarnrc -.yarnrc.yml -.yarn/install-state.gz - -# TypeScript -*.tsbuildinfo - -# Vercel -.vercel - -# Others -.cache -.tempdencies -node_modules/ - -# Testing -coverage/ - -# Production builds -build/ -dist/ -.next/ -.nuxt/ -.output/ - -# Miscellaneous -.env -.env.* -!.env.example - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# IDEs and editors -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Misc -.npm -.yarn -.pnp -.yarnrc -.yarnrc.yml -.yarn/install-state.gz - -# TypeScript -*.tsbuildinfo - -# Vercel -.vercel - -# Others -.cache -.tempdencies -node_modules/ - -# Testing -coverage/ - -# Production builds -build/ -dist/ -.next/ -.nuxt/ -.output/ - -# Miscellaneous -.env -.env.* -!.env.example - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# IDEs and editors -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Misc -.npm -.yarn -.pnp -.yarnrc -.yarnrc.yml -.yarn/install-state.gz - -# TypeScript -*.tsbuildinfo - -# Vercel -.vercel - -# Others -.cache -.tempdencies -node_modules/ - -# Testing -coverage/ - -# Production builds -build/ -dist/ -.next/ -.nuxt/ -.output/ - -# Miscellaneous -.env -.env.* -!.env.example - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# IDEs and editors -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Misc -.npm -.yarn -.pnp -.yarnrc -.yarnrc.yml -.yarn/install-state.gz - -# TypeScript -*.tsbuildinfo - -# Vercel -.vercel - -# Others -.cache -.tempdencies -node_modules/ - -# Testing -coverage/ - -# Production builds -build/ -dist/ -.next/ -.nuxt/ -.output/ - -# Miscellaneous -.env -.env.* -!.env.example - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# IDEs and editors -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Misc -.npm -.yarn -.pnp -.yarnrc -.yarnrc.yml -.yarn/install-state.gz - -# TypeScript -*.tsbuildinfo - -# Vercel -.vercel - -# Others +# Cache .cache .temp - # Python tooling artifacts __pycache__/ *.pyc .coverage -coverage/ diff --git a/CODE_REVIEW_FIXES.md b/CODE_REVIEW_FIXES.md new file mode 100644 index 0000000..f61c5af --- /dev/null +++ b/CODE_REVIEW_FIXES.md @@ -0,0 +1,96 @@ +# Code-review fix reports (chronological) + +Each item is the original finding, what changed, and how to verify. + +## Critical + +### C1 — Firestore superadmin via email allowlist +**Issue:** `isSuperAdmin()` / `hasSuperAdminClaim()` treated four hardcoded emails as admin, so a spoofed or compromised Google account with those addresses bypassed claims. +**Fix:** Superadmin is exclusively `request.auth.token.role == 'superadmin'` or `token.superadmin == true`. Email lists removed from `firestore.rules`. +**Verify:** Rules no longer mention `shuvo*` emails; unauthenticated or claim-less users cannot write privileged collections. + +### C2 — Entitlements query `userId` vs `ownerId` / document id +**Issue:** `fetchEntitlements` queried `user_subscriptions` with `.where('userId', '==', uid)`, missing the real doc keyed by uid / `ownerId`. +**Fix:** `src/lib/serverAuth.ts` reads `user_subscriptions/{uid}`. Vitest mock updated to `.doc().get()` with `{ exists, data }`. +**Verify:** `phase1Hardening.test.tsx` “maps Firestore subscription docs…” passes with `plan: 'pro'`. + +### C3 — Discord `test-discord` ungated +**Issue:** `/api/notifications/webhook/test-discord` posted to caller-supplied URLs without auth. +**Fix:** Same `requireSuperadmin` gate as Slack/email (demo only when `ALLOW_UNAUTH_DEMO=true` and not production). +**Verify:** Production without identity → 401; signed-in non-admin → 403. + +### C4 — GitHub unauthenticated in-memory bus +**Issue:** List/create/stream/events were public; webhook secrets used `Math.random`; SSE broadcast to every subscriber. +**Fix:** `requireIdentity` on all GitHub routes except HMAC webhook. `ownerId` from verified uid. `crypto.randomBytes` for secrets. SSE `Map` filters events. +**Verify:** `api.test.ts` GitHub suite still 200 under test `ALLOW_UNAUTH_DEMO`; webhook HMAC tests unchanged. + +### C5 — Markdown / public HTML XSS +**Issue:** Markdown `[text](javascript:…)` and `public/*.html` interpolated engine output / URLs into `innerHTML`. +**Fix:** `sanitizeHref` in `MarkdownRenderer` (http/https/mailto/relative/# only). Shared `public/xss-guard.js` (`CLEscape` / `CLColorize`) included from static pages; interpolations escaped before colorize. +**Verify:** `javascript:` links render as text; terminal output with ` + + + + + + + + + + + + + + + + + @@ -237,9 +242,12 @@

📦 Repository Hygiene

+ + + + + + + + + + + + + + +