docs(qa): add B16 (migrations, secrets & deploy) — close plan coverage gaps

Gap analysis of the codebase vs the plan surfaced surfaces with no QA home:
- DB migration system (idempotency/rollback/fresh==migrated, money conversions)
- encryption-key lifecycle (missing/rotated key → graceful degrade, no plaintext/leak)
- container deploy (docker-entrypoint: dir perms chmod 700, non-root, run migrations)
- update-check phone-home (external request → disclosed + opt-out)
- rate-limiter completeness (backupOperationLimiter, skipRateLimitIfNoUsers)

Added the B16 batch + playbook, and extended B0 recon to enumerate
middleware/workers/migrations/deploy so future cycles can't miss them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 09:53:37 -05:00
parent d82aa06652
commit 9876207781
1 changed files with 33 additions and 0 deletions

View File

@ -104,6 +104,7 @@ before cross-cutting; regression last). Update **Status** and **Findings** every
| B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | ✅ | 0 / 1 |
| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | ✅ | 0 / 4 |
| B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | ✅ | 0 / 0 |
| B16 | Migrations, secrets & deploy | migration idempotency/rollback/fresh==migrated, encryption-key lifecycle, `docker-entrypoint` (perms/first-run/migrate), update-check phone-home | scratch + docker | ⬜ | 0 / 0 |
> After B15, if any batch is 🔁 or has open S1/S2, loop back. Then start a new
> cycle from B0 against the next build/version.
@ -316,6 +317,8 @@ Run these, then compare the output to the batch playbooks (§7) and the [route m
- [ ] **API route mounts**`grep -nE "app.use\('/api" server.js` — every mounted route group is in B13's list and mapped in Appendix C.
- [ ] **Services & components**`ls services/` and `ls client/components/**/` — new service/component families have a home in a playbook.
- [ ] **UI primitives**`ls client/components/ui/` — every shared primitive is covered by the [B-UI](#b-ui--design-system-primitives) playbook; a new primitive gets a row there.
- [ ] **Middleware & workers**`ls middleware/ workers/` (+ `services/*Worker*`, `*Scheduler*`) — each is covered (csrf/rateLimiter/securityHeaders/requireAuth → B13; dailyWorker/bankSyncWorker/backupScheduler → B10).
- [ ] **Migrations & deploy** — new `db/database.js` migrations, `Dockerfile`/`docker-entrypoint.sh` changes, and `encryptionService`/`updateCheckService` behavior are covered by [B16](#b16--migrations-secrets--deployment).
- [ ] **Interactive-control census (makes "every button tested" *provable*)** — for each page, enumerate every button, link, toggle/switch, checkbox, select, text/number/date/file input, tab, menu, and filter control, and record it in a per-page control checklist (template: [Appendix E](#appendix-e--per-page-control-census)). A control that isn't on a checklist hasn't been tested — the census is the completeness guarantee the batch playbooks alone don't give you. Quick starting inventory: `grep -rnoE "type=[\"'][a-z]+[\"']" client/pages client/components` and `grep -rn "onClick=" client/pages/<Page>.jsx`.
- [ ] **Feature flags / conditional surfaces** — search for `Only`, `enabled`, `featureFlag`, env gates that hide/show pages; ensure each state is tested.
- [ ] **What changed since last cycle** — skim `git log`/`HISTORY.md` since the previous cycle's commit (see [Cycle Log](#11-qa-cycle-log)) for new features/pages.
@ -467,6 +470,36 @@ Run on the **production build** (`npm start`), not dev:
- [ ] Bogus URL → 404; logout → login redirect. Console clean throughout.
- [ ] Confirm [exit criteria](#appendix-b--exit--sign-off-criteria).
### B16 — Migrations, secrets & deployment
Added Cycle 1 (previously uncovered). These run on every boot / container start and
touch money columns and at-rest secrets — a bug here corrupts data or leaks/breaks
secrets silently.
**Migrations** (`db/database.js` migration system, `scripts/migrate-db.js`, `schema_migrations`, `rollbackMigration`)
- [ ] **Idempotent:** boot twice on the same DB → second run applies nothing ("Skipping already applied"), no errors, no duplicate rows/columns.
- [ ] **Fresh == migrated:** a brand-new DB (schema.sql + all migrations) has the same schema as a DB migrated up from an old version — same tables/columns/indexes, money columns are **integer cents**.
- [ ] **Rollback:** `rollbackMigration` on the latest migration reverts cleanly and re-applying works; partial/failed migration leaves the DB consistent (transactions per migration).
- [ ] **Money conversions correct:** v1.03 (dollars→cents) and v1.04 (template JSON) convert exact values, no ×100 drift, run once only.
- [ ] Migrating a large/real DB doesn't lose or duplicate bills/payments/categories.
**Encryption-key lifecycle** (`services/encryptionService.js`, `TOKEN_ENCRYPTION_KEY`, HKDF v1/v2)
- [ ] **Key present:** secrets (SMTP pw, OIDC secret, push tokens, login IP/UA) encrypt at rest and decrypt correctly.
- [ ] **Key missing:** app boots; secret features degrade gracefully (no crash); confirm secrets are **not** silently stored/served in plaintext.
- [ ] **Key rotated/wrong:** old ciphertext fails to decrypt **gracefully** (no crash, no stack leak); `safeDecrypt` fallback path is sane; re-encryption migrations (v0.770.79) behave.
- [ ] Encryption key is never committed, logged, or returned in any API response.
**Container / deploy** (`Dockerfile`, `docker-compose.yml`, `docker-entrypoint.sh`, `deploy.sh`)
- [ ] Image **builds**; container **starts**; app reachable; `/api/version` responds.
- [ ] Entrypoint: creates `DATA_DIR`/`DB_DIR`/`BACKUP_DIR`, sets **`chmod 700`** (not world-readable), `chown`s to the non-root `bill` user, runs migrations when `RUN_DB_MIGRATIONS=true`.
- [ ] Data **persists** across container restart (mounted volume); DB not re-created.
- [ ] Runs as **non-root**; secrets come from env, not baked into the image.
**Update check / phone-home** (`services/updateCheckService.js`)
- [ ] Confirm the external request to `REPO_API_URL` (default `dream.scheller.ltd`) is **disclosed** (privacy page) and **opt-out-able**; it must send no user data, only fetch the latest release; failure/offline degrades silently.
**Rate-limiter completeness** (`middleware/rateLimiter.js`) — beyond B13's list
- [ ] `backupOperationLimiter` throttles admin backup/restore/cleanup; `skipRateLimitIfNoUsers` only relaxes limits on a genuinely empty instance (first-run), never afterward.
---
## 8. Appendices