Files
familysync/.planning/phases/19-local-auth-no-oidc-mode/19-01-SUMMARY.md
T

8.8 KiB

phase, plan, subsystem, tags, status, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags status dependency_graph tech_stack key_files decisions metrics
19-local-auth-no-oidc-mode 01 auth
local-auth
scrypt
jwt
session-cookie
migration
boot-guard
docker-hygiene
checkpoint
requires provides affects
hashPassword/verifyPassword (node:crypto scrypt, PHC-encoded)
issueLocalSessionCookie/verifyLocalSessionCookie/clearLocalSessionCookie (Hono Jwt HS256)
assertLocalSessionSecretSet (boot guard)
local_credentials Drizzle table + 0003 migration
LOCAL_SESSION_SECRET in generate-secrets.mjs
apps/api/scripts/ .dockerignore exclusion (D-15)
apps/api/src/index.ts (boot guard wired)
apps/api/test/setup.ts (afterEach cleanup)
.dockerignore (D-15 image hygiene)
added patterns
PHC-style encoded scrypt hash (scrypt$N$r$p$salt_b64url$hash_b64url)
Stateless JWT session cookie via hono/utils/jwt Jwt.sign/Jwt.verify
Boot guard pattern (mirrors assertNotDevBypassInProduction)
TDD RED/GREEN
failing test committed before implementation
created modified
apps/api/src/auth/localCredentials.ts
apps/api/src/auth/localSession.ts
apps/api/src/db/migrations/0003_warm_deathstrike.sql
apps/api/tests/auth/localCredentials.test.ts
apps/api/tests/auth/localSession.test.ts
apps/api/src/lib/bootGuards.ts
apps/api/src/index.ts
apps/api/src/db/schema.ts
apps/api/test/setup.ts
scripts/generate-secrets.mjs
.dockerignore
Used node:crypto scryptSync (not async) — blocking but acceptable for 2-person household infrequent logins (D-08)
PHC-style encoding embeds N/r/p/salt in stored string — future parameter upgrades without DB migration
Jwt namespace import from hono/utils/jwt (Pitfall 8 — named sign/verify don't exist)
Cookie name: local-session (distinct from oidc-auth, Pitfall 4)
assertLocalSessionSecretSet exempts DEV_AUTH_BYPASS=true — bypass never issues local JWTs
Migration generated by drizzle-kit generate (never push) — purely additive CREATE TABLE
.dockerignore: excluded entire apps/api/scripts/ dir (supersedes per-file exclusion, D-15)
duration completed tasks_completed tasks_total files_created files_modified
~6 minutes 2026-06-17 3 4 5 6

Phase 19 Plan 01: Local Auth Foundation Summary

One-liner: Scrypt password primitives, stateless local-session JWT cookie helpers, local_credentials MariaDB table + additive migration, LOCAL_SESSION_SECRET boot guard wired in index.ts, and .dockerignore break-glass script exclusion.

Status: CHECKPOINT REACHED

Task 4 is a type="checkpoint:human-verify" (gate="blocking"). Tasks 1-3 are complete and committed. The plan pauses for human review of the generated migration SQL before proceeding.

Tasks Completed

Task Name Commit Key Files
1 (RED) hashPassword/verifyPassword tests 7ece966 apps/api/tests/auth/localCredentials.test.ts
1 (GREEN) hashPassword/verifyPassword implementation 85b01b5 apps/api/src/auth/localCredentials.ts
2 (RED) localSession + bootGuards tests 0d8f3fa apps/api/tests/auth/localSession.test.ts
2 (GREEN) localSession + bootGuards + index.ts 7d61148 apps/api/src/auth/localSession.ts, bootGuards.ts, index.ts
3 schema + migration + secrets + dockerignore 96f0991 schema.ts, 0003_warm_deathstrike.sql, generate-secrets.mjs, .dockerignore

Task 4: Checkpoint (Pending Human Review)

Checkpoint type: human-verify (blocking)

The migration 0003_warm_deathstrike.sql was generated by drizzle-kit generate and applied to the dev DB with pnpm --filter @familysync/api db:migrate (exit 0). It contains:

CREATE TABLE `local_credentials` (
	`id` int AUTO_INCREMENT NOT NULL,
	`user_id` int NOT NULL,
	`username` varchar(128) NOT NULL,
	`password_hash` varchar(256) NOT NULL,
	`created_at` timestamp NOT NULL DEFAULT (now()),
	`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
	CONSTRAINT `local_credentials_id` PRIMARY KEY(`id`),
	CONSTRAINT `uniq_local_cred_user` UNIQUE(`user_id`),
	CONSTRAINT `uniq_local_cred_username` UNIQUE(`username`)
);
--> statement-breakpoint
ALTER TABLE `local_credentials` ADD CONSTRAINT `local_credentials_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX `idx_local_credentials_user_id` ON `local_credentials` (`user_id`);

The SQL is purely additive. No ALTER/DROP/TRUNCATE/RENAME touches any existing table.

What the human needs to verify:

  1. Review apps/api/src/db/migrations/0003_warm_deathstrike.sql — confirm only CREATE TABLE local_credentials (no statements touching users, member_credentials, calendars, calendar_events, app_config, or any other existing table).
  2. Confirm LOCAL_SESSION_SECRET is set in your local .env (>=32 chars) — without it the API will refuse to boot in non-bypass mode. Add it via node scripts/generate-secrets.mjs if not present.

Resume signal: Type "approved" if migration is purely additive and LOCAL_SESSION_SECRET is set.

What Was Built

Task 1: hashPassword/verifyPassword (TDD)

apps/api/src/auth/localCredentials.ts exports:

  • hashPassword(password: string): string — scrypt + 16-byte random salt, returns PHC-encoded string
  • verifyPassword(storedEncoded: string, candidate: string): boolean — timingSafeEqual, never throws

Zero new npm dependencies. All 5 unit tests pass (round-trip, wrong-password, unique-salt, malformed-hash, PHC-shape).

Task 2: localSession.ts + boot guard (TDD)

apps/api/src/auth/localSession.ts exports:

  • issueLocalSessionCookie(c, userId) — signs JWT (HS256) with LOCAL_SESSION_SECRET, sets httpOnly cookie
  • verifyLocalSessionCookie(c) — returns userId or null (never throws, catches Jwt.verify expiry throws)
  • clearLocalSessionCookie(c) — deletes the cookie with matching attributes

apps/api/src/lib/bootGuards.ts adds:

  • assertLocalSessionSecretSet() — exits with FATAL if secret missing/<32 chars when not in bypass mode

apps/api/src/index.tsassertLocalSessionSecretSet() called immediately after assertNotDevBypassInProduction().

All 5 unit tests pass; pnpm --filter @familysync/api typecheck exits 0.

Task 3: Schema + Migration + Secrets + .dockerignore

  • apps/api/src/db/schema.tslocalCredentials table exported (UNIQUE user_id, UNIQUE username, FK->users cascade)
  • apps/api/src/db/migrations/0003_warm_deathstrike.sql — purely additive CREATE TABLE; applied to dev DB
  • apps/api/test/setup.tslocalCredentials added to afterEach cleanup (FK-safe ordering)
  • scripts/generate-secrets.mjs — emits LOCAL_SESSION_SECRET (base64 32-byte, 44 chars)
  • .dockerignore — added apps/api/scripts/ directory exclusion (D-15/IMG-02)

Deviations from Plan

Auto-fixed Issues

None. Plan executed as written.

Notes

  • The migration file generated by drizzle-kit is named 0003_warm_deathstrike.sql (drizzle-kit generates random animal names for migrations). The plan referenced 0003_local_credentials.sql as an expected name — this is not a semantic deviation, only a filename difference from drizzle-kit's naming convention. The content and purpose match exactly.
  • pnpm --filter @familysync/api db:migrate was run against the dev stack DB (credentials from .env). The worktree shares the main repo's dev DB connection, which is expected and safe for an additive migration.
  • Tests requiring MariaDB were run with CI=true to bypass the global-setup root-DB-provisioning step (which requires a root MySQL connection that isn't available from the worktree's network context). Pure unit tests (no DB access) work correctly in this mode.

Threat Surface Scan

No new network endpoints introduced in this plan. All new surface is internal stdlib / crypto utilities and a DB table migration. No changes to trust boundaries that aren't already covered by the plan's threat model (T-19-01 through T-19-04 and T-19-SC).

Known Stubs

None. This plan provides foundational utilities without UI or stub placeholders.

Self-Check: PASSED

All created files confirmed present on disk:

  • FOUND: apps/api/src/auth/localCredentials.ts
  • FOUND: apps/api/src/auth/localSession.ts
  • FOUND: apps/api/src/db/migrations/0003_warm_deathstrike.sql
  • FOUND: apps/api/tests/auth/localCredentials.test.ts
  • FOUND: apps/api/tests/auth/localSession.test.ts

All commits confirmed in git log:

  • 7ece966: test(19-01): add failing tests for hashPassword/verifyPassword
  • 85b01b5: feat(19-01): implement hashPassword/verifyPassword
  • 0d8f3fa: test(19-01): add failing tests for localSession
  • 7d61148: feat(19-01): implement localSession JWT cookie helpers
  • 96f0991: feat(19-01): schema + migration + secrets + dockerignore