Basilic
Architecture

Security Baseline

Pre-commit and CI scanning for secrets and CVEs, plus DeepSec source-code review. OSS tools only.

Layers: local pre-commit, security.yml on every PR and push to main, DeepSec on trusted same-repo PRs (deepsec.yml), and Fastify request hardening. Install scanners with pnpm run setup. Manual install: gitleaks, OSV Scanner, TruffleHog.

Pre-commit

simple-git-hooks runs hooks:pre-commit: block secret file types → gitleaks on staged files → OSV on dependencies → Biome on staged files.

Blocked paths include .env (templates like .env.defaults.example, .env.schema, .env.test are allowed), *.pem / *.key / *.p12 / keystores, id_rsa*, certificate dumps, *.keytab.

Gitleaks looks for crypto keys and mnemonics, API keys, JWTs, DB passwords, and cloud credentials.

pnpm security:block-files
pnpm security:secrets          # staged
pnpm security:secrets:full     # whole repo
pnpm security:osv
pnpm security:audit
pnpm security:check            # all of the above
pnpm security:deepsec:scan     # regex only, no AI
pnpm security:deepsec:process:diff   # AI review vs origin/main
pnpm security:deepsec:process:diff:grok  # same, Cursor Grok 4.6
pnpm security:deepsec:process        # full-repo AI review (local)

CI (security.yml)

Lockfile integrity (--frozen-lockfile), gitleaks, TruffleHog (filesystem + history), OSV on pnpm-lock.yaml, pnpm audit (--ignore-registry-errors so a registry timeout is not a finding). DeepSec is a separate workflow. See GitHub Actions.

DeepSec

DeepSec reviews application code. It does not replace gitleaks, TruffleHog, OSV, or pnpm audit. Workspace: .deepsec/ at the repo root (not a pnpm workspace package). Commit deepsec.config.ts, INFO.md, and generated matchers. Scan output, .env.local, and node_modules stay gitignored. Not in pre-commit or security.yml.

Agents: CI and default local process use GPT-5.6 Sol on Codex (--agent codex --model gpt-5.6-sol). Alternate: Grok 4.6 on DeepSec pi (pnpm security:deepsec:process:diff:grok). Both go through Vercel AI Gateway. Claude is not used.

Local: scan is regex-only and free. process / process:diff need AI_GATEWAY_API_KEY. Full-repo process is an operator follow-up.

CI: Trusted same-repo PRs only. Job split, secrets, and skip conditions: GitHub Actions.

If a secret lands in git

  1. Rotate it immediately. Editing the file is not enough.
  2. If not pushed: git reset HEAD~1, remove the secret, commit again.
  3. If pushed: history rewrite (git-filter-repo / BFG) and a coordinated force-push. False positives go in .gitleaks.toml — never real secrets.

API hardening

  • Headers: nosniff, frame deny, CSP, HSTS in production (apps/api/src/plugins/security.ts).
  • CORS: ALLOWED_ORIGINS must be explicit origins in production (boot fails on * or empty). Fastify plugins/cors.ts is the source of truth — do not set Access-Control-Allow-Origin in vercel.json or other deployment headers. Dev/test may use *.
  • Rate limit: in-memory per client IP (RATE_LIMIT_MAX / RATE_LIMIT_TIME_WINDOW). Behind a reverse proxy, set TRUST_PROXY=true so Fastify honors X-Forwarded-For (Fastify 5 treats numeric hop counts as fail-closed). Local-only, not shared across instances.
  • Validation: TypeBox on Fastify routes. Suspicious URL/user-agent patterns are logged in apps/api/src/lib/security.ts (not blocked; rate limits handle abuse).
  • JWT access and refresh tokens are signed and verified with HS256 only (@fastify/jwt).
  • POST /ai/chat does not fetch client-supplied file URLs; only data: file parts are accepted. Remote URLs are rejected at validation.
  • 6-digit login codes (magic link, change email): HMAC-SHA256 with ENCRYPTION_KEY. API keys and other high-entropy tokens: SHA-256 at rest with timingSafeEqual on verify.

Login route rate limit (as shipped)

A tighter cap applies only to routes that start login or exchange OAuth/passkey callback codes — not every unauthenticated auth mutation.

authLoginRouteConfig wires authRouteRateLimit: in production, productionLoginRateLimitMax (10) requests per RATE_LIMIT_TIME_WINDOW; otherwise RATE_LIMIT_MAX (default 100; Vitest and E2E use 10000).

Routes under this cap:

  • POST /auth/magiclink/request
  • OAuth GET …/authorize-url and POST …/exchange for GitHub, Google, Facebook, and Twitter
  • POST /auth/oauth/google/verify-id-token
  • POST /auth/passkey/start
  • POST /auth/passkey/verify
  • POST /auth/passkey/exchange
  • POST /auth/passkey/resolve-user
  • POST /auth/sessions/revoke

Other controls (not the Fastify 10/min cap): magic-link verify uses authAttempts (5 per 15 minutes → TOO_MANY_ATTEMPTS); OAuth link authorize-url is Bearer-authenticated with 10 requests per user per hour in the database.

Limits are in-memory per API instance, keyed by trusted client IP (getTrustedClientIp / request.ip). Behind a reverse proxy, set TRUST_PROXY=true.

Session list, revoke, and new-device mail

  • GET /auth/sessions / DELETE /auth/sessions/:id: Bearer JWT only. API keys get 400 USE_KEY_REVOKE.
  • POST /auth/sessions/revoke: public (security: []), body { token, verificationId }. Compare-and-swap consume of session_revoke. Idempotent 200 { ok: true } on a second click. Wrong/expired token: INVALID_TOKEN / EXPIRED_TOKEN.
  • Revoke CTA origin is API WEB_APP_URL, not the request Origin (OAuth exchange is server-side). If WEB_APP_URL is not allowlisted, Fastify skips the email.
  • Web: /auth/session/revoke is a public proxy path (prefix match). Query verificationId + token call Fastify POST /auth/sessions/revoke. Rate-limited with other login routes (authLoginRouteConfig).

The Next.js POST /api/auth/update-tokens adapter writes api.session only for same-origin browser requests whose access/refresh pair Fastify has validated (POST /auth/session/validate-tokens). The cookie remains JS-readable (httpOnly: false) by design — this gate closes cross-site and forged writes, not same-origin XSS.

On this page