Files
familysync/.planning/phases/16-ci-dependency-audit-and-security-checks/16-01-PLAN.md
T
Lucas Berger e039c85a22 docs(16): create phase plan — 6 plans, 2 waves (dep audit, security checks, image hygiene)
SEC-01/02, DEP-01/02, IMG-01/02/03, CI-03. Wave 1: image-hygiene runtime guard (TDD), audit+outdated wrappers (TDD), eslint-plugin-security fold, gitleaks config+baseline+.dockerignore. Wave 2: ci.yml security job + gate wiring, publish.yml hygiene assertions + boot-smoke. esbuild GHSA-gv7w-rqvm-qjhr waivered in 16-02 before the gate goes live.
2026-06-12 23:23:18 -04:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
16-ci-dependency-audit-and-security-checks 01 tdd 1
apps/api/src/lib/bootGuards.ts
apps/api/tests/lib/bootGuards.test.ts
apps/api/src/index.ts
apps/api/Dockerfile
true
IMG-01
truths artifacts key_links
A production image with DEV_AUTH_BYPASS=true refuses to boot (process exits non-zero) instead of silently no-op'ing
The production Docker stage bakes NODE_ENV=production so the devBypass hard guard is actually engaged in the shipped image
The boot guard is a unit-tested exported function, not inline startup logic
path provides exports
apps/api/src/lib/bootGuards.ts assertNotDevBypassInProduction() exported guard function
assertNotDevBypassInProduction
path provides
apps/api/tests/lib/bootGuards.test.ts Unit tests for the boot guard (3 cases)
path provides contains
apps/api/Dockerfile ENV NODE_ENV=production in the production stage ENV NODE_ENV=production
from to via pattern
apps/api/src/index.ts apps/api/src/lib/bootGuards.ts import + call as first statement in isMainModule() assertNotDevBypassInProduction()
Implement the dev/prod image-boundary runtime enforcement (D-07 + D-08). Bake `ENV NODE_ENV=production` into the production Dockerfile stage so the existing `devBypass.ts` hard guard is actually engaged in the shipped image, and add a boot-time refuse-to-boot guard `assertNotDevBypassInProduction()` that exits non-zero when `NODE_ENV==='production'` AND `DEV_AUTH_BYPASS==='true'`.

Purpose: Today the production image sets no NODE_ENV, so the devBypass.ts hard guard (NODE_ENV==='production' first) is only safe by accident (the second DEV_AUTH_BYPASS !== 'true' check passes when unset). An operator who accidentally sets DEV_AUTH_BYPASS=true in the production compose would silently bypass auth. D-07 engages the guard; D-08 turns a silent misconfig into a loud, immediate failure. The app holds real family credentials — this is the highest-leverage, lowest-cost hardening in the phase.

