diff --git a/scripts/rotate-encryption-key.ts b/scripts/rotate-encryption-key.ts index 89681a2f7..acfbe3c2a 100644 --- a/scripts/rotate-encryption-key.ts +++ b/scripts/rotate-encryption-key.ts @@ -562,6 +562,17 @@ async function main() { ), ); + // ───── v1.38.0 dose free text (Bytes column) ───── + // The note on an administered dose — a reaction, a sore arm. Always + // encrypted on write, so every non-null row rotates. + results.push( + await rotateBytesColumn( + "VaccinationRecord", + "noteEncrypted", + prisma.vaccinationRecord, + ), + ); + // ───── Inbound clinical document (Bytes column, codec-dispatched) ───── // The raw uploaded document. Two layouts recorded per row in // `contentCodec` ("base64v1" string codec | "binary2" binary codec), so diff --git a/src/lib/crypto/encrypted-columns.ts b/src/lib/crypto/encrypted-columns.ts index 6caf22db1..99afe45c9 100644 --- a/src/lib/crypto/encrypted-columns.ts +++ b/src/lib/crypto/encrypted-columns.ts @@ -242,6 +242,13 @@ export const ENCRYPTED_COLUMNS: readonly EncryptedColumn[] = [ { model: "Encounter", field: "outcomeEncrypted", kind: "bytes" }, { model: "Practitioner", field: "noteEncrypted", kind: "bytes" }, + // ───── v1.38.0 dose free text (Bytes column) ───── + // What the person wrote about the dose — a reaction, a sore arm, why the + // schedule went the way it did. The Pass's own structured fields stay + // queryable plaintext (the batch code is a batch code, not a secret), so + // this is the one column on the record that rotates. + { model: "VaccinationRecord", field: "noteEncrypted", kind: "bytes" }, + // ───── Inbound clinical document (Bytes column, codec-dispatched) ───── // The raw uploaded document (the most sensitive blob in the corpus; never // logged, never in wide-event meta). Two layouts, recorded per row in diff --git a/src/lib/data-wipe/wipe-plan.ts b/src/lib/data-wipe/wipe-plan.ts index 98092efe2..04b1ac9f8 100644 --- a/src/lib/data-wipe/wipe-plan.ts +++ b/src/lib/data-wipe/wipe-plan.ts @@ -91,6 +91,15 @@ export const WIPE_MODELS = [ "EncounterConditionLink", "Encounter", + // ── Vaccinations ──────────────────────────────────────────────────────── + // Same ordering argument as the visit links: the document link cascades + // from the dose AND from the stored document, so it is deleted before + // either or its count reads zero on the audit row. The dose points at a + // practitioner and an encounter with SetNull, which is why it goes here, + // ahead of both. + "VaccinationDocumentLink", + "VaccinationRecord", + // ── Clinical record ───────────────────────────────────────────────────── "MentalHealthAssessment", "LabResult", diff --git a/src/lib/export/backup-plan.ts b/src/lib/export/backup-plan.ts index ac3e0cdc5..12c5414e0 100644 --- a/src/lib/export/backup-plan.ts +++ b/src/lib/export/backup-plan.ts @@ -142,6 +142,14 @@ export const BACKED_UP_MODELS = [ "EncounterLabLink", "EncounterConditionLink", + // ── Vaccinations ────────────────────────────────────────────────────────── + // An immunization history is the one part of a health record that has no + // other copy: the paper Pass is the original, and once it has been + // transcribed the transcription is what the person relies on. Nothing + // re-syncs it from a provider. + "VaccinationRecord", + "VaccinationDocumentLink", + // ── Coach ───────────────────────────────────────────────────────────────── "CoachConversation", "CoachMessage", @@ -262,6 +270,15 @@ export const TWO_ENDED_MODELS = [ "EncounterDocumentLink", "EncounterLabLink", "EncounterConditionLink", + // Doses travel both ways from the release that introduces them, and the + // link with them. `DocumentConditionLink` one list down says what the + // alternative costs — documents and conditions both restore, the filing + // between them does not. Repeating that here would return a restored + // Impfpass scan and a restored dose with nothing between them, which is the + // same regret against a record a person cannot reconstruct from anywhere + // else. + "VaccinationRecord", + "VaccinationDocumentLink", ] as const; /** One model claimed to travel both ways. */