25. Hardening — guardrail gates + security review
Date: 2026-06-06
Status
Accepted
Context
Step 25 adds automated guardrails and records a security self-review of the build so far. (The multi-agent adversarial review the earlier steps used is currently blocked by an Anthropic monthly spend limit; this is the manual pass in its place.)
Decision
- Guardrail gate (
scripts/guardrails.mjs,pnpm guardrails): fails on either forbidden pattern undersrc/, withfile:line:- Wise funding call (
fundTransfer,fundWithBalance,.fund(,/transfers/…/payments) — money movement is DRAFT-ONLY (ADR-0007); the owner funds in Wise. None exist today; the gate keeps it that way. - Secret in a
NEXT_PUBLIC_*var (…SECRET|SERVICE_ROLE|SERVICE_KEY|PRIVATE|PASSWORD) — those ship to the browser. (The public anon key is correctly not flagged.) Wired into lefthookpre-pushalongside typecheck + tests. (No GitHub Actions workflow exists in this repo yet — adding the samepnpm guardrails && pnpm typecheck && pnpm testto CI is a one-step follow-up indocs/test-checklist.md.)
- Wise funding call (
testscript fixed tovitest run(was barevitest= watch mode, which would hang CI/pre-push);test:watchadded for local use.
Security posture (reviewed)
- Tenancy: RLS on every table; the service-role client (which bypasses RLS) is used only in
server actions/services after an explicit auth check, and those reads are company-scoped. SECURITY
DEFINER RPCs (payroll close/discard, invoice create/void, check reconcile) are revoked from
anon/authenticatedand granted only toservice_role; advisors clean apart from the account-level "leaked password protection" toggle. - Money: integer minor units throughout; pay/invoice/interest/reconcile engines are pure + golden-tested; owner-gating on every money/destructive action; atomic RPCs with row-lock / optimistic-concurrency guards; double-pay/double-invoice/double-reconcile guards.
- Attribution: financial RPCs set
app.actor_idso the audit log records the acting owner under the service role. - Data minimization: bank accounts stored last-4 only; check OCR omits routing/account numbers; magic-link tokens hashed, single-use, expiring; secrets only in env.
- A11y: components follow the project guidelines — visible labels,
role="alert"/<output>for feedback, status conveyed by text + color (never color alone), ≥44px primary touch targets, semantic tables in horizontal-scroll wrappers. A full keyboard/contrast/screen-reader audit remains a manual checklist item.
Consequences
- A push that introduces a funding endpoint or a client-exposed secret now fails locally before it can land.
- Remaining hardening is tracked in
docs/test-checklist.md: the integration test lane (RLS per tenant, concurrency-safe numbering/close, magic-link single-use), the GitHub Actions gate, enabling Supabase leaked-password protection, and the manual a11y/security audit + staging dry-run.