fix(image): ship the operator scripts the runbooks run in the container

The password-reset runbook tells an operator to run
`docker compose exec app node scripts/reset-password.mjs`, and the
intake-repair runbook runs `healthlog-tsx scripts/repair-intake-anomalies.ts`
inside the container. The runner stage copied nothing from scripts/, so
both failed with "Cannot find module", the reset at the moment an admin
is locked out.

The runner stage now copies both scripts and the Argon2 parameter module
the reset imports, and proves at build time that each reaches its own
first check with every import resolved. A structural test holds every
in-container command in docs/ and the Dockerfile copies in step.

Key rotation is documented the way it works on the published image: the
Rotate now button under Admin > Encryption is the primary path. The
rotation script imports the application graph and runs from a source
checkout of the same release; the runbook, the admin hint in all seven
languages and the decrypt error now say so.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 15:26:03 +02:00
1 parent 3c3e5ceab7
commit 13db7935f6
16 files changed
+203 -44

No files matched your search

+6 -5
View File
@@ -3,9 +3,10 @@
*
* v1.23 — admin-triggered encryption-key rotation. Enqueues the
* `encryption-key-rotate` pg-boss job, which re-encrypts the whole encrypted-
* column corpus to the configured ACTIVE key id. The CLI
* (`scripts/rotate-encryption-key.ts`) stays the canonical path; this is a
* convenience that reuses the same column registry and is safe by construction:
* column corpus to the configured ACTIVE key id. This is the primary path on
* the published image, which contains neither a package manager nor
* `scripts/rotate-encryption-key.ts`; the CLI runs from a source checkout and
* reuses the same column registry. Safe by construction:
*
* - ACTIVE-KEY-ONLY + NEVER ADDS / DROPS A KEY: the job only writes the active
* key and never touches `ENCRYPTION_KEYS`. Dropping a key remains an env +
@@ -16,8 +17,8 @@
* Auth: cookie-only `requireAdmin` (Bearer can never reach admin) PLUS
* `requireFreshMfa` step-up — re-encrypting the whole sensitive corpus is a
* high-blast-radius action. An admin without a second factor cannot use the
* button and falls back to the CLI (this is intentional: step-up cannot be
* softened, and the CLI is the documented canonical path).
* button and falls back to the CLI from a source checkout (intentional:
* step-up cannot be softened).
*/
import {
apiHandler,