Output: A testable exported guard function (the primary Wave 0 test asset), its unit tests, the index.ts wiring, and the Dockerfile ENV line. This plan is consumed by 16-06 (publish.yml boot-smoke verifies the guard fires in the actual built image).

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/16-ci-dependency-audit-and-security-checks/16-CONTEXT.md @.planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md Task 1: RED — write failing unit tests for assertNotDevBypassInProduction() - apps/api/tests/lib/bootGuards.test.ts (file being created) - apps/api/tests/auth/devBypass.test.ts (EXACT analog: afterEach env-restoration pattern at lines 18-29, three-case structure, vitest imports) - apps/api/src/auth/devBypass.ts (the existing hard-guard idiom this mirrors — lines 58-76) - apps/api/tests/fixtures/ (note: tests live in tests/, NEVER in src/ — see memory) - Test 1: NODE_ENV='production' AND DEV_AUTH_BYPASS='true' → calls process.exit(1) (spy throws so the call is observable) - Test 2: NODE_ENV='development' AND DEV_AUTH_BYPASS='true' → does NOT call process.exit - Test 3: NODE_ENV='production' AND DEV_AUTH_BYPASS unset → does NOT call process.exit Create apps/api/tests/lib/bootGuards.test.ts. Import { describe, it, expect, vi, afterEach } from 'vitest' and { assertNotDevBypassInProduction } from '../../src/lib/bootGuards.js'. Capture originalNodeEnv and originalBypassFlag at describe scope; restore both in afterEach exactly like devBypass.test.ts (delete DEV_AUTH_BYPASS when originalBypassFlag is undefined). In each test set process.env.NODE_ENV and process.env.DEV_AUTH_BYPASS, install vi.spyOn(process,'exit').mockImplementation(() => { throw new Error('process.exit called'); }), and assert: Test 1 expects the call to throw 'process.exit called' and expect(exitSpy).toHaveBeenCalledWith(1); Tests 2 and 3 expect it NOT to throw and exitSpy NOT to have been called. Call exitSpy.mockRestore() at the end of each test. Run the suite to confirm it FAILS because src/lib/bootGuards.ts does not exist yet. Commit: `test(16-01): add failing tests for boot-time dev-bypass guard`. cd apps/api && pnpm test -- --run tests/lib/bootGuards.test.ts 2>&1 | grep -Eq 'Cannot find|Failed to load|No test files|failed|error' && echo RED-OK - tests/lib/bootGuards.test.ts exists with exactly 3 `it(...)` cases matching the behavior block - Running the suite fails (module under test does not yet exist) — RED state confirmed - afterEach restores NODE_ENV and DEV_AUTH_BYPASS (delete when originally undefined) The test file exists, encodes the 3 cases, and fails because src/lib/bootGuards.ts is absent. Task 2: GREEN — implement bootGuards.ts and wire it into index.ts - apps/api/src/lib/bootGuards.ts (file being created) - apps/api/src/auth/devBypass.ts (JSDoc + env-at-call-time pattern to mirror; lines 1-25, 58-76) - apps/api/src/index.ts (the isMainModule() block at lines 112-147 — guard call goes FIRST inside it; import block lines 1-18) - apps/api/src/lib/ (sibling utilities follow this dir's conventions — listAccess.ts, rank.ts) Create apps/api/src/lib/bootGuards.ts exporting `assertNotDevBypassInProduction(): void`. Logic: `if (process.env.NODE_ENV === 'production' && process.env.DEV_AUTH_BYPASS === 'true') { console.error('[FATAL] DEV_AUTH_BYPASS=true is set in a production environment. This configuration is forbidden. Refusing to start.'); process.exit(1); }`. Add a JSDoc block (mirror devBypass.ts style) stating it is D-08, exported for unit-testing without forking a process, and MUST be called as the FIRST statement inside the isMainModule() block. Then in apps/api/src/index.ts add `import { assertNotDevBypassInProduction } from './lib/bootGuards.js';` to the existing import block, and call `assertNotDevBypassInProduction();` as the FIRST statement inside `if (isMainModule()) {` — before the VAPID config (currently line 116), before startBrokerPoller/startOutboxWorker/startReminderScheduler, before serve(). Do NOT change the top-level `devBypassActive` computation (line 24). Commit: `feat(16-01): add boot-time refuse-to-boot guard for dev-bypass in production`. cd apps/api && pnpm test -- --run tests/lib/bootGuards.test.ts && pnpm typecheck - `assertNotDevBypassInProduction` is exported from src/lib/bootGuards.ts - `grep -n "assertNotDevBypassInProduction()" apps/api/src/index.ts` shows the call inside the isMainModule() block, before VAPID/worker/serve lines - `pnpm test -- --run tests/lib/bootGuards.test.ts` is GREEN (3/3) - `pnpm typecheck` (apps/api) passes — guard placement does not break the startup module The guard function exists, is imported and called first in isMainModule(), and all 3 unit tests pass with typecheck green. Task 3: Bake ENV NODE_ENV=production into the production Dockerfile stage - apps/api/Dockerfile (the production stage at lines 35-46; the gap D-07 fixes) - .planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md (Dockerfile section — exact placement: after WORKDIR /app/apps/api, before COPY --from=pwa-builder) In apps/api/Dockerfile, in the `FROM base AS production` stage, add `ENV NODE_ENV=production` after the `WORKDIR /app/apps/api` line (currently line 41) and before the `COPY --from=pwa-builder` line (currently line 45). Add a one-line comment above it referencing D-07 ("Enforce production identity — engages the NODE_ENV=production hard guard in devBypass.ts"). Do NOT add ENV to the `base`, `builder`, `dev`, or `pwa-builder` stages — only `production`. Leave the CMD line unchanged. awk '/FROM base AS production/{p=1} p&&/ENV NODE_ENV=production/{print "FOUND"; exit}' apps/api/Dockerfile | grep -q FOUND && echo OK - `ENV NODE_ENV=production` appears within the `production` stage (after `FROM base AS production`), not in any other stage - The line sits between `WORKDIR /app/apps/api` and `COPY --from=pwa-builder` - `grep -c "ENV NODE_ENV=production" apps/api/Dockerfile` returns exactly 1 The production stage bakes NODE_ENV=production; no other stage is affected.

<threat_model>

Trust Boundaries

Boundary Description
operator config → production container An operator-supplied env (docker-compose, Unraid) crosses into the running process; DEV_AUTH_BYPASS is attacker-equivalent if it slips into prod
Docker image build → shipped artifact The baked image environment (NODE_ENV) is the last line of defense before runtime

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-16-01 Elevation of Privilege Production container with DEV_AUTH_BYPASS=true (misconfigured compose) mitigate D-07: ENV NODE_ENV=production baked into the production stage engages the devBypass.ts hard guard so the bypass middleware can never inject DEV_USER in the shipped image (Task 3)
T-16-02 Elevation of Privilege Silent no-op of the dev-bypass guard hides the misconfiguration mitigate D-08: assertNotDevBypassInProduction() throws and process.exit(1) on NODE_ENV=production + DEV_AUTH_BYPASS=true, converting a silent bypass into an immediate crash (Tasks 1-2); verified in the built image by 16-06 boot-smoke
T-16-03 Tampering Guard placed too late in startup (DB/port opened before it fires) accept Guard is the FIRST statement in isMainModule(), before VAPID/workers/serve — no port or DB connection precedes it (Task 2 placement rule). Residual risk nil given enforced placement
</threat_model>
- `pnpm --filter @familysync/api test -- --run tests/lib/bootGuards.test.ts` → 3/3 green - `pnpm --filter @familysync/api typecheck` → green (startup module intact) - Dockerfile production stage contains exactly one `ENV NODE_ENV=production` - Full image-level proof (boot-smoke) is deferred to plan 16-06 against the built image

<success_criteria>

  • assertNotDevBypassInProduction() exists, is exported, imported in index.ts, and called first in isMainModule()
  • Unit tests cover all 3 env combinations and pass
  • The production Dockerfile stage bakes NODE_ENV=production
  • No change to dev/builder/pwa-builder stages or to the top-level devBypassActive logic </success_criteria>
Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-01-SUMMARY.md` when done.