Merge branch 'wave/v125-sec' into release/v1.25.0

This commit is contained in:
Marc-André Bombeck committed 2026-06-28 14:40:39 +02:00
commit 52e8dab3d7
27 files changed
+1111 -60

No files matched your search

@@ -0,0 +1,35 @@
-- v1.25 — encrypt medication free-text notes at rest.
--
-- Adds the AES-256-GCM ciphertext columns for the three medication
-- free-text fields that were still plaintext: the side-effect note
-- (`medication_side_effects.notes`), the dose-change titration note
-- (`medication_dose_changes.note`), and the inventory-item note
-- (`medication_inventory_items.notes`). These were the last plaintext
-- PHI columns left after the v1.23 note-encryption rollout.
--
-- STORAGE CONVENTION: Bytes (`bytea`), matching the dominant free-text
-- encrypted-note convention already in the schema (mood_entries.note_
-- encrypted, measurements.notes_encrypted, illness_day_logs.note_encrypted,
-- labs.note_encrypted, coach_*.{...}_encrypted) — the `encrypt()`
-- ciphertext string stored UTF-8 as `bytea`, the shape the rotation
-- script's `rotateBytesColumn` already covers. All three columns are
-- registered in ENCRYPTED_COLUMNS + the rotation script in the same change.
--
-- This migration is ADDITIVE ONLY. The legacy plaintext columns
-- (`medication_side_effects.notes`, `medication_dose_changes.note`,
-- `medication_inventory_items.notes`) are intentionally NOT dropped here:
-- an idempotent, transactional, fail-closed boot-time job encrypts the
-- existing rows (plaintext -> ciphertext, then nulls the plaintext) so the
-- read path can fall back to plaintext for not-yet-migrated rows. Dropping
-- the plaintext columns is a deliberate FOLLOW-UP release, once the backfill
-- reports zero remaining plaintext-but-no-ciphertext rows — the same
-- boundary the encryption-key-rotation playbook uses before dropping a
-- legacy key.
ALTER TABLE "medication_side_effects"
ADD COLUMN IF NOT EXISTS "notes_encrypted" BYTEA;
ALTER TABLE "medication_dose_changes"
ADD COLUMN IF NOT EXISTS "note_encrypted" BYTEA;
ALTER TABLE "medication_inventory_items"
ADD COLUMN IF NOT EXISTS "notes_encrypted" BYTEA;
+12
View File
@@ -2310,6 +2310,10 @@ model MedicationDoseChange {
/// User-entered titration note ("stepped up after 4 weeks on 5 mg,
/// tolerated 7.5 mg with mild nausea day 2").
note String?
/// v1.25 — AES-256-GCM ciphertext for `note`. The plaintext column
/// stays for read-fallback until the backfill reports zero remaining
/// rows; the plaintext drop is DEFERRED to a later release.
noteEncrypted Bytes? @map("note_encrypted")
createdAt DateTime @default(now()) @map("created_at")
medication Medication @relation(fields: [medicationId], references: [id], onDelete: Cascade)
@@ -2401,6 +2405,10 @@ model MedicationInventoryItem {
purchasedAt DateTime? @map("purchased_at")
/// Free-text note, application-layer max 200 chars.
notes String?
/// v1.25 — AES-256-GCM ciphertext for `notes`. The plaintext column
/// stays for read-fallback until the backfill reports zero remaining
/// rows; the plaintext drop is DEFERRED to a later release.
notesEncrypted Bytes? @map("notes_encrypted")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -2457,6 +2465,10 @@ model MedicationSideEffect {
/// application layer. The DB column is unbounded TEXT for
/// consistency with sibling "notes" columns on this schema.
notes String?
/// v1.25 — AES-256-GCM ciphertext for `notes`. The plaintext column
/// stays for read-fallback until the backfill reports zero remaining
/// rows; the plaintext drop is DEFERRED to a later release.
notesEncrypted Bytes? @map("notes_encrypted")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)