From 579789ec386b02e5a724da388ee66eef359921bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 07:07:46 +0200 Subject: [PATCH] fix(api): declare the 400 and 415 the single-value profile writes answer The six /api/auth/me scalar writes (unit preference, glucose unit, automatic document reading, local lab OCR, workout routes, the central Codex switch) read their body through safeJson with a 1 KB cap, so each answers 415 for a body that is not JSON by content type and 400 for one that does not parse, beside the 413 they already declared. Their descriptions said so and the responses did not. All three now come from one block. The modules and disable-coach route tests had been skipping the contract check over a 400 and a 409 the registry already declares; the skips are gone. --- docs/api/openapi.yaml | 535 +++++++++--------- src/lib/openapi/routes/profile.ts | 53 +- .../routes/auth-me-disable-coach.test.ts | 3 - .../routes/auth-me-modules.test.ts | 23 +- .../routes/auth-me-unit-preference.test.ts | 6 - 5 files changed, 300 insertions(+), 320 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 8638f44b0..e6762c399 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -9631,14 +9631,26 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchUnitPreferenceEnvelope" + "400": &a21 + description: The request body is not parseable JSON. Nothing was written. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "413": + "413": &a22 description: Request body exceeds 1024 bytes. Nothing was written. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" + "415": &a23 + description: "The request did not carry `Content-Type: application/json`. Nothing was read and nothing was written." + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 /api/auth/me/glucose-unit: @@ -9689,14 +9701,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchGlucoseUnitEnvelope" + "400": *a21 "401": *a1 "403": *a9 - "413": - description: Request body exceeds 1024 bytes. Nothing was written. - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "413": *a22 + "415": *a23 "422": *a4 "429": *a5 /api/auth/me/documents-auto-ai-read: @@ -9752,14 +9761,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchDocumentsAutoAiReadEnvelope" + "400": *a21 "401": *a1 "403": *a9 - "413": - description: Request body exceeds 1024 bytes. Nothing was written. - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "413": *a22 + "415": *a23 "422": *a4 "429": *a5 /api/auth/me/injection-site-prefs: @@ -10061,14 +10067,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchLabsLocalOcrEnvelope" + "400": *a21 "401": *a1 "403": *a9 - "413": - description: Request body exceeds 1024 bytes. Nothing was written. - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "413": *a22 + "415": *a23 "422": *a4 "429": *a5 /api/auth/me/workout-routes: @@ -10109,14 +10112,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchWorkoutRoutesEnvelope" + "400": *a21 "401": *a1 "403": *a9 - "413": - description: Request body exceeds 1024 bytes. Nothing was written. - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "413": *a22 + "415": *a23 "422": *a4 "429": *a5 /api/auth/me/use-central-codex: @@ -10162,14 +10162,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchUseCentralCodexEnvelope" + "400": *a21 "401": *a1 "403": *a9 - "413": - description: Request body exceeds 1024 bytes. Nothing was written. - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "413": *a22 + "415": *a23 "422": *a4 "429": *a5 /api/auth/me/locale: @@ -10376,7 +10373,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 - "503": &a23 + "503": &a26 description: "The AI capability could not be resolved (its inputs could not be read) and the answer failed closed: `meta.errorCode` = `ai.unavailable`, `meta.reason` = `check_failed`. Retry later; no data depends on it." content: @@ -11592,7 +11589,7 @@ paths: $ref: "#/components/schemas/CorrectAnamnesisFactResponse" "401": *a1 "403": *a9 - "404": &a21 + "404": &a24 description: No current revision with this id exists for the authenticated account. content: application/json: @@ -11629,7 +11626,7 @@ paths: $ref: "#/components/schemas/RemoveAnamnesisFactResponse" "401": *a1 "403": *a9 - "404": *a21 + "404": *a24 "409": description: Anamnesis fact changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. @@ -12168,7 +12165,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteList" "401": *a1 - "403": &a22 + "403": &a25 description: The session is valid but its account is not an administrator. `meta.errorCode` = `auth.admin.required`. content: application/json: @@ -12209,7 +12206,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteCreated" "401": *a1 - "403": *a22 + "403": *a25 "422": *a4 "429": *a5 /api/admin/invites/{id}: @@ -12234,7 +12231,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteRevoked" "401": *a1 - "403": *a22 + "403": *a25 "422": *a4 "429": *a5 /api/insights/chat/messages/{id}/feedback: @@ -12290,7 +12287,7 @@ paths: schema: $ref: "#/components/schemas/AdminMeasurementMaintenanceStatusResponse" "401": *a1 - "403": *a22 + "403": *a25 "422": *a4 "429": *a5 post: @@ -12601,7 +12598,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "503": *a23 + "503": *a26 post: tags: - Insights @@ -14062,7 +14059,7 @@ paths: $ref: "#/components/schemas/MoodCustomTagPatchEnvelope" "401": *a1 "403": *a9 - "404": &a24 + "404": &a27 description: Not found / not owned by the caller. content: application/json: @@ -14098,7 +14095,7 @@ paths: $ref: "#/components/schemas/MoodCustomTagDeleteEnvelope" "401": *a1 "403": *a9 - "404": *a24 + "404": *a27 "422": *a4 "429": *a5 /api/mood/tags/{key}/hidden: @@ -14135,7 +14132,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "404": *a24 + "404": *a27 "422": *a4 "429": *a5 /api/mood/tags/groups: @@ -14190,7 +14187,7 @@ paths: $ref: "#/components/schemas/MoodTagGroupPatchEnvelope" "401": *a1 "403": *a9 - "404": *a24 + "404": *a27 "422": *a4 "429": *a5 delete: @@ -14222,7 +14219,7 @@ paths: $ref: "#/components/schemas/MoodTagGroupDeleteEnvelope" "401": *a1 "403": *a9 - "404": *a24 + "404": *a27 "422": *a4 "429": *a5 /api/mood/tags/layout: @@ -14390,7 +14387,7 @@ paths: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 "403": *a15 - "409": &a25 + "409": &a28 description: A mood entry with the same `(date, moodLoggedAt)` instant already exists. `meta.errorCode` = `mood.duplicate_timestamp`. Only a request without `externalId` can answer this — with one, the write upserts in place. @@ -14422,7 +14419,7 @@ paths: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 "403": *a15 - "404": *a24 + "404": *a27 "422": *a4 "429": *a5 put: @@ -14457,8 +14454,8 @@ paths: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 "403": *a15 - "404": *a24 - "409": *a25 + "404": *a27 + "409": *a28 "422": *a4 "429": *a5 delete: @@ -14484,7 +14481,7 @@ paths: $ref: "#/components/schemas/MoodEntryDeleteEnvelope" "401": *a1 "403": *a15 - "404": *a24 + "404": *a27 "422": *a4 "429": *a5 /api/mood-entries/restore: @@ -14635,7 +14632,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutNtfySettingsResponse" - "400": &a26 + "400": &a29 description: Body is not parseable JSON. content: application/json: @@ -14650,13 +14647,13 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "413": &a27 + "413": &a30 description: Body exceeds 64 KiB. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "415": &a28 + "415": &a31 description: Content-Type is not `application/json`. content: application/json: @@ -14717,7 +14714,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutTelegramSettingsResponse" - "400": *a26 + "400": *a29 "401": *a1 "403": description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the @@ -14733,8 +14730,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "413": *a27 - "415": *a28 + "413": *a30 + "415": *a31 "422": description: Body matched neither accepted shape; or `enabled` is true without both a bot token and a chat id; or the deployment's `APP_URL` is not a public HTTPS host on port 80/88/443/8443; or Telegram refused the webhook @@ -14789,11 +14786,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PutWebhookSettingsResponse" - "400": *a26 + "400": *a29 "401": *a1 "403": *a9 - "413": *a27 - "415": *a28 + "413": *a30 + "415": *a31 "422": description: Body matched neither accepted shape, or `enabled` is true while no webhook URL is stored; or the URL is on a private network the operator has not listed in `NOTIFICATION_PRIVATE_ORIGINS` (`meta.errorCode` = @@ -15458,7 +15455,7 @@ paths: anyOf: - $ref: "#/components/schemas/ConsentLatestForKindEnvelope" - $ref: "#/components/schemas/ConsentLatestByKindEnvelope" - "400": &a29 + "400": &a32 description: "`kind` was present but is not one of `ai_full`, `ai_insights_only`, `ai_coach`, `ai_extraction`. Multi-issue envelope; nothing was read or revoked. The consent family answers 400 here where the rest of the API answers 422." @@ -15519,7 +15516,7 @@ paths: anyOf: - $ref: "#/components/schemas/ConsentRevokeForKindEnvelope" - $ref: "#/components/schemas/ConsentRevokeAllEnvelope" - "400": *a29 + "400": *a32 "401": *a1 "403": *a9 "422": *a4 @@ -16312,7 +16309,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderEnvelope" "401": *a1 "403": *a15 - "404": &a30 + "404": &a33 description: Measurement reminder not found / not owned. content: application/json: @@ -16350,7 +16347,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderPatchEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 delete: @@ -16373,7 +16370,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderDeleteEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/satisfy: @@ -16398,7 +16395,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderSatisfyEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/complete: @@ -16426,7 +16423,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderCompleteEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/skip: @@ -16456,7 +16453,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderSkipEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/snooze: @@ -16490,7 +16487,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderSnoozeEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/history: @@ -16531,7 +16528,7 @@ paths: $ref: "#/components/schemas/MeasurementReminderHistoryEnvelope" "401": *a1 "403": *a15 - "404": *a30 + "404": *a33 "422": *a4 "429": *a5 /api/labs: @@ -16693,7 +16690,7 @@ paths: $ref: "#/components/schemas/GetLabResultResponse" "401": *a1 "403": *a15 - "404": &a31 + "404": &a34 description: Lab result not found (or owned by another user). content: application/json: @@ -16728,7 +16725,7 @@ paths: $ref: "#/components/schemas/UpdateLabResultResponse" "401": *a1 "403": *a15 - "404": *a31 + "404": *a34 "422": *a4 "429": *a5 delete: @@ -16751,7 +16748,7 @@ paths: $ref: "#/components/schemas/DeleteLabResultResponse" "401": *a1 "403": *a15 - "404": *a31 + "404": *a34 "422": *a4 "429": *a5 /api/labs/restore: @@ -16839,7 +16836,7 @@ paths: schema: $ref: "#/components/schemas/OcrTextExtractRequest" responses: - "202": &a41 + "202": &a44 description: Queued for the background worker. Every refusal the route can answer quickly (capability, provider, rate limit, budget, validation) has already been answered; anything that goes wrong from here is reported on the run. @@ -17020,7 +17017,7 @@ paths: $ref: "#/components/schemas/GetBiomarkerResponse" "401": *a1 "403": *a15 - "404": &a32 + "404": &a35 description: "Biomarker not found (or owned by another user). `meta.errorCode` = `labs.biomarker.notFound`. Since v1.43 a marker that was merged into another answers with `meta.mergedInto` naming the marker it went into: drop the cached marker and open that one." @@ -17062,7 +17059,7 @@ paths: $ref: "#/components/schemas/UpdateBiomarkerResponse" "401": *a1 "403": *a15 - "404": *a32 + "404": *a35 "409": description: "`labs.biomarker.duplicate`: a rename collides with another of the caller's markers. `labs.biomarker.alias.taken` (v1.43): one of `aliases` is already the name or an alias of another marker; @@ -17096,7 +17093,7 @@ paths: $ref: "#/components/schemas/DeleteBiomarkerResponse" "401": *a1 "403": *a15 - "404": *a32 + "404": *a35 "422": *a4 "429": *a5 /api/biomarkers/{id}/merge: @@ -17134,7 +17131,7 @@ paths: $ref: "#/components/schemas/MergeBiomarkersResponse" "401": *a1 "403": *a15 - "404": *a32 + "404": *a35 "409": *a7 "422": description: "`labs.biomarker.merge.invalid`: a malformed body, or the target among the sources (Zod issues in @@ -17359,7 +17356,7 @@ paths: $ref: "#/components/schemas/GetEncounterEnvelope" "401": *a1 "403": *a15 - "404": &a33 + "404": &a36 description: Visit not found (or owned by another user). content: application/json: @@ -17393,7 +17390,7 @@ paths: $ref: "#/components/schemas/UpdateEncounterEnvelope" "401": *a1 "403": *a15 - "404": *a33 + "404": *a36 "422": *a4 "429": *a5 delete: @@ -17418,7 +17415,7 @@ paths: $ref: "#/components/schemas/DeleteEncounterEnvelope" "401": *a1 "403": *a15 - "404": *a33 + "404": *a36 "422": *a4 "429": *a5 /api/encounters/{id}/restore: @@ -17444,7 +17441,7 @@ paths: $ref: "#/components/schemas/RestoreEncounterEnvelope" "401": *a1 "403": *a15 - "404": *a33 + "404": *a36 "422": *a4 "429": *a5 /api/encounters/{id}/links: @@ -17476,7 +17473,7 @@ paths: $ref: "#/components/schemas/LinkEncounterEnvelope" "401": *a1 "403": *a9 - "404": *a33 + "404": *a36 "422": *a4 "429": *a5 delete: @@ -17506,7 +17503,7 @@ paths: $ref: "#/components/schemas/UnlinkEncounterEnvelope" "401": *a1 "403": *a9 - "404": *a33 + "404": *a36 "422": *a4 "429": *a5 /api/practitioners: @@ -17595,7 +17592,7 @@ paths: $ref: "#/components/schemas/GetPractitionerEnvelope" "401": *a1 "403": *a15 - "404": &a34 + "404": &a37 description: Practitioner not found (or owned by another user). content: application/json: @@ -17629,7 +17626,7 @@ paths: $ref: "#/components/schemas/UpdatePractitionerEnvelope" "401": *a1 "403": *a15 - "404": *a34 + "404": *a37 "422": *a4 "429": *a5 delete: @@ -17654,7 +17651,7 @@ paths: $ref: "#/components/schemas/DeletePractitionerEnvelope" "401": *a1 "403": *a15 - "404": *a34 + "404": *a37 "422": *a4 "429": *a5 /api/practitioners/{id}/restore: @@ -17680,7 +17677,7 @@ paths: $ref: "#/components/schemas/RestorePractitionerEnvelope" "401": *a1 "403": *a15 - "404": *a34 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/custom: @@ -17724,7 +17721,7 @@ paths: $ref: "#/components/schemas/CreateCustomVaccineEnvelope" "401": *a1 "403": *a15 - "409": &a35 + "409": &a38 description: A live definition on the record already carries this name, compared without case (`vaccination.custom.name-taken`). content: @@ -17761,13 +17758,13 @@ paths: $ref: "#/components/schemas/UpdateCustomVaccineEnvelope" "401": *a1 "403": *a15 - "404": &a36 + "404": &a39 description: No live definition with this id on the record (`vaccination.custom.not-found`). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": *a35 + "409": *a38 "422": *a4 "429": *a5 delete: @@ -17792,7 +17789,7 @@ paths: $ref: "#/components/schemas/DeleteCustomVaccineEnvelope" "401": *a1 "403": *a15 - "404": *a36 + "404": *a39 "422": *a4 "429": *a5 /api/vaccinations: @@ -17904,7 +17901,7 @@ paths: $ref: "#/components/schemas/GetVaccinationEnvelope" "401": *a1 "403": *a15 - "404": &a37 + "404": &a40 description: Vaccination not found (or owned by another user). content: application/json: @@ -17939,7 +17936,7 @@ paths: $ref: "#/components/schemas/UpdateVaccinationEnvelope" "401": *a1 "403": *a15 - "404": *a37 + "404": *a40 "422": *a4 "429": *a5 delete: @@ -17964,7 +17961,7 @@ paths: $ref: "#/components/schemas/DeleteVaccinationEnvelope" "401": *a1 "403": *a15 - "404": *a37 + "404": *a40 "422": *a4 "429": *a5 /api/vaccinations/{id}/restore: @@ -17989,7 +17986,7 @@ paths: $ref: "#/components/schemas/RestoreVaccinationEnvelope" "401": *a1 "403": *a15 - "404": *a37 + "404": *a40 "422": *a4 "429": *a5 /api/vaccinations/{id}/booster: @@ -18057,7 +18054,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "404": *a37 + "404": *a40 "422": *a4 "429": *a5 /api/vaccinations/suggest: @@ -18118,7 +18115,7 @@ paths: $ref: "#/components/schemas/LinkVaccinationEnvelope" "401": *a1 "403": *a9 - "404": *a37 + "404": *a40 "422": *a4 "429": *a5 delete: @@ -18148,7 +18145,7 @@ paths: $ref: "#/components/schemas/UnlinkVaccinationEnvelope" "401": *a1 "403": *a9 - "404": *a37 + "404": *a40 "422": *a4 "429": *a5 /api/illness/episodes: @@ -18263,7 +18260,7 @@ paths: $ref: "#/components/schemas/GetIllnessEpisodeEnvelope" "401": *a1 "403": *a15 - "404": &a38 + "404": &a41 description: Illness episode not found (or owned by another user). content: application/json: @@ -18298,7 +18295,7 @@ paths: $ref: "#/components/schemas/UpdateIllnessEpisodeEnvelope" "401": *a1 "403": *a15 - "404": *a38 + "404": *a41 "422": *a4 "429": *a5 delete: @@ -18322,7 +18319,7 @@ paths: $ref: "#/components/schemas/DeleteIllnessEpisodeEnvelope" "401": *a1 "403": *a15 - "404": *a38 + "404": *a41 "422": *a4 "429": *a5 /api/illness/episodes/{id}/restore: @@ -18350,7 +18347,7 @@ paths: $ref: "#/components/schemas/RestoreIllnessEpisodeEnvelope" "401": *a1 "403": *a15 - "404": *a38 + "404": *a41 "409": *a7 "422": *a4 "429": *a5 @@ -18383,7 +18380,7 @@ paths: $ref: "#/components/schemas/ResolveIllnessEpisodeEnvelope" "401": *a1 "403": *a15 - "404": *a38 + "404": *a41 "422": *a4 "429": *a5 /api/illness/episodes/{id}/day-logs: @@ -18434,7 +18431,7 @@ paths: $ref: "#/components/schemas/GetIllnessDayLogEnvelope" "401": *a1 "403": *a15 - "404": *a38 + "404": *a41 "422": *a4 "429": *a5 post: @@ -18471,7 +18468,7 @@ paths: $ref: "#/components/schemas/UpsertIllnessDayLogCreatedEnvelope" "401": *a1 "403": *a15 - "404": *a38 + "404": *a41 "422": *a4 "429": *a5 /api/illness/episodes/{id}/correlation: @@ -18499,7 +18496,7 @@ paths: $ref: "#/components/schemas/IllnessCorrelationEnvelope" "401": *a1 "403": *a9 - "404": *a38 + "404": *a41 "422": *a4 "429": *a5 /api/illness/insights: @@ -18716,7 +18713,7 @@ paths: $ref: "#/components/schemas/GetAllergyEnvelope" "401": *a1 "403": *a15 - "404": &a39 + "404": &a42 description: Allergy not found (or owned by another user). content: application/json: @@ -18750,7 +18747,7 @@ paths: $ref: "#/components/schemas/UpdateAllergyEnvelope" "401": *a1 "403": *a15 - "404": *a39 + "404": *a42 "422": *a4 "429": *a5 delete: @@ -18774,7 +18771,7 @@ paths: $ref: "#/components/schemas/DeleteAllergyEnvelope" "401": *a1 "403": *a15 - "404": *a39 + "404": *a42 "422": *a4 "429": *a5 /api/family-history: @@ -18848,7 +18845,7 @@ paths: $ref: "#/components/schemas/GetFamilyHistoryEnvelope" "401": *a1 "403": *a15 - "404": &a40 + "404": &a43 description: Family history entry not found (or owned by another user). content: application/json: @@ -18882,7 +18879,7 @@ paths: $ref: "#/components/schemas/UpdateFamilyHistoryEnvelope" "401": *a1 "403": *a15 - "404": *a40 + "404": *a43 "422": *a4 "429": *a5 delete: @@ -18906,7 +18903,7 @@ paths: $ref: "#/components/schemas/DeleteFamilyHistoryEnvelope" "401": *a1 "403": *a15 - "404": *a40 + "404": *a43 "422": *a4 "429": *a5 /api/documents/inbound: @@ -19579,7 +19576,7 @@ paths: required: true schema: type: string - - &a42 + - &a45 name: Prefer in: header required: false @@ -19604,9 +19601,9 @@ paths: application/json: schema: $ref: "#/components/schemas/InboundExtractEnvelope" - "202": *a41 + "202": *a44 "401": *a1 - "403": &a43 + "403": &a46 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: AI refusal: the `documentAi` capability is unavailable for this record. The envelope carries `meta.errorCode`, `meta.capability` and @@ -19632,7 +19629,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 - "503": &a44 + "503": &a47 description: "`ai.unavailable` — the inputs behind the AI capability could not be read, and the answer failed closed. Nothing was sent. Retry later." content: @@ -19662,7 +19659,7 @@ paths: required: true schema: type: string - - *a42 + - *a45 requestBody: required: false content: @@ -19698,12 +19695,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentSuggestEnvelope" - "202": *a41 + "202": *a44 "401": *a1 - "403": *a43 + "403": *a46 "422": *a4 "429": *a5 - "503": *a44 + "503": *a47 /api/documents/inbound/{id}/summary: post: tags: @@ -19753,7 +19750,7 @@ paths: type: boolean default: false description: With `persist=true`, explicitly replace an existing stored summary. - - *a42 + - *a45 requestBody: required: false content: @@ -19789,12 +19786,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentSummaryEnvelope" - "202": *a41 + "202": *a44 "401": *a1 - "403": *a43 + "403": *a46 "422": *a4 "429": *a5 - "503": *a44 + "503": *a47 /api/documents/inbound/{id}/index: post: tags: @@ -19820,7 +19817,7 @@ paths: required: true schema: type: string - - *a42 + - *a45 requestBody: required: false content: @@ -19856,12 +19853,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentIndexEnvelope" - "202": *a41 + "202": *a44 "401": *a1 - "403": *a43 + "403": *a46 "422": *a4 "429": *a5 - "503": *a44 + "503": *a47 /api/documents/inbound/{id}/chat: post: tags: @@ -19901,10 +19898,10 @@ paths: type: string description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes." "401": *a1 - "403": *a43 + "403": *a46 "422": *a4 "429": *a5 - "503": *a44 + "503": *a47 get: tags: - Documents @@ -19968,10 +19965,10 @@ paths: schema: $ref: "#/components/schemas/DocumentReindexEnvelope" "401": *a1 - "403": *a43 + "403": *a46 "422": *a4 "429": *a5 - "503": *a44 + "503": *a47 /api/documents/inbound/{id}/facts/{factId}: patch: tags: @@ -20081,7 +20078,7 @@ paths: $ref: "#/components/schemas/CreateCustomMetricResponse" "401": *a1 "403": *a9 - "409": &a46 + "409": &a49 description: A custom metric with this name already exists for the caller. content: application/json: @@ -20110,7 +20107,7 @@ paths: $ref: "#/components/schemas/GetCustomMetricResponse" "401": *a1 "403": *a15 - "404": &a45 + "404": &a48 description: Custom metric not found (or owned by another user). content: application/json: @@ -20145,8 +20142,8 @@ paths: $ref: "#/components/schemas/UpdateCustomMetricResponse" "401": *a1 "403": *a9 - "404": *a45 - "409": *a46 + "404": *a48 + "409": *a49 "422": *a4 "429": *a5 delete: @@ -20170,7 +20167,7 @@ paths: $ref: "#/components/schemas/DeleteCustomMetricResponse" "401": *a1 "403": *a9 - "404": *a45 + "404": *a48 "422": *a4 "429": *a5 /api/custom-metrics/{id}/entries: @@ -20214,7 +20211,7 @@ paths: $ref: "#/components/schemas/ListCustomMetricEntriesEnvelope" "401": *a1 "403": *a15 - "404": *a45 + "404": *a48 "422": *a4 "429": *a5 post: @@ -20244,7 +20241,7 @@ paths: $ref: "#/components/schemas/CreateCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": *a45 + "404": *a48 "409": *a7 "422": *a4 "429": *a16 @@ -20281,7 +20278,7 @@ paths: $ref: "#/components/schemas/UpdateCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": &a47 + "404": &a50 description: Custom metric entry not found (or owned by another user). content: application/json: @@ -20315,7 +20312,7 @@ paths: $ref: "#/components/schemas/DeleteCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": *a47 + "404": *a50 "422": *a4 "429": *a5 /api/custom-metrics/{id}/entries/restore: @@ -20623,7 +20620,7 @@ paths: schema: $ref: "#/components/schemas/NutrientCatalogEnvelope" "401": *a1 - "403": &a48 + "403": &a51 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: The opt-in `nutrients` module is @@ -20772,7 +20769,7 @@ paths: schema: $ref: "#/components/schemas/NutrientOverviewEnvelope" "401": *a1 - "403": *a48 + "403": *a51 "422": *a4 "429": *a5 /api/nutrients/daily: @@ -20809,7 +20806,7 @@ paths: schema: $ref: "#/components/schemas/NutrientDailySeriesEnvelope" "401": *a1 - "403": *a48 + "403": *a51 "422": *a4 "429": *a5 /api/nutrients/water: @@ -20838,7 +20835,7 @@ paths: schema: $ref: "#/components/schemas/NutrientWaterWriteEnvelope" "401": *a1 - "403": *a48 + "403": *a51 "409": *a7 "422": *a4 "429": *a5 @@ -21359,7 +21356,7 @@ paths: application/json: schema: $ref: "#/components/schemas/WithingsCredentialsUpdateEnvelope" - "400": &a49 + "400": &a52 description: Body is not parseable JSON. content: application/json: @@ -21367,19 +21364,19 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "413": &a50 + "413": &a53 description: Body exceeds 64 KiB. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "415": &a51 + "415": &a54 description: Content-Type is not `application/json`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": &a52 + "422": &a55 description: "`clientId` and `clientSecret` are both required and each must be 1–200 characters. The envelope carries a single fixed message, not per-field issues." content: @@ -21570,12 +21567,12 @@ paths: application/json: schema: $ref: "#/components/schemas/FitbitCredentialsUpdateEnvelope" - "400": *a49 + "400": *a52 "401": *a1 "403": *a9 - "413": *a50 - "415": *a51 - "422": *a52 + "413": *a53 + "415": *a54 + "422": *a55 "429": *a5 delete: tags: @@ -21730,12 +21727,12 @@ paths: application/json: schema: $ref: "#/components/schemas/GoogleHealthCredentialsUpdateEnvelope" - "400": *a49 + "400": *a52 "401": *a1 "403": *a9 - "413": *a50 - "415": *a51 - "422": *a52 + "413": *a53 + "415": *a54 + "422": *a55 "429": *a5 delete: tags: @@ -21901,12 +21898,12 @@ paths: application/json: schema: $ref: "#/components/schemas/OuraCredentialsUpdateEnvelope" - "400": *a49 + "400": *a52 "401": *a1 "403": *a9 - "413": *a50 - "415": *a51 - "422": *a52 + "413": *a53 + "415": *a54 + "422": *a55 "429": *a5 delete: tags: @@ -21986,21 +21983,21 @@ paths: $ref: "#/components/schemas/OuraTestEnvelope" "401": *a1 "403": *a9 - "422": &a53 + "422": &a56 description: No usable connection for this provider — nothing stored, or the stored grant could no longer be resolved into an access token. `meta.errorCode` = `not_configured`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": &a54 + "429": &a57 description: More than 5 requests in 60 s from this user. `meta.errorCode` = `rate_limited_self` — this is the local limiter, distinct from a `rate_limited` 502 which is the provider's. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "502": &a55 + "502": &a58 description: "The probe did not complete. `meta.errorCode` names the class: `credentials_rejected` (the provider answered 401/403 — the stored grant is gone), `rate_limited` (the PROVIDER answered 429, which is not this endpoint's own limiter), `upstream_error` (5xx), `timeout` (no answer within 5 s), `connection_failed` @@ -22046,12 +22043,12 @@ paths: application/json: schema: $ref: "#/components/schemas/PolarCredentialsUpdateEnvelope" - "400": *a49 + "400": *a52 "401": *a1 "403": *a9 - "413": *a50 - "415": *a51 - "422": *a52 + "413": *a53 + "415": *a54 + "422": *a55 "429": *a5 delete: tags: @@ -22131,9 +22128,9 @@ paths: $ref: "#/components/schemas/PolarTestEnvelope" "401": *a1 "403": *a9 - "422": *a53 - "429": *a54 - "502": *a55 + "422": *a56 + "429": *a57 + "502": *a58 /api/strava/credentials: get: tags: @@ -22170,12 +22167,12 @@ paths: application/json: schema: $ref: "#/components/schemas/StravaCredentialsUpdateEnvelope" - "400": *a49 + "400": *a52 "401": *a1 "403": *a9 - "413": *a50 - "415": *a51 - "422": *a52 + "413": *a53 + "415": *a54 + "422": *a55 "429": *a5 delete: tags: @@ -22257,9 +22254,9 @@ paths: $ref: "#/components/schemas/StravaTestEnvelope" "401": *a1 "403": *a9 - "422": *a53 - "429": *a54 - "502": *a55 + "422": *a56 + "429": *a57 + "502": *a58 /api/integrations/status: get: tags: @@ -22297,7 +22294,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a54 + "429": *a57 /api/integrations/fitbit/test: post: tags: @@ -22323,9 +22320,9 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a53 - "429": *a54 - "502": *a55 + "422": *a56 + "429": *a57 + "502": *a58 /api/integrations/google-health/resume: post: tags: @@ -22343,7 +22340,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a54 + "429": *a57 /api/integrations/google-health/test: post: tags: @@ -22377,9 +22374,9 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a53 - "429": *a54 - "502": *a55 + "422": *a56 + "429": *a57 + "502": *a58 /api/integrations/whoop/resume: post: tags: @@ -22397,7 +22394,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a54 + "429": *a57 /api/integrations/whoop/test: post: tags: @@ -22415,9 +22412,9 @@ paths: $ref: "#/components/schemas/WhoopIntegrationTestEnvelope" "401": *a1 "403": *a9 - "422": *a53 - "429": *a54 - "502": *a55 + "422": *a56 + "429": *a57 + "502": *a58 /api/integrations/withings/resume: post: tags: @@ -22436,7 +22433,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a54 + "429": *a57 /api/integrations/withings/test: post: tags: @@ -22455,8 +22452,8 @@ paths: $ref: "#/components/schemas/WithingsIntegrationTestEnvelope" "401": *a1 "403": *a9 - "422": *a53 - "429": *a54 + "422": *a56 + "429": *a57 "502": description: "The probe did not complete. The shared codes apply (`credentials_rejected`, `rate_limited`, `upstream_error`, `timeout`, `connection_failed`) and this path adds one of its own: @@ -22874,7 +22871,7 @@ paths: schema: $ref: "#/components/schemas/RecordActivityEnvelope" "401": *a1 - "403": &a56 + "403": &a59 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Refused. `meta.errorCode` is `sharing.not_permitted` when the request was made while acting on another account (grant management is never @@ -22901,7 +22898,7 @@ paths: schema: $ref: "#/components/schemas/AccountGrantListEnvelope" "401": *a1 - "403": *a56 + "403": *a59 "422": *a4 "429": *a5 post: @@ -23005,7 +23002,7 @@ paths: schema: $ref: "#/components/schemas/AcceptedAccountGrantEnvelope" "401": *a1 - "403": *a56 + "403": *a59 "404": description: No such invitation for this account. content: @@ -23049,7 +23046,7 @@ paths: schema: $ref: "#/components/schemas/RevokedAccountGrantEnvelope" "401": *a1 - "403": *a56 + "403": *a59 "404": description: No such grant on this account's record. content: @@ -23090,7 +23087,7 @@ paths: schema: $ref: "#/components/schemas/RenouncedAccountGrantEnvelope" "401": *a1 - "403": *a56 + "403": *a59 "404": description: No such grant held by this account. content: @@ -23482,7 +23479,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a56 + "403": *a59 "409": description: "The record this browser session is on is not the record the request asserted. `meta.errorCode` is `sharing.session.changed`. It is not a refusal of access — the caller may well be entitled to both records — @@ -23871,7 +23868,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectionListEnvelope" "401": *a1 - "403": &a57 + "403": &a60 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: The operator has disabled the API instance-wide. Every route in this module refuses while that setting is off, for a cookie session as well as @@ -23905,7 +23902,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectionRevokeEnvelope" "401": *a1 - "403": *a57 + "403": *a60 "404": description: No live connection with this id for this caller — also the answer for an already-revoked one, so a repeat revoke is 404 rather than an idempotent 200. @@ -23932,7 +23929,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectorTokenListEnvelope" "401": *a1 - "403": *a57 + "403": *a60 "422": *a4 "429": *a5 post: @@ -23965,7 +23962,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a57 + "403": *a60 "413": description: Body exceeds 16 KiB. content: @@ -24008,7 +24005,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectorTokenRevokeEnvelope" "401": *a1 - "403": *a57 + "403": *a60 "404": description: No `health:read` token with this id for this caller. An already-revoked token is still found, so a repeat revoke answers 200. @@ -24070,7 +24067,7 @@ paths: record (audited). Fitbit is not offered: its classic API is retired. Cookie session, while acting on a managed record the caller guards." parameters: - - &a58 + - &a61 name: provider in: path required: true @@ -24117,7 +24114,7 @@ paths: Audited against the record with the Guardian as the actor. Cookie session or Bearer, while acting on a managed record the caller guards." parameters: - - *a58 + - *a61 responses: "200": description: The provider is disconnected. @@ -24143,7 +24140,7 @@ paths: description: Returns the named family's resolved settings for the record the session is acting on. Cookie session or Bearer, while acting on a managed record the caller guards. parameters: - - &a59 + - &a62 name: family in: path required: true @@ -24187,7 +24184,7 @@ paths: ride the audit row. Body capped at 64 KiB. Cookie session or Bearer, while acting on a managed record the caller guards. parameters: - - *a59 + - *a62 requestBody: required: true content: @@ -24814,13 +24811,13 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupDocument" - "403": &a60 + "403": &a63 description: Caller is not an admin. `requireAdmin()` is cookie-only, so a Bearer token lands here whatever its scope. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "404": &a61 + "404": &a64 description: No stored copy with this id. content: application/json: @@ -24894,8 +24891,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "403": *a60 - "404": *a61 + "403": *a63 + "404": *a64 "409": description: A restore of the same account is already queued or running (`meta.errorCode` = `backup.restore.active`, `meta.jobId` names it), the stored copy was replaced by a newer one while it was being checked @@ -24939,7 +24936,7 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupRestoreJobListResponse" - "403": *a60 + "403": *a63 /api/admin/backups/restores/{jobId}: get: tags: @@ -24962,7 +24959,7 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupRestoreJobResponse" - "403": *a60 + "403": *a63 "404": description: No restore job with this id. content: @@ -24981,7 +24978,7 @@ paths: request leaves the server. Needs neither the Documents module nor the list: with the list unset, `available` is false and any connection still stored is listed (with `originAllowed: false`) so it can be removed. Cookie session only; not delegable." - security: &a62 + security: &a65 - cookieAuth: [] responses: "200": @@ -24991,7 +24988,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourcesStatusEnvelope" "401": *a1 - "403": &a63 + "403": &a66 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Refused before any request to the source: `documents.sources.browserOnly` for a Bearer caller, `sharing.not_permitted` while acting on another @@ -25016,9 +25013,9 @@ paths: `documents.sources.tokenRequired`), so a stored token is never sent to an address it was not saved for. A Papra organization id is letters, digits, `_` and `-`. The token is stored encrypted (AES-256-GCM) and never returned. Ten saves and tests a minute per person. Cookie session only; not delegable." - security: *a62 + security: *a65 parameters: - - &a64 + - &a67 name: system in: path required: true @@ -25057,8 +25054,8 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceConnectionEnvelope" "401": *a1 - "403": *a63 - "404": &a65 + "403": *a66 + "404": &a68 description: "`documents.sources.unavailable`: the operator has not listed any origin in `DOCUMENT_SOURCE_ORIGINS`, so the picker is off. `documents.sources.notConnected`: no connection is saved for this system. `documents.sources.notFound`: the source has no such document, or (Papra) no such organization, including @@ -25077,7 +25074,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "502": &a66 + "502": &a69 description: "The source could not answer usefully, with the reason in `meta.errorCode` and, when the source answered at all, its HTTP status in `meta.upstreamStatus` (its body is never passed on): `documents.sources.unreachable` (no connection, timeout, or a destination refused at dial time), `documents.sources.redirected` (the source @@ -25096,9 +25093,9 @@ paths: description: Deletes the caller's connection for this system, and its token with it. Works even after the operator removed the origin from the list, and with the Documents module switched off. `disconnected` is false when there was nothing to delete. Cookie session only; not delegable. - security: *a62 + security: *a65 parameters: - - *a64 + - *a67 responses: "200": description: Removed, or there was nothing to remove. @@ -25107,7 +25104,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceDisconnectEnvelope" "401": *a1 - "403": *a63 + "403": *a66 "422": *a4 "429": *a5 /api/documents/sources/{system}/test: @@ -25118,9 +25115,9 @@ paths: description: One request to the source with the stored token. Answers `ok` with the round-trip time and stamps `lastVerifiedAt`, or the reason in `meta.errorCode`. Shares the save's allowance of ten a minute. Cookie session only; not delegable. - security: *a62 + security: *a65 parameters: - - *a64 + - *a67 responses: "200": description: The source answered. @@ -25129,9 +25126,9 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceTestEnvelope" "401": *a1 - "403": *a63 - "404": *a65 - "422": &a67 + "403": *a66 + "404": *a68 + "422": &a70 description: "`documents.sources.originNotAllowed`: the base address's origin is not on the operator's list (checked on every request, so a connection saved earlier is cut off once its origin is removed), or another validation failure." @@ -25140,7 +25137,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "502": *a66 + "502": *a69 /api/documents/sources/{system}/tags: get: tags: @@ -25148,9 +25145,9 @@ paths: summary: Tags of a connected archive description: The source's tags for the picker's filter, the first 250 by name, in the source's own ids. Shares the search allowance of sixty a minute. Cookie session only; not delegable. - security: *a62 + security: *a65 parameters: - - *a64 + - *a67 responses: "200": description: The tags. @@ -25159,11 +25156,11 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceTagsEnvelope" "401": *a1 - "403": *a63 - "404": *a65 - "422": *a67 + "403": *a66 + "404": *a68 + "422": *a70 "429": *a5 - "502": *a66 + "502": *a69 /api/documents/sources/{system}/search: get: tags: @@ -25177,9 +25174,9 @@ paths: under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` (deleted in HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared with the tag read. Cookie session only; not delegable." - security: *a62 + security: *a65 parameters: - - *a64 + - *a67 - name: q in: query required: false @@ -25220,11 +25217,11 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceSearchEnvelope" "401": *a1 - "403": *a63 - "404": *a65 - "422": *a67 + "403": *a66 + "404": *a68 + "422": *a70 "429": *a5 - "502": *a66 + "502": *a69 /api/documents/sources/{system}/import: post: tags: @@ -25243,9 +25240,9 @@ paths: fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 otherwise. Cookie session only; not delegable." - security: *a62 + security: *a65 parameters: - - *a64 + - *a67 requestBody: required: true content: @@ -25303,7 +25300,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceImportEnvelope" "401": *a1 - "403": *a63 + "403": *a66 "404": description: "`documents.sources.unavailable`: the operator has not listed any origin in `DOCUMENT_SOURCE_ORIGINS`, so the picker is off. `documents.sources.notConnected`: no connection is saved for this system. @@ -25334,7 +25331,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a67 + "422": *a70 "429": description: "`documents.inbound.rateLimited` (the person's hourly upload allowance, with the rate-limit headers) or `documents.inbound.uploadBusy` (too many uploads in progress; `Retry-After: 1`)." @@ -25342,7 +25339,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "502": *a66 + "502": *a69 /api/symptoms/definitions: get: tags: @@ -25366,7 +25363,7 @@ paths: schema: $ref: "#/components/schemas/ListSymptomDefinitionsEnvelope" "401": *a1 - "403": &a68 + "403": &a71 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: `illness.disabled`: the record @@ -25410,7 +25407,7 @@ paths: schema: $ref: "#/components/schemas/CreateSymptomDefinitionEnvelope" "401": *a1 - "403": *a68 + "403": *a71 "409": *a7 "422": *a4 "429": *a5 @@ -25440,8 +25437,8 @@ paths: schema: $ref: "#/components/schemas/UpdateSymptomDefinitionEnvelope" "401": *a1 - "403": *a68 - "404": &a69 + "403": *a71 + "404": &a72 description: "`symptoms.definition.notFound`: no definition with this id on the record." content: application/json: @@ -25475,8 +25472,8 @@ paths: schema: $ref: "#/components/schemas/DeleteSymptomDefinitionEnvelope" "401": *a1 - "403": *a68 - "404": *a69 + "403": *a71 + "404": *a72 "422": *a4 "429": *a5 /api/symptoms/events: @@ -25525,7 +25522,7 @@ paths: schema: $ref: "#/components/schemas/ListSymptomEventsEnvelope" "401": *a1 - "403": *a68 + "403": *a71 "422": *a4 "429": *a5 post: @@ -25551,8 +25548,8 @@ paths: schema: $ref: "#/components/schemas/CreateSymptomEventEnvelope" "401": *a1 - "403": *a68 - "404": *a69 + "403": *a71 + "404": *a72 "409": *a7 "422": *a4 "429": *a5 @@ -25582,8 +25579,8 @@ paths: schema: $ref: "#/components/schemas/UpdateSymptomEventEnvelope" "401": *a1 - "403": *a68 - "404": &a70 + "403": *a71 + "404": &a73 description: "`symptoms.event.notFound`: no occurrence with this id on the record." content: application/json: @@ -25610,8 +25607,8 @@ paths: schema: $ref: "#/components/schemas/DeleteSymptomEventEnvelope" "401": *a1 - "403": *a68 - "404": *a70 + "403": *a71 + "404": *a73 "422": *a4 "429": *a5 /api/day/index: @@ -25762,7 +25759,7 @@ paths: schema: $ref: "#/components/schemas/TimelineEnvelope" "401": *a1 - "403": &a71 + "403": &a74 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: `module.disabled` with @@ -25801,7 +25798,7 @@ paths: schema: $ref: "#/components/schemas/TimelineReadinessEnvelope" "401": *a1 - "403": *a71 + "403": *a74 "422": *a4 "429": *a5 /api/life-events: @@ -25819,7 +25816,7 @@ paths: schema: $ref: "#/components/schemas/LifeEventListEnvelope" "401": *a1 - "403": &a72 + "403": &a75 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Owner-only in v1.42: no share @@ -25861,7 +25858,7 @@ paths: schema: $ref: "#/components/schemas/CreateLifeEventEnvelope" "401": *a1 - "403": *a72 + "403": *a75 "409": *a7 "422": *a4 "429": *a16 @@ -25892,8 +25889,8 @@ paths: schema: $ref: "#/components/schemas/UpdateLifeEventEnvelope" "401": *a1 - "403": *a72 - "404": &a73 + "403": *a75 + "404": &a76 description: No such life event on this account. content: application/json: @@ -25920,8 +25917,8 @@ paths: schema: $ref: "#/components/schemas/DeleteLifeEventEnvelope" "401": *a1 - "403": *a72 - "404": *a73 + "403": *a75 + "404": *a76 "422": *a4 "429": *a5 /api/search: @@ -26096,7 +26093,7 @@ paths: required: - file responses: - "202": *a41 + "202": *a44 "401": *a1 "403": description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the @@ -26132,7 +26129,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "503": *a23 + "503": *a26 /api/anamnesis/life-circumstances: get: tags: diff --git a/src/lib/openapi/routes/profile.ts b/src/lib/openapi/routes/profile.ts index a510c4fad..a19f2eb14 100644 --- a/src/lib/openapi/routes/profile.ts +++ b/src/lib/openapi/routes/profile.ts @@ -1141,6 +1141,29 @@ const profileSummaryResponse = z "The bounded profile view a record viewer gets: allergies, family history and current lifestyle facts. Structurally incapable of becoming a Settings or AI surface.", }); +/** + * What the single-value `/api/auth/me/*` writes answer for a body they refuse + * to read. Each reads through `safeJson` with a 1 KB cap, so the three + * refusals are the cap's, not the route's: a wrong content type is 415, a body + * over the cap 413, and one that does not parse 400. All three are answered + * before validation, and nothing is written. + */ +const cappedScalarBodyRefusals = { + "400": { + description: "The request body is not parseable JSON. Nothing was written.", + content: { "application/json": { schema: errorEnvelope } }, + }, + "413": { + description: "Request body exceeds 1024 bytes. Nothing was written.", + content: { "application/json": { schema: errorEnvelope } }, + }, + "415": { + description: + "The request did not carry `Content-Type: application/json`. Nothing was read and nothing was written.", + content: { "application/json": { schema: errorEnvelope } }, + }, +}; + export const profilePaths: NonNullable = { "/api/profile/summary": { get: { @@ -1863,10 +1886,7 @@ export const profilePaths: NonNullable = { }, }, }, - "413": { - description: "Request body exceeds 1024 bytes. Nothing was written.", - content: { "application/json": { schema: errorEnvelope } }, - }, + ...cappedScalarBodyRefusals, ...stdResponses, }, }, @@ -1917,10 +1937,7 @@ export const profilePaths: NonNullable = { }, }, }, - "413": { - description: "Request body exceeds 1024 bytes. Nothing was written.", - content: { "application/json": { schema: errorEnvelope } }, - }, + ...cappedScalarBodyRefusals, ...stdResponses, }, }, @@ -1971,10 +1988,7 @@ export const profilePaths: NonNullable = { }, }, }, - "413": { - description: "Request body exceeds 1024 bytes. Nothing was written.", - content: { "application/json": { schema: errorEnvelope } }, - }, + ...cappedScalarBodyRefusals, ...stdResponses, }, }, @@ -2257,10 +2271,7 @@ export const profilePaths: NonNullable = { }, }, }, - "413": { - description: "Request body exceeds 1024 bytes. Nothing was written.", - content: { "application/json": { schema: errorEnvelope } }, - }, + ...cappedScalarBodyRefusals, ...stdResponses, }, }, @@ -2307,10 +2318,7 @@ export const profilePaths: NonNullable = { }, }, }, - "413": { - description: "Request body exceeds 1024 bytes. Nothing was written.", - content: { "application/json": { schema: errorEnvelope } }, - }, + ...cappedScalarBodyRefusals, ...stdResponses, }, }, @@ -2360,10 +2368,7 @@ export const profilePaths: NonNullable = { }, }, }, - "413": { - description: "Request body exceeds 1024 bytes. Nothing was written.", - content: { "application/json": { schema: errorEnvelope } }, - }, + ...cappedScalarBodyRefusals, ...stdResponses, }, }, diff --git a/tests/integration/routes/auth-me-disable-coach.test.ts b/tests/integration/routes/auth-me-disable-coach.test.ts index fff8b24d3..26a75e251 100644 --- a/tests/integration/routes/auth-me-disable-coach.test.ts +++ b/tests/integration/routes/auth-me-disable-coach.test.ts @@ -174,9 +174,6 @@ describe("PATCH /api/auth/me/disable-coach", () => { route: ROUTE, headers: { "content-type": "application/json" }, auth: await makeSession(user), - contract: { - skip: "the registry declares no 400 for this PATCH, which answers one for a body that is not JSON", - }, }); expect(res.status).toBe(400); expect(res.error).toBe("Invalid JSON body"); diff --git a/tests/integration/routes/auth-me-modules.test.ts b/tests/integration/routes/auth-me-modules.test.ts index db2c39337..7e9faec7d 100644 --- a/tests/integration/routes/auth-me-modules.test.ts +++ b/tests/integration/routes/auth-me-modules.test.ts @@ -37,11 +37,6 @@ type ModulesResponse = { updatedAt?: string; }; -const NO_400 = - "the registry declares no 400 for PATCH /api/auth/me/modules although the handler answers one for a body that is not JSON"; -const NO_409 = - "the registry declares no 409 for PATCH /api/auth/me/modules although the handler answers modules_conflict for a stale base token"; - beforeEach(() => truncateAllTables(prisma)); async function storedPrefs(userId: string): Promise { @@ -63,14 +58,12 @@ async function storedUpdatedAt(userId: string): Promise { async function patch( auth: Awaited>, body: Record, - contract?: { skip: string }, ) { return callRoute(PATCH, { method: "PATCH", route: ROUTE, body, auth, - contract, }); } @@ -229,7 +222,6 @@ describe("PATCH /api/auth/me/modules", () => { route: ROUTE, headers: { "content-type": "application/json" }, auth: await makeSession(user), - contract: { skip: NO_400 }, }); expect(res.status).toBe(400); expect(res.error).toBe("Invalid JSON body"); @@ -281,11 +273,10 @@ describe("PATCH /api/auth/me/modules evicts the cached score", () => { const user = await makeUser(); caches.analytics.set(`${user.id}|score`, { score: 71 }); - const res = await patch( - await makeSession(user), - { sleep: false, baseUpdatedAt: "2020-01-01T00:00:00.000Z" }, - { skip: NO_409 }, - ); + const res = await patch(await makeSession(user), { + sleep: false, + baseUpdatedAt: "2020-01-01T00:00:00.000Z", + }); expect(res.status).toBe(409); expect(caches.analytics.get(`${user.id}|score`)).toEqual({ score: 71 }); }); @@ -320,11 +311,7 @@ describe("PATCH /api/auth/me/modules optimistic concurrency", () => { const first = await patch(session, { mood: false, baseUpdatedAt: base }); expect(first.status).toBe(200); - const second = await patch( - session, - { sleep: false, baseUpdatedAt: base }, - { skip: NO_409 }, - ); + const second = await patch(session, { sleep: false, baseUpdatedAt: base }); expect(second.status).toBe(409); expect(second.meta?.errorCode).toBe("modules_conflict"); expect(await storedPrefs(user.id)).toEqual({ mood: false }); diff --git a/tests/integration/routes/auth-me-unit-preference.test.ts b/tests/integration/routes/auth-me-unit-preference.test.ts index c9abbc0fd..9c9aa5cec 100644 --- a/tests/integration/routes/auth-me-unit-preference.test.ts +++ b/tests/integration/routes/auth-me-unit-preference.test.ts @@ -171,9 +171,6 @@ describe("PATCH /api/auth/me/unit-preference", () => { route: ROUTE, headers: { "content-type": "application/json" }, auth: await makeSession(user), - contract: { - skip: "the registry declares no 400 for this PATCH although the description names it", - }, }); expect(res.status).toBe(400); expect(await storedPreference(user.id)).toBe("metric"); @@ -199,9 +196,6 @@ describe("PATCH /api/auth/me/unit-preference", () => { body: "unitPreference=imperial", headers: { "content-type": "application/x-www-form-urlencoded" }, auth: await makeSession(user), - contract: { - skip: "the registry declares no 415 for this PATCH although the description names it", - }, }); expect(res.status).toBe(415); expect(await storedPreference(user.id)).toBe("metric");