feat(workspaces): encrypted write-only credential storage

Workspaces Payload collection with the PAT as a write-only field:
access.read () => false strips it from every access-controlled read
path, and a field-level beforeChange hook encrypts on write with
AES-256-GCM (scrypt key from VEECT_SECRET, random per-value salt + IV,
v1 storage format) via node:crypto only. The real repository replaces
the phase-1 stub with payload create/findByID; toDomain never maps the
credential, and getDecryptedCredential(id) is the single server-side
decrypt path for the runner handoff (story 07). Contract suite now
covers create, write-only behaviour, and the decrypt path against both
the mock and the Payload impl (stub runs the real collection hooks).
Missing VEECT_SECRET fails production bind/boot with an actionable
message; dev-seed boots without it. Env declared in turbo.json
globalEnv + .env.example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 22:19:14 +02:00
parent 8219c1fabb
commit c990e1b871
21 changed files with 788 additions and 97 deletions

View File

@@ -11,6 +11,12 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5433/template
# Payload CMS encryption key. Any random 32+ char string in dev.
PAYLOAD_SECRET=replace-with-a-random-32-char-string
# Workspace credential encryption key (AES-256-GCM, scrypt-derived).
# Required in production mode - boot fails without it. Dev-seed mode
# (USE_DEV_SEED=true / plain `pnpm dev`) boots fine without it.
# Generate via `openssl rand -hex 32`.
VEECT_SECRET=replace-with-a-random-64-char-hex-string
# --- Optional: app URLs (defaults work in dev) ---
NEXT_PUBLIC_APP_URL=http://localhost:3000