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
@@ -124,10 +124,7 @@ function idempotentOperationsFromContract(): Set<string> {
|
||||
* Listed rather than filtered silently, so the exclusion stays visible in the
|
||||
* file that would otherwise claim full coverage.
|
||||
*/
|
||||
const UNPUBLISHED_PATHS: Readonly<Record<string, string>> = {
|
||||
"/api/admin/backups/{id}/restore":
|
||||
"Admin surface: cookie-only by construction, so arguably out of scope for a client contract.",
|
||||
};
|
||||
const UNPUBLISHED_PATHS: Readonly<Record<string, string>> = {};
|
||||
|
||||
describe("idempotency — the contract matches the wrapper", () => {
|
||||
it("finds idempotent operations in the source tree at all", () => {
|
||||
|
||||
@@ -217,14 +217,10 @@ const UNPUBLISHED: Readonly<Record<string, Exemption>> = {
|
||||
"/api/admin/backups": { kind: "adminConsole", methods: ["GET"] },
|
||||
"/api/admin/backups/run": { kind: "adminConsole", methods: ["POST"] },
|
||||
"/api/admin/backups/upload": { kind: "adminConsole", methods: ["POST"] },
|
||||
"/api/admin/backups/{id}/download": {
|
||||
kind: "adminConsole",
|
||||
methods: ["GET"],
|
||||
},
|
||||
"/api/admin/backups/{id}/restore": {
|
||||
kind: "adminConsole",
|
||||
methods: ["POST"],
|
||||
},
|
||||
// `/api/admin/backups/{id}/download` and `/api/admin/backups/{id}/restore`
|
||||
// are published rather than exempted: both open the encrypted stored copy,
|
||||
// and the refusal they answer when it will not open is a promise an operator
|
||||
// needs to be able to look up. See `src/lib/openapi/routes/admin-backups.ts`.
|
||||
"/api/admin/backups/{id}/summary": { kind: "adminConsole", methods: ["GET"] },
|
||||
"/api/admin/central-codex": {
|
||||
kind: "adminConsole",
|
||||
|
||||
Reference in new issue
Block a user