mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(backup): refuse a stored copy that will not open with 422
A `DataBackup` row that fails to decrypt answered 500 on the restore, the download and the preview. It is not a server fault: either the key that wrote the copy is no longer in `ENCRYPTION_KEYS` — a rotation that dropped the legacy entry before the script reported zero rows, the case the rotation runbook warns about — or the stored bytes are not the ones that were written. Every other bad-input arm on those routes already answers 4xx, and the 500 also paged the error reporter as if the process had broken. All three now answer 422 with `meta.errorCode = backup.payload.undecryptable` and one shared sentence, so the three routes cannot drift into three wordings for one condition. The download's second 500 stays: a payload this instance wrote and cannot parse really is a fault here. The two routes that open a stored copy move from the unpublished list into the contract, because the refusal is now a promise an operator can look up rather than whatever the handler happened to answer. The restore publishes its `Idempotency-Key` header with them.
This commit is contained in:
1 parent
6b366b4f27
commit
d3f087475c
11 files changed
+471
-27
No files matched your search
@@ -385,12 +385,16 @@ test.describe("Backup and restore, through the settings surfaces", () => {
|
||||
const response = await restoreThroughDialog(page, tamperedId);
|
||||
|
||||
// The envelope is AES-256-GCM and its tag covers every ciphertext byte, so
|
||||
// the flip is caught before the transaction opens. 500 with this sentence
|
||||
// is what the route answers on a payload it could not decrypt.
|
||||
expect(response.status()).toBe(500);
|
||||
// the flip is caught before the transaction opens. 422 and
|
||||
// `backup.payload.undecryptable` are the refusal the route documents —
|
||||
// `docs/api/openapi.yaml`, and `docs/ops/backup-restore.md` for the
|
||||
// operator reading it after a key rotation — rather than whatever the
|
||||
// handler happened to answer. The code is asserted and not the sentence:
|
||||
// the sentence is prose that may be reworded, the code is the contract.
|
||||
expect(response.status()).toBe(422);
|
||||
expect(await response.json()).toMatchObject({
|
||||
data: null,
|
||||
error: "Failed to decrypt backup payload",
|
||||
meta: { errorCode: "backup.payload.undecryptable" },
|
||||
});
|
||||
|
||||
// A refusal that still wrote something would be worse than no refusal.
|
||||
|
||||
Reference in new issue
Block a user