From 5ca39945236f47cb8aee4defbf00fb427e7701a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 07:07:34 +0200 Subject: [PATCH] fix(api): declare the FHIR refusals as the OperationOutcomes they are A /api/fhir/* handler answers its own refusals as FHIR: a bad search parameter is a 400 OperationOutcome, the hourly fhir bucket a 429 OperationOutcome (issue code throttled), both served as application/fhir+json, the same way the 404 on a read already was. The contract declared no 400 and the generic JSON 429 for the 429, and no 403 for the doctorReport module gate, which every data route runs. The searches now declare the 400, every data route the 403 module.disabled envelope and the FHIR 429 with the rate-limit headers apiHandler attaches to it. The module gate keeps answering the JSON envelope: it is the errorCode every client already branches on for a disabled module, and it runs before the FHIR face does. The route tests drop their skips and assert the 429's content type and Retry-After. --- docs/api/openapi.yaml | 1058 +++++++++-------- src/lib/openapi/routes/health-record.ts | 45 + .../routes/fhir-module-gate.test.ts | 13 +- tests/integration/routes/fhir.test.ts | 8 +- 4 files changed, 591 insertions(+), 533 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 2ffb877dd..8638f44b0 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -399,7 +399,7 @@ paths: "403": *a2 "409": *a7 "422": *a4 - "429": &a13 + "429": &a16 description: "Rate limit exceeded. This route shares one per-account bucket with every other authenticated write — `record-write:`, 300 requests per 60 seconds — keyed on the ACTING account, so a delegate burns their own allowance rather than the record owner's. Only the routes that meter themselves with a dedicated @@ -770,7 +770,7 @@ paths: application/json: schema: $ref: "#/components/schemas/CyclePrefsPatchEnvelope" - "400": &a15 + "400": &a18 description: The request body is not parseable JSON. Nothing was read and nothing was written. `meta.errorCode` names the surface that refused it (`.body.invalid_json`). This is a client-side serialisation fault, not a validation failure — a body that parses but fails the schema is the 422 beside this. @@ -983,7 +983,7 @@ paths: type: string format: binary "401": *a1 - "403": &a12 + "403": &a15 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: this request named a record the caller may not act on (`meta.errorCode` = `sharing.access.denied`). Byte-identical for an account @@ -1309,10 +1309,38 @@ paths: schema: type: string format: binary + "400": &a10 + description: A search parameter the server cannot honour, such as an `_offset` above 9007199254740991. The body is an + `OperationOutcome` (`issue[0].code` = `invalid`, the reason in `diagnostics`) served as + `application/fhir+json`. + content: + application/fhir+json: + schema: + type: string + format: binary "401": *a1 - "403": *a9 + "403": &a11 + 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 for this account: + the user (or operator) has the module turned off. `meta.errorCode` = `module.disabled` and `meta.module` + carries the disabled module key (e.g. \"sleep\"). Returned even for a valid Bearer token. Clients hide the + whole module surface end-to-end rather than retry." + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a5 + "429": &a12 + description: "Rate limit exceeded: the per-account `fhir:` bucket, 120 requests per hour, shared by every + `/api/fhir/*` data route. The body is an `OperationOutcome` (`issue[0].code` = `throttled`) served as + `application/fhir+json`, not the JSON error envelope. The `Retry-After`, `X-RateLimit-Limit`, + `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers describe the bucket; back off on `Retry-After`." + headers: *a8 + content: + application/fhir+json: + schema: + type: string + format: binary /api/fhir/Observation: get: tags: @@ -1356,10 +1384,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationStatement: get: tags: @@ -1386,10 +1415,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationAdministration: get: tags: @@ -1416,10 +1446,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Coverage: get: tags: @@ -1447,10 +1478,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Patient/{id}: get: tags: @@ -1474,7 +1506,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1483,7 +1515,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Coverage/{id}: get: tags: @@ -1507,7 +1539,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1516,7 +1548,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Observation/{id}: get: tags: @@ -1541,7 +1573,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1550,7 +1582,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationStatement/{id}: get: tags: @@ -1574,7 +1606,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1583,7 +1615,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationAdministration/{id}: get: tags: @@ -1607,7 +1639,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1616,7 +1648,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Patient/$everything: get: tags: @@ -1650,10 +1682,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/export: get: tags: @@ -2177,7 +2210,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": &a10 + "409": &a13 description: 'Refused before anything was checked or spent: the request stated `clientTransport.protocol: "http"` on a non-loopback host while this server issues `Secure` session cookies, so the browser would drop the session. `meta.errorCode` = `auth.session.insecure_transport`, `meta.sessionCookieSecure` = `true`. No password was @@ -2213,7 +2246,7 @@ paths: $ref: "#/components/schemas/MfaVerifyResponse" "401": *a1 "403": *a9 - "409": *a10 + "409": *a13 "422": *a4 "429": *a5 /api/auth/oidc/native/token: @@ -2520,7 +2553,7 @@ paths: application/json: schema: $ref: "#/components/schemas/TotpSetupEnvelope" - "401": &a11 + "401": &a14 description: No session or elevation; or, on the cookie path, `auth.reproof.required` (no recent sign-in and no proof in the body) or `auth.reproof.failed` (the proof did not verify). content: @@ -2673,7 +2706,7 @@ paths: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRegisterOptionsEnvelope" - "401": *a11 + "401": *a14 "403": *a9 "422": *a4 "429": *a5 @@ -2827,7 +2860,7 @@ paths: $ref: "#/components/schemas/MfaWebauthnVerifyResponse" "401": *a1 "403": *a9 - "409": *a10 + "409": *a13 "422": *a4 "429": *a5 /api/auth/passkey/login-verify: @@ -3193,7 +3226,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": *a10 + "409": *a13 "429": *a5 /api/auth/passkeys/{id}: patch: @@ -3815,7 +3848,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": &a17 + "429": &a20 description: "Too many email-address changes, and the request asked for nothing else. The address change shares one per-account bucket across both profile routes — `profile-email:`, 10 requests per 3600 seconds — keyed on the ACTING account, so an account cannot collect a fresh allowance by probing a different address. @@ -4272,7 +4305,7 @@ paths: schema: $ref: "#/components/schemas/GetMeasurementsSeriesBatchResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/measurements/series: @@ -4327,7 +4360,7 @@ paths: schema: $ref: "#/components/schemas/GetMeasurementsSeriesResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/measurements: @@ -4548,7 +4581,7 @@ paths: schema: $ref: "#/components/schemas/ListMeasurementsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -4604,7 +4637,7 @@ paths: schema: $ref: "#/components/schemas/CreateMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": description: A measurement with this data already exists — the `(userId, type, source, externalId)` dedup index rejected the write. Never for a named sleep-stage body, which answers a re-post with 200 instead. @@ -4620,7 +4653,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/measurements/{id}: get: tags: @@ -4641,7 +4674,7 @@ paths: schema: $ref: "#/components/schemas/GetMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Measurement not found (or owned by another user). content: @@ -4689,7 +4722,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Measurement not found (or owned by another user). content: @@ -4725,7 +4758,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Measurement not found (or owned by another user). content: @@ -4838,7 +4871,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementsBulkDeleteResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -4867,7 +4900,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementsRestoreResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -4900,7 +4933,7 @@ paths: schema: $ref: "#/components/schemas/SleepNightResponse" "401": *a1 - "403": &a14 + "403": &a17 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 for this account: @@ -4943,7 +4976,7 @@ paths: schema: $ref: "#/components/schemas/SleepRhythmResponse" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/workouts: @@ -4992,7 +5025,7 @@ paths: schema: $ref: "#/components/schemas/ListWorkoutsResponse" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/workouts/{id}: @@ -5028,16 +5061,7 @@ paths: schema: $ref: "#/components/schemas/GetWorkoutResponse" "401": *a1 - "403": &a21 - 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 for this account: - the user (or operator) has the module turned off. `meta.errorCode` = `module.disabled` and `meta.module` - carries the disabled module key (e.g. \"sleep\"). Returned even for a valid Bearer token. Clients hide the - whole module surface end-to-end rather than retry." - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "403": *a11 "404": description: Workout not found (or owned by another user). content: @@ -5189,7 +5213,7 @@ paths: schema: $ref: "#/components/schemas/ActivityCalendarEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/activity/goals: @@ -5232,7 +5256,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a13 + "429": *a16 /api/measurements/provenance: get: tags: @@ -5357,7 +5381,7 @@ paths: schema: $ref: "#/components/schemas/DayProvenanceEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/measurements/provenance/compare: @@ -5535,7 +5559,7 @@ paths: schema: $ref: "#/components/schemas/SourceComparisonEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/devices: @@ -5647,7 +5671,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeAggregateEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": description: The query failed validation — a missing or unknown `scope`, or a `days` outside 1..365. Multi-issue envelope; an audit breadcrumb is filed under the RECORD as `medications.intake.list.validation-failed` with @@ -5777,7 +5801,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/medications/{id}/glp1: get: tags: @@ -5802,7 +5826,7 @@ paths: schema: $ref: "#/components/schemas/Glp1DetailResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -5886,7 +5910,7 @@ paths: schema: $ref: "#/components/schemas/Glp1PostResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6025,7 +6049,7 @@ paths: schema: $ref: "#/components/schemas/ReminderPhaseConfigEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6143,7 +6167,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationSideEffectsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6182,7 +6206,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationSideEffectEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6227,7 +6251,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMedicationSideEffectEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No such entry for this medication and this record. An unknown id, another medication's entry and another record's entry are indistinguishable. @@ -6275,7 +6299,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMedicationIntakeEventEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No live event with that id under this medication and record. An unknown id and an already-tombstoned one are indistinguishable. @@ -6323,7 +6347,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMedicationIntakeEventEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No event with that id under this medication and record. content: @@ -6366,7 +6390,7 @@ paths: schema: $ref: "#/components/schemas/BulkDeleteMedicationIntakeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6408,7 +6432,7 @@ paths: schema: $ref: "#/components/schemas/PurgeMedicationIntakeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6453,7 +6477,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeImportAcceptedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6534,7 +6558,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -6566,7 +6590,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -6586,7 +6610,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationCategoriesResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -6613,7 +6637,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationCategoryResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -6846,7 +6870,7 @@ paths: schema: $ref: "#/components/schemas/MedicationListLayoutResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 put: @@ -6881,7 +6905,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": &a16 + "422": &a19 description: Request validation failed. When the body carried a `baseUpdatedAt` that could not be parsed as an ISO-8601 timestamp — including an explicit `null` — `meta.errorCode` = `invalid_base_updated_at` and nothing was written. Omit the key entirely for the unconditional write; do not send `null` for it. @@ -6927,7 +6951,7 @@ paths: schema: $ref: "#/components/schemas/GetMedicationResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -6968,7 +6992,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMedicationResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7042,7 +7066,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationIntakeResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7060,7 +7084,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 get: tags: - Medications @@ -7125,7 +7149,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationIntakeEventsResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7158,7 +7182,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationInventoryResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7194,7 +7218,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationInventoryItemResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7238,7 +7262,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMedicationInventoryItemResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Inventory item not found (or owned by another user / medication). content: @@ -7273,7 +7297,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMedicationInventoryItemResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Inventory item not found (or owned by another user / medication). content: @@ -7371,7 +7395,7 @@ paths: schema: $ref: "#/components/schemas/GetMedicationCadenceResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7420,7 +7444,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationComplianceResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/medications/{id}/compliance: @@ -7605,7 +7629,7 @@ paths: schema: $ref: "#/components/schemas/ListScheduleRevisionsResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7643,7 +7667,7 @@ paths: schema: $ref: "#/components/schemas/CreateScheduleRevisionResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7687,7 +7711,7 @@ paths: schema: $ref: "#/components/schemas/UpdateScheduleRevisionResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication or revision not found (or owned by another user). content: @@ -7728,7 +7752,7 @@ paths: schema: $ref: "#/components/schemas/DeleteScheduleRevisionResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication or revision not found (or owned by another user). content: @@ -7791,7 +7815,7 @@ paths: schema: $ref: "#/components/schemas/MedicationDoseHistoryImportQueuedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": description: A dose-history import is already in progress. content: @@ -7853,7 +7877,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeImportJobStatusEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No such job for this medication (or owned by another user). content: @@ -7887,7 +7911,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeImportJobStatusEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No such job for this account (or owned by another user). content: @@ -7917,7 +7941,7 @@ paths: schema: $ref: "#/components/schemas/ProfileSummaryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/ai/test: @@ -8127,7 +8151,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutCoachPrefsResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "409": @@ -8138,7 +8162,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/auth/me/disable-coach: get: @@ -8177,7 +8201,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchDisableCoachResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "422": *a4 @@ -8338,7 +8362,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchModulesResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "409": @@ -8349,7 +8373,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/auth/me/source-priority: get: @@ -9301,7 +9325,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutSourcePriorityResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "422": *a4 @@ -9461,7 +9485,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a17 + "429": *a20 /api/auth/me/notification-prefs: get: tags: @@ -9504,7 +9528,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchNotificationPrefsResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "409": @@ -9515,7 +9539,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/auth/me/report-selection: get: @@ -9558,7 +9582,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutReportSelectionResponseEnvelope" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "422": *a4 @@ -10352,7 +10376,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 - "503": &a20 + "503": &a23 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: @@ -10771,7 +10795,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 delete: tags: @@ -10953,7 +10977,7 @@ paths: schema: $ref: "#/components/schemas/CoachNudgeStatus" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/coach/seen: @@ -11074,7 +11098,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/anamnesis/facts: get: @@ -11093,7 +11117,7 @@ paths: schema: $ref: "#/components/schemas/GetAnamnesisFactsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -11568,7 +11592,7 @@ paths: $ref: "#/components/schemas/CorrectAnamnesisFactResponse" "401": *a1 "403": *a9 - "404": &a18 + "404": &a21 description: No current revision with this id exists for the authenticated account. content: application/json: @@ -11605,7 +11629,7 @@ paths: $ref: "#/components/schemas/RemoveAnamnesisFactResponse" "401": *a1 "403": *a9 - "404": *a18 + "404": *a21 "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. @@ -11976,7 +12000,7 @@ paths: schema: $ref: "#/components/schemas/CoachRemindersList" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -12144,7 +12168,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteList" "401": *a1 - "403": &a19 + "403": &a22 description: The session is valid but its account is not an administrator. `meta.errorCode` = `auth.admin.required`. content: application/json: @@ -12185,7 +12209,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteCreated" "401": *a1 - "403": *a19 + "403": *a22 "422": *a4 "429": *a5 /api/admin/invites/{id}: @@ -12210,7 +12234,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteRevoked" "401": *a1 - "403": *a19 + "403": *a22 "422": *a4 "429": *a5 /api/insights/chat/messages/{id}/feedback: @@ -12266,7 +12290,7 @@ paths: schema: $ref: "#/components/schemas/AdminMeasurementMaintenanceStatusResponse" "401": *a1 - "403": *a19 + "403": *a22 "422": *a4 "429": *a5 post: @@ -12396,7 +12420,7 @@ paths: schema: $ref: "#/components/schemas/DashboardSnapshotResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/comprehensive: @@ -12418,7 +12442,7 @@ paths: schema: $ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/cards: @@ -12439,7 +12463,7 @@ paths: schema: $ref: "#/components/schemas/InsightsCardsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/pregenerate: @@ -12577,7 +12601,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "503": *a20 + "503": *a23 post: tags: - Insights @@ -12783,7 +12807,7 @@ paths: schema: $ref: "#/components/schemas/CoachReadStripEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/narrative: @@ -12832,7 +12856,7 @@ paths: schema: $ref: "#/components/schemas/NarrativeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/glp1-timeline: @@ -12869,7 +12893,7 @@ paths: schema: $ref: "#/components/schemas/Glp1TimelineEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/pulse/intraday: @@ -12905,7 +12929,7 @@ paths: schema: $ref: "#/components/schemas/IntradayPulseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/settings: @@ -12981,7 +13005,7 @@ paths: schema: $ref: "#/components/schemas/TargetsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/analytics/range: @@ -13055,7 +13079,7 @@ paths: schema: $ref: "#/components/schemas/BloodPressureStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/pulse-status: @@ -13087,7 +13111,7 @@ paths: schema: $ref: "#/components/schemas/PulseStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/weight-status: @@ -13119,7 +13143,7 @@ paths: schema: $ref: "#/components/schemas/WeightStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/bmi-status: @@ -13151,7 +13175,7 @@ paths: schema: $ref: "#/components/schemas/BmiStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/mood-status: @@ -13183,7 +13207,7 @@ paths: schema: $ref: "#/components/schemas/MoodStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/medication-compliance-status: @@ -13216,7 +13240,7 @@ paths: schema: $ref: "#/components/schemas/MedicationComplianceStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/metric-status: @@ -13311,7 +13335,7 @@ paths: schema: $ref: "#/components/schemas/MetricStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/biomarker-assessment: @@ -13356,7 +13380,7 @@ paths: schema: $ref: "#/components/schemas/BiomarkerAssessmentResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/derived: @@ -13468,7 +13492,7 @@ paths: schema: $ref: "#/components/schemas/DerivedMetricResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/derived/batch: @@ -13508,7 +13532,7 @@ paths: schema: $ref: "#/components/schemas/DerivedBatchResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/score-history: @@ -13558,7 +13582,7 @@ paths: schema: $ref: "#/components/schemas/ScoreHistoryResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/glp1-plateau: @@ -13578,7 +13602,7 @@ paths: schema: $ref: "#/components/schemas/InsightsGlp1PlateauResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/ecg: @@ -13600,7 +13624,7 @@ paths: schema: $ref: "#/components/schemas/EcgListResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -13678,7 +13702,7 @@ paths: schema: $ref: "#/components/schemas/EcgDetailResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No ECG recording with that id for the authenticated user (existence sealed — a foreign id is indistinguishable from a missing one). @@ -13710,7 +13734,7 @@ paths: schema: $ref: "#/components/schemas/RhythmEventsResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/correlations: @@ -13732,7 +13756,7 @@ paths: schema: $ref: "#/components/schemas/CorrelationDiscoveryResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/patterns: @@ -13750,7 +13774,7 @@ paths: schema: $ref: "#/components/schemas/CorrelationPatternListResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/patterns/{id}: @@ -13780,7 +13804,7 @@ paths: schema: $ref: "#/components/schemas/UpdateCorrelationPatternResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No current pattern with that id for this account. content: @@ -13805,7 +13829,7 @@ paths: schema: $ref: "#/components/schemas/InsightsHealthStatusEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/breathing-screening: @@ -13824,7 +13848,7 @@ paths: schema: $ref: "#/components/schemas/InsightsBreathingScreeningEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/labs-changes: @@ -13843,7 +13867,7 @@ paths: schema: $ref: "#/components/schemas/InsightsLabsChangesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/mood/analytics: @@ -13866,7 +13890,7 @@ paths: schema: $ref: "#/components/schemas/MoodDailySeriesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/mood/insights: @@ -13901,7 +13925,7 @@ paths: schema: $ref: "#/components/schemas/MoodAggregatesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/mood/prognosis: @@ -13924,7 +13948,7 @@ paths: schema: $ref: "#/components/schemas/MoodPrognosisEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/mood-entries/bulk: @@ -14038,7 +14062,7 @@ paths: $ref: "#/components/schemas/MoodCustomTagPatchEnvelope" "401": *a1 "403": *a9 - "404": &a22 + "404": &a24 description: Not found / not owned by the caller. content: application/json: @@ -14074,7 +14098,7 @@ paths: $ref: "#/components/schemas/MoodCustomTagDeleteEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 /api/mood/tags/{key}/hidden: @@ -14111,7 +14135,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 /api/mood/tags/groups: @@ -14166,7 +14190,7 @@ paths: $ref: "#/components/schemas/MoodTagGroupPatchEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 delete: @@ -14198,7 +14222,7 @@ paths: $ref: "#/components/schemas/MoodTagGroupDeleteEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 /api/mood/tags/layout: @@ -14248,7 +14272,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/mood-entries: get: @@ -14332,7 +14356,7 @@ paths: schema: $ref: "#/components/schemas/MoodEntryListEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -14365,8 +14389,8 @@ paths: schema: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 - "403": *a12 - "409": &a23 + "403": *a15 + "409": &a25 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. @@ -14375,7 +14399,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a13 + "429": *a16 /api/mood-entries/{id}: get: tags: @@ -14397,8 +14421,8 @@ paths: schema: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 - "403": *a12 - "404": *a22 + "403": *a15 + "404": *a24 "422": *a4 "429": *a5 put: @@ -14432,9 +14456,9 @@ paths: schema: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 - "403": *a12 - "404": *a22 - "409": *a23 + "403": *a15 + "404": *a24 + "409": *a25 "422": *a4 "429": *a5 delete: @@ -14459,8 +14483,8 @@ paths: schema: $ref: "#/components/schemas/MoodEntryDeleteEnvelope" "401": *a1 - "403": *a12 - "404": *a22 + "403": *a15 + "404": *a24 "422": *a4 "429": *a5 /api/mood-entries/restore: @@ -14489,7 +14513,7 @@ paths: schema: $ref: "#/components/schemas/MoodEntriesRestoreEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -14518,7 +14542,7 @@ paths: schema: $ref: "#/components/schemas/MoodEntriesBulkDeleteEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -14562,7 +14586,7 @@ paths: schema: $ref: "#/components/schemas/MoodLinkedContextEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/settings/ntfy: @@ -14611,7 +14635,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutNtfySettingsResponse" - "400": &a24 + "400": &a26 description: Body is not parseable JSON. content: application/json: @@ -14626,13 +14650,13 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "413": &a25 + "413": &a27 description: Body exceeds 64 KiB. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "415": &a26 + "415": &a28 description: Content-Type is not `application/json`. content: application/json: @@ -14693,7 +14717,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutTelegramSettingsResponse" - "400": *a24 + "400": *a26 "401": *a1 "403": description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the @@ -14709,8 +14733,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "413": *a25 - "415": *a26 + "413": *a27 + "415": *a28 "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 @@ -14765,11 +14789,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PutWebhookSettingsResponse" - "400": *a24 + "400": *a26 "401": *a1 "403": *a9 - "413": *a25 - "415": *a26 + "413": *a27 + "415": *a28 "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` = @@ -14796,7 +14820,7 @@ paths: schema: $ref: "#/components/schemas/GetReminderThresholdsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/settings/global-services: @@ -15434,7 +15458,7 @@ paths: anyOf: - $ref: "#/components/schemas/ConsentLatestForKindEnvelope" - $ref: "#/components/schemas/ConsentLatestByKindEnvelope" - "400": &a27 + "400": &a29 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." @@ -15495,7 +15519,7 @@ paths: anyOf: - $ref: "#/components/schemas/ConsentRevokeForKindEnvelope" - $ref: "#/components/schemas/ConsentRevokeAllEnvelope" - "400": *a27 + "400": *a29 "401": *a1 "403": *a9 "422": *a4 @@ -16238,7 +16262,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderListEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -16263,7 +16287,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderCreatedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -16287,8 +16311,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderEnvelope" "401": *a1 - "403": *a12 - "404": &a28 + "403": *a15 + "404": &a30 description: Measurement reminder not found / not owned. content: application/json: @@ -16325,8 +16349,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderPatchEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 delete: @@ -16348,8 +16372,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderDeleteEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/satisfy: @@ -16373,8 +16397,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderSatisfyEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/complete: @@ -16401,8 +16425,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderCompleteEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/skip: @@ -16431,8 +16455,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderSkipEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/snooze: @@ -16465,8 +16489,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderSnoozeEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/history: @@ -16506,8 +16530,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderHistoryEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/labs: @@ -16585,7 +16609,7 @@ paths: schema: $ref: "#/components/schemas/ListLabResultsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -16612,7 +16636,7 @@ paths: schema: $ref: "#/components/schemas/CreateLabResultResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": description: "Nothing was written. `meta.errorCode` = `labs.create.invalid` when the body failed validation (every issue @@ -16627,7 +16651,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/labs/overview: get: tags: @@ -16645,7 +16669,7 @@ paths: schema: $ref: "#/components/schemas/LabOverviewEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/labs/{id}: @@ -16668,8 +16692,8 @@ paths: schema: $ref: "#/components/schemas/GetLabResultResponse" "401": *a1 - "403": *a12 - "404": &a29 + "403": *a15 + "404": &a31 description: Lab result not found (or owned by another user). content: application/json: @@ -16703,8 +16727,8 @@ paths: schema: $ref: "#/components/schemas/UpdateLabResultResponse" "401": *a1 - "403": *a12 - "404": *a29 + "403": *a15 + "404": *a31 "422": *a4 "429": *a5 delete: @@ -16726,8 +16750,8 @@ paths: schema: $ref: "#/components/schemas/DeleteLabResultResponse" "401": *a1 - "403": *a12 - "404": *a29 + "403": *a15 + "404": *a31 "422": *a4 "429": *a5 /api/labs/restore: @@ -16753,7 +16777,7 @@ paths: schema: $ref: "#/components/schemas/RestoreLabResultsResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -16815,7 +16839,7 @@ paths: schema: $ref: "#/components/schemas/OcrTextExtractRequest" responses: - "202": &a39 + "202": &a41 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. @@ -16939,7 +16963,7 @@ paths: schema: $ref: "#/components/schemas/ListBiomarkersEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -16964,7 +16988,7 @@ paths: schema: $ref: "#/components/schemas/CreateBiomarkerResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": description: A biomarker with this name already exists for the caller. content: @@ -16972,7 +16996,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a13 + "429": *a16 /api/biomarkers/{id}: get: tags: @@ -16995,8 +17019,8 @@ paths: schema: $ref: "#/components/schemas/GetBiomarkerResponse" "401": *a1 - "403": *a12 - "404": &a30 + "403": *a15 + "404": &a32 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." @@ -17037,8 +17061,8 @@ paths: schema: $ref: "#/components/schemas/UpdateBiomarkerResponse" "401": *a1 - "403": *a12 - "404": *a30 + "403": *a15 + "404": *a32 "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; @@ -17071,8 +17095,8 @@ paths: schema: $ref: "#/components/schemas/DeleteBiomarkerResponse" "401": *a1 - "403": *a12 - "404": *a30 + "403": *a15 + "404": *a32 "422": *a4 "429": *a5 /api/biomarkers/{id}/merge: @@ -17109,8 +17133,8 @@ paths: schema: $ref: "#/components/schemas/MergeBiomarkersResponse" "401": *a1 - "403": *a12 - "404": *a30 + "403": *a15 + "404": *a32 "409": *a7 "422": description: "`labs.biomarker.merge.invalid`: a malformed body, or the target among the sources (Zod issues in @@ -17121,7 +17145,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/encounters: get: tags: @@ -17184,7 +17208,7 @@ paths: schema: $ref: "#/components/schemas/ListEncountersEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17210,10 +17234,10 @@ paths: schema: $ref: "#/components/schemas/CreateEncounterEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/encounters/procedures: get: tags: @@ -17244,7 +17268,7 @@ paths: schema: $ref: "#/components/schemas/ListProceduresEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/body-sites: @@ -17281,7 +17305,7 @@ paths: schema: $ref: "#/components/schemas/ListBodySitesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/encounters/suggest: @@ -17311,7 +17335,7 @@ paths: schema: $ref: "#/components/schemas/SuggestEncounterEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/encounters/{id}: @@ -17334,8 +17358,8 @@ paths: schema: $ref: "#/components/schemas/GetEncounterEnvelope" "401": *a1 - "403": *a12 - "404": &a31 + "403": *a15 + "404": &a33 description: Visit not found (or owned by another user). content: application/json: @@ -17368,8 +17392,8 @@ paths: schema: $ref: "#/components/schemas/UpdateEncounterEnvelope" "401": *a1 - "403": *a12 - "404": *a31 + "403": *a15 + "404": *a33 "422": *a4 "429": *a5 delete: @@ -17393,8 +17417,8 @@ paths: schema: $ref: "#/components/schemas/DeleteEncounterEnvelope" "401": *a1 - "403": *a12 - "404": *a31 + "403": *a15 + "404": *a33 "422": *a4 "429": *a5 /api/encounters/{id}/restore: @@ -17419,8 +17443,8 @@ paths: schema: $ref: "#/components/schemas/RestoreEncounterEnvelope" "401": *a1 - "403": *a12 - "404": *a31 + "403": *a15 + "404": *a33 "422": *a4 "429": *a5 /api/encounters/{id}/links: @@ -17452,7 +17476,7 @@ paths: $ref: "#/components/schemas/LinkEncounterEnvelope" "401": *a1 "403": *a9 - "404": *a31 + "404": *a33 "422": *a4 "429": *a5 delete: @@ -17482,7 +17506,7 @@ paths: $ref: "#/components/schemas/UnlinkEncounterEnvelope" "401": *a1 "403": *a9 - "404": *a31 + "404": *a33 "422": *a4 "429": *a5 /api/practitioners: @@ -17522,7 +17546,7 @@ paths: schema: $ref: "#/components/schemas/ListPractitionersEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17546,7 +17570,7 @@ paths: schema: $ref: "#/components/schemas/CreatePractitionerEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -17570,8 +17594,8 @@ paths: schema: $ref: "#/components/schemas/GetPractitionerEnvelope" "401": *a1 - "403": *a12 - "404": &a32 + "403": *a15 + "404": &a34 description: Practitioner not found (or owned by another user). content: application/json: @@ -17604,8 +17628,8 @@ paths: schema: $ref: "#/components/schemas/UpdatePractitionerEnvelope" "401": *a1 - "403": *a12 - "404": *a32 + "403": *a15 + "404": *a34 "422": *a4 "429": *a5 delete: @@ -17629,8 +17653,8 @@ paths: schema: $ref: "#/components/schemas/DeletePractitionerEnvelope" "401": *a1 - "403": *a12 - "404": *a32 + "403": *a15 + "404": *a34 "422": *a4 "429": *a5 /api/practitioners/{id}/restore: @@ -17655,8 +17679,8 @@ paths: schema: $ref: "#/components/schemas/RestorePractitionerEnvelope" "401": *a1 - "403": *a12 - "404": *a32 + "403": *a15 + "404": *a34 "422": *a4 "429": *a5 /api/vaccinations/custom: @@ -17674,7 +17698,7 @@ paths: schema: $ref: "#/components/schemas/ListCustomVaccinesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17699,8 +17723,8 @@ paths: schema: $ref: "#/components/schemas/CreateCustomVaccineEnvelope" "401": *a1 - "403": *a12 - "409": &a33 + "403": *a15 + "409": &a35 description: A live definition on the record already carries this name, compared without case (`vaccination.custom.name-taken`). content: @@ -17708,7 +17732,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a13 + "429": *a16 /api/vaccinations/custom/{id}: patch: tags: @@ -17736,14 +17760,14 @@ paths: schema: $ref: "#/components/schemas/UpdateCustomVaccineEnvelope" "401": *a1 - "403": *a12 - "404": &a34 + "403": *a15 + "404": &a36 description: No live definition with this id on the record (`vaccination.custom.not-found`). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": *a33 + "409": *a35 "422": *a4 "429": *a5 delete: @@ -17767,8 +17791,8 @@ paths: schema: $ref: "#/components/schemas/DeleteCustomVaccineEnvelope" "401": *a1 - "403": *a12 - "404": *a34 + "403": *a15 + "404": *a36 "422": *a4 "429": *a5 /api/vaccinations: @@ -17821,7 +17845,7 @@ paths: schema: $ref: "#/components/schemas/ListVaccinationsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17848,7 +17872,7 @@ paths: schema: $ref: "#/components/schemas/CreateVaccinationEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: A practice, visit or own vaccine the body names is not the record's (`vaccination.practitioner-not-found`, `vaccination.encounter-not-found`, `vaccination.custom-vaccine-not-found`). @@ -17858,7 +17882,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/vaccinations/{id}: get: tags: @@ -17879,8 +17903,8 @@ paths: schema: $ref: "#/components/schemas/GetVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": &a35 + "403": *a15 + "404": &a37 description: Vaccination not found (or owned by another user). content: application/json: @@ -17914,8 +17938,8 @@ paths: schema: $ref: "#/components/schemas/UpdateVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": *a35 + "403": *a15 + "404": *a37 "422": *a4 "429": *a5 delete: @@ -17939,8 +17963,8 @@ paths: schema: $ref: "#/components/schemas/DeleteVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": *a35 + "403": *a15 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/{id}/restore: @@ -17964,8 +17988,8 @@ paths: schema: $ref: "#/components/schemas/RestoreVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": *a35 + "403": *a15 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/{id}/booster: @@ -18033,7 +18057,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "404": *a35 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/suggest: @@ -18061,7 +18085,7 @@ paths: schema: $ref: "#/components/schemas/VaccinationSuggestEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/vaccinations/{id}/links: @@ -18094,7 +18118,7 @@ paths: $ref: "#/components/schemas/LinkVaccinationEnvelope" "401": *a1 "403": *a9 - "404": *a35 + "404": *a37 "422": *a4 "429": *a5 delete: @@ -18124,7 +18148,7 @@ paths: $ref: "#/components/schemas/UnlinkVaccinationEnvelope" "401": *a1 "403": *a9 - "404": *a35 + "404": *a37 "422": *a4 "429": *a5 /api/illness/episodes: @@ -18188,7 +18212,7 @@ paths: schema: $ref: "#/components/schemas/ListIllnessEpisodesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18214,7 +18238,7 @@ paths: schema: $ref: "#/components/schemas/CreateIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -18238,8 +18262,8 @@ paths: schema: $ref: "#/components/schemas/GetIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": &a36 + "403": *a15 + "404": &a38 description: Illness episode not found (or owned by another user). content: application/json: @@ -18273,8 +18297,8 @@ paths: schema: $ref: "#/components/schemas/UpdateIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 delete: @@ -18297,8 +18321,8 @@ paths: schema: $ref: "#/components/schemas/DeleteIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 /api/illness/episodes/{id}/restore: @@ -18325,8 +18349,8 @@ paths: schema: $ref: "#/components/schemas/RestoreIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "409": *a7 "422": *a4 "429": *a5 @@ -18358,8 +18382,8 @@ paths: schema: $ref: "#/components/schemas/ResolveIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 /api/illness/episodes/{id}/day-logs: @@ -18409,8 +18433,8 @@ paths: schema: $ref: "#/components/schemas/GetIllnessDayLogEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 post: @@ -18446,8 +18470,8 @@ paths: schema: $ref: "#/components/schemas/UpsertIllnessDayLogCreatedEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 /api/illness/episodes/{id}/correlation: @@ -18475,7 +18499,7 @@ paths: $ref: "#/components/schemas/IllnessCorrelationEnvelope" "401": *a1 "403": *a9 - "404": *a36 + "404": *a38 "422": *a4 "429": *a5 /api/illness/insights: @@ -18549,7 +18573,7 @@ paths: schema: $ref: "#/components/schemas/ListMentalHealthAssessmentsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18582,7 +18606,7 @@ paths: schema: $ref: "#/components/schemas/CreateMentalHealthAssessmentEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -18609,7 +18633,7 @@ paths: schema: $ref: "#/components/schemas/GetMentalHealthAssessmentEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/allergies: @@ -18642,7 +18666,7 @@ paths: schema: $ref: "#/components/schemas/ListAllergiesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18667,10 +18691,10 @@ paths: schema: $ref: "#/components/schemas/CreateAllergyEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/allergies/{id}: get: tags: @@ -18691,8 +18715,8 @@ paths: schema: $ref: "#/components/schemas/GetAllergyEnvelope" "401": *a1 - "403": *a12 - "404": &a37 + "403": *a15 + "404": &a39 description: Allergy not found (or owned by another user). content: application/json: @@ -18725,8 +18749,8 @@ paths: schema: $ref: "#/components/schemas/UpdateAllergyEnvelope" "401": *a1 - "403": *a12 - "404": *a37 + "403": *a15 + "404": *a39 "422": *a4 "429": *a5 delete: @@ -18749,8 +18773,8 @@ paths: schema: $ref: "#/components/schemas/DeleteAllergyEnvelope" "401": *a1 - "403": *a12 - "404": *a37 + "403": *a15 + "404": *a39 "422": *a4 "429": *a5 /api/family-history: @@ -18774,7 +18798,7 @@ paths: schema: $ref: "#/components/schemas/ListFamilyHistoryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18799,7 +18823,7 @@ paths: schema: $ref: "#/components/schemas/CreateFamilyHistoryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -18823,8 +18847,8 @@ paths: schema: $ref: "#/components/schemas/GetFamilyHistoryEnvelope" "401": *a1 - "403": *a12 - "404": &a38 + "403": *a15 + "404": &a40 description: Family history entry not found (or owned by another user). content: application/json: @@ -18857,8 +18881,8 @@ paths: schema: $ref: "#/components/schemas/UpdateFamilyHistoryEnvelope" "401": *a1 - "403": *a12 - "404": *a38 + "403": *a15 + "404": *a40 "422": *a4 "429": *a5 delete: @@ -18881,8 +18905,8 @@ paths: schema: $ref: "#/components/schemas/DeleteFamilyHistoryEnvelope" "401": *a1 - "403": *a12 - "404": *a38 + "403": *a15 + "404": *a40 "422": *a4 "429": *a5 /api/documents/inbound: @@ -18997,7 +19021,7 @@ paths: schema: $ref: "#/components/schemas/InboundDocumentListEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -19283,7 +19307,7 @@ paths: schema: $ref: "#/components/schemas/DocumentUsageEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/documents/inbound/layout: @@ -19301,7 +19325,7 @@ paths: schema: $ref: "#/components/schemas/DocumentsLayoutEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 put: @@ -19372,7 +19396,7 @@ paths: schema: $ref: "#/components/schemas/InboundDocumentDetailEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 patch: @@ -19494,7 +19518,7 @@ paths: type: string format: binary "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/documents/inbound/{id}/thumbnail: @@ -19523,7 +19547,7 @@ paths: type: string format: binary "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/documents/inbound/{id}/extract: @@ -19555,7 +19579,7 @@ paths: required: true schema: type: string - - &a40 + - &a42 name: Prefer in: header required: false @@ -19580,9 +19604,9 @@ paths: application/json: schema: $ref: "#/components/schemas/InboundExtractEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": &a41 + "403": &a43 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 @@ -19608,7 +19632,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 - "503": &a42 + "503": &a44 description: "`ai.unavailable` — the inputs behind the AI capability could not be read, and the answer failed closed. Nothing was sent. Retry later." content: @@ -19638,7 +19662,7 @@ paths: required: true schema: type: string - - *a40 + - *a42 requestBody: required: false content: @@ -19674,12 +19698,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentSuggestEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/summary: post: tags: @@ -19729,7 +19753,7 @@ paths: type: boolean default: false description: With `persist=true`, explicitly replace an existing stored summary. - - *a40 + - *a42 requestBody: required: false content: @@ -19765,12 +19789,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentSummaryEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/index: post: tags: @@ -19796,7 +19820,7 @@ paths: required: true schema: type: string - - *a40 + - *a42 requestBody: required: false content: @@ -19832,12 +19856,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentIndexEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/chat: post: tags: @@ -19877,10 +19901,10 @@ paths: type: string description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes." "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 get: tags: - Documents @@ -19944,10 +19968,10 @@ paths: schema: $ref: "#/components/schemas/DocumentReindexEnvelope" "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/facts/{factId}: patch: tags: @@ -20031,7 +20055,7 @@ paths: schema: $ref: "#/components/schemas/ListCustomMetricsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -20057,7 +20081,7 @@ paths: $ref: "#/components/schemas/CreateCustomMetricResponse" "401": *a1 "403": *a9 - "409": &a44 + "409": &a46 description: A custom metric with this name already exists for the caller. content: application/json: @@ -20085,8 +20109,8 @@ paths: schema: $ref: "#/components/schemas/GetCustomMetricResponse" "401": *a1 - "403": *a12 - "404": &a43 + "403": *a15 + "404": &a45 description: Custom metric not found (or owned by another user). content: application/json: @@ -20121,8 +20145,8 @@ paths: $ref: "#/components/schemas/UpdateCustomMetricResponse" "401": *a1 "403": *a9 - "404": *a43 - "409": *a44 + "404": *a45 + "409": *a46 "422": *a4 "429": *a5 delete: @@ -20146,7 +20170,7 @@ paths: $ref: "#/components/schemas/DeleteCustomMetricResponse" "401": *a1 "403": *a9 - "404": *a43 + "404": *a45 "422": *a4 "429": *a5 /api/custom-metrics/{id}/entries: @@ -20189,8 +20213,8 @@ paths: schema: $ref: "#/components/schemas/ListCustomMetricEntriesEnvelope" "401": *a1 - "403": *a12 - "404": *a43 + "403": *a15 + "404": *a45 "422": *a4 "429": *a5 post: @@ -20220,10 +20244,10 @@ paths: $ref: "#/components/schemas/CreateCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": *a43 + "404": *a45 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/custom-metrics/{id}/entries/{entryId}: patch: tags: @@ -20257,7 +20281,7 @@ paths: $ref: "#/components/schemas/UpdateCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": &a45 + "404": &a47 description: Custom metric entry not found (or owned by another user). content: application/json: @@ -20291,7 +20315,7 @@ paths: $ref: "#/components/schemas/DeleteCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": *a45 + "404": *a47 "422": *a4 "429": *a5 /api/custom-metrics/{id}/entries/restore: @@ -20599,7 +20623,7 @@ paths: schema: $ref: "#/components/schemas/NutrientCatalogEnvelope" "401": *a1 - "403": &a46 + "403": &a48 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 @@ -20748,7 +20772,7 @@ paths: schema: $ref: "#/components/schemas/NutrientOverviewEnvelope" "401": *a1 - "403": *a46 + "403": *a48 "422": *a4 "429": *a5 /api/nutrients/daily: @@ -20785,7 +20809,7 @@ paths: schema: $ref: "#/components/schemas/NutrientDailySeriesEnvelope" "401": *a1 - "403": *a46 + "403": *a48 "422": *a4 "429": *a5 /api/nutrients/water: @@ -20814,7 +20838,7 @@ paths: schema: $ref: "#/components/schemas/NutrientWaterWriteEnvelope" "401": *a1 - "403": *a46 + "403": *a48 "409": *a7 "422": *a4 "429": *a5 @@ -21335,7 +21359,7 @@ paths: application/json: schema: $ref: "#/components/schemas/WithingsCredentialsUpdateEnvelope" - "400": &a47 + "400": &a49 description: Body is not parseable JSON. content: application/json: @@ -21343,19 +21367,19 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "413": &a48 + "413": &a50 description: Body exceeds 64 KiB. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "415": &a49 + "415": &a51 description: Content-Type is not `application/json`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": &a50 + "422": &a52 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: @@ -21546,12 +21570,12 @@ paths: application/json: schema: $ref: "#/components/schemas/FitbitCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -21706,12 +21730,12 @@ paths: application/json: schema: $ref: "#/components/schemas/GoogleHealthCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -21877,12 +21901,12 @@ paths: application/json: schema: $ref: "#/components/schemas/OuraCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -21962,21 +21986,21 @@ paths: $ref: "#/components/schemas/OuraTestEnvelope" "401": *a1 "403": *a9 - "422": &a51 + "422": &a53 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": &a52 + "429": &a54 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": &a53 + "502": &a55 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` @@ -22022,12 +22046,12 @@ paths: application/json: schema: $ref: "#/components/schemas/PolarCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -22107,9 +22131,9 @@ paths: $ref: "#/components/schemas/PolarTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/strava/credentials: get: tags: @@ -22146,12 +22170,12 @@ paths: application/json: schema: $ref: "#/components/schemas/StravaCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -22233,9 +22257,9 @@ paths: $ref: "#/components/schemas/StravaTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/status: get: tags: @@ -22273,7 +22297,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/fitbit/test: post: tags: @@ -22299,9 +22323,9 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/google-health/resume: post: tags: @@ -22319,7 +22343,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/google-health/test: post: tags: @@ -22353,9 +22377,9 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/whoop/resume: post: tags: @@ -22373,7 +22397,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/whoop/test: post: tags: @@ -22391,9 +22415,9 @@ paths: $ref: "#/components/schemas/WhoopIntegrationTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/withings/resume: post: tags: @@ -22412,7 +22436,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/withings/test: post: tags: @@ -22431,8 +22455,8 @@ paths: $ref: "#/components/schemas/WithingsIntegrationTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 + "422": *a53 + "429": *a54 "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: @@ -22465,7 +22489,7 @@ paths: schema: $ref: "#/components/schemas/DailyDigestEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/daily/digest/dismiss: @@ -22558,7 +22582,7 @@ paths: schema: $ref: "#/components/schemas/DashboardSummaryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/dashboard/widgets: @@ -22577,7 +22601,7 @@ paths: schema: $ref: "#/components/schemas/DashboardLayout" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 put: @@ -22612,7 +22636,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 delete: tags: @@ -22850,7 +22874,7 @@ paths: schema: $ref: "#/components/schemas/RecordActivityEnvelope" "401": *a1 - "403": &a54 + "403": &a56 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 @@ -22877,7 +22901,7 @@ paths: schema: $ref: "#/components/schemas/AccountGrantListEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "422": *a4 "429": *a5 post: @@ -22981,7 +23005,7 @@ paths: schema: $ref: "#/components/schemas/AcceptedAccountGrantEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "404": description: No such invitation for this account. content: @@ -23025,7 +23049,7 @@ paths: schema: $ref: "#/components/schemas/RevokedAccountGrantEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "404": description: No such grant on this account's record. content: @@ -23066,7 +23090,7 @@ paths: schema: $ref: "#/components/schemas/RenouncedAccountGrantEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "404": description: No such grant held by this account. content: @@ -23458,7 +23482,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "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 — @@ -23847,7 +23871,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectionListEnvelope" "401": *a1 - "403": &a55 + "403": &a57 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 @@ -23881,7 +23905,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectionRevokeEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "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. @@ -23908,7 +23932,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectorTokenListEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "422": *a4 "429": *a5 post: @@ -23941,7 +23965,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "413": description: Body exceeds 16 KiB. content: @@ -23984,7 +24008,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectorTokenRevokeEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "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. @@ -24011,7 +24035,7 @@ paths: schema: $ref: "#/components/schemas/RecordSettingsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/record-settings/integrations: @@ -24030,7 +24054,7 @@ paths: schema: $ref: "#/components/schemas/RecordSettingsIntegrationsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/record-settings/integrations/{provider}/connect: @@ -24046,7 +24070,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: - - &a56 + - &a58 name: provider in: path required: true @@ -24068,7 +24092,7 @@ paths: schema: $ref: "#/components/schemas/ManagedConnectStartEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: The path named a provider outside the closed set. Answered before the caller is authenticated. content: @@ -24093,7 +24117,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: - - *a56 + - *a58 responses: "200": description: The provider is disconnected. @@ -24102,7 +24126,7 @@ paths: schema: $ref: "#/components/schemas/ManagedDisconnectEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: The path named a provider outside the closed set, or the record has no connection to it. content: @@ -24119,7 +24143,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: - - &a57 + - &a59 name: family in: path required: true @@ -24143,7 +24167,7 @@ paths: schema: $ref: "#/components/schemas/GetRecordSettingsFamilyEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Either the path named a family outside the closed set, or the record row itself is gone. The two are not distinguished, and the first is answered before the caller is authenticated at all. @@ -24163,7 +24187,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: - - *a57 + - *a59 requestBody: required: true content: @@ -24184,7 +24208,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: The path named a family outside the closed set. Answered before authentication. content: @@ -24298,7 +24322,7 @@ paths: schema: $ref: "#/components/schemas/ListPersonalRecordsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": description: "`metricType` or `limit` carried an unacceptable value. `meta.errorCode` = `personal_records.invalid_query`, with the offending path under `details.issues`." @@ -24351,7 +24375,7 @@ paths: schema: $ref: "#/components/schemas/AchievementsEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/environment/preferences: @@ -24377,7 +24401,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentPreferencesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment: @@ -24395,7 +24419,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentOverviewEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment/home: @@ -24432,7 +24456,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentHomeEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "413": description: Body exceeds 4 KiB. content: @@ -24492,7 +24516,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentBackfillEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: No home location is set, so there is nothing to resolve the days against (`meta.errorCode` = `environment.no_home`), or a backfill for this account is already queued in the current twenty-minute slot @@ -24552,7 +24576,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentGeocodeEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": description: "`q` was missing, empty after trimming, or longer than 120 characters. `meta.errorCode` = `environment.invalid`." @@ -24591,7 +24615,7 @@ paths: schema: $ref: "#/components/schemas/CreateEnvironmentTravelEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "413": description: Body exceeds 4 KiB. content: @@ -24644,7 +24668,7 @@ paths: schema: $ref: "#/components/schemas/DeleteEnvironmentTravelEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "404": description: No such override for this account. `meta.errorCode` = `environment.travel.not_found`. Also the answer for an id owned by somebody else — existence is not leaked. @@ -24790,13 +24814,13 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupDocument" - "403": &a58 + "403": &a60 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": &a59 + "404": &a61 description: No stored copy with this id. content: application/json: @@ -24870,8 +24894,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "403": *a58 - "404": *a59 + "403": *a60 + "404": *a61 "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 @@ -24915,7 +24939,7 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupRestoreJobListResponse" - "403": *a58 + "403": *a60 /api/admin/backups/restores/{jobId}: get: tags: @@ -24938,7 +24962,7 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupRestoreJobResponse" - "403": *a58 + "403": *a60 "404": description: No restore job with this id. content: @@ -24957,7 +24981,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: &a60 + security: &a62 - cookieAuth: [] responses: "200": @@ -24967,7 +24991,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourcesStatusEnvelope" "401": *a1 - "403": &a61 + "403": &a63 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 @@ -24992,9 +25016,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: *a60 + security: *a62 parameters: - - &a62 + - &a64 name: system in: path required: true @@ -25033,8 +25057,8 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceConnectionEnvelope" "401": *a1 - "403": *a61 - "404": &a63 + "403": *a63 + "404": &a65 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 @@ -25053,7 +25077,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "502": &a64 + "502": &a66 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 @@ -25072,9 +25096,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: *a60 + security: *a62 parameters: - - *a62 + - *a64 responses: "200": description: Removed, or there was nothing to remove. @@ -25083,7 +25107,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceDisconnectEnvelope" "401": *a1 - "403": *a61 + "403": *a63 "422": *a4 "429": *a5 /api/documents/sources/{system}/test: @@ -25094,9 +25118,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: *a60 + security: *a62 parameters: - - *a62 + - *a64 responses: "200": description: The source answered. @@ -25105,9 +25129,9 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceTestEnvelope" "401": *a1 - "403": *a61 - "404": *a63 - "422": &a65 + "403": *a63 + "404": *a65 + "422": &a67 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." @@ -25116,7 +25140,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "502": *a64 + "502": *a66 /api/documents/sources/{system}/tags: get: tags: @@ -25124,9 +25148,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: *a60 + security: *a62 parameters: - - *a62 + - *a64 responses: "200": description: The tags. @@ -25135,11 +25159,11 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceTagsEnvelope" "401": *a1 - "403": *a61 - "404": *a63 - "422": *a65 + "403": *a63 + "404": *a65 + "422": *a67 "429": *a5 - "502": *a64 + "502": *a66 /api/documents/sources/{system}/search: get: tags: @@ -25153,9 +25177,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: *a60 + security: *a62 parameters: - - *a62 + - *a64 - name: q in: query required: false @@ -25196,11 +25220,11 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceSearchEnvelope" "401": *a1 - "403": *a61 - "404": *a63 - "422": *a65 + "403": *a63 + "404": *a65 + "422": *a67 "429": *a5 - "502": *a64 + "502": *a66 /api/documents/sources/{system}/import: post: tags: @@ -25219,9 +25243,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: *a60 + security: *a62 parameters: - - *a62 + - *a64 requestBody: required: true content: @@ -25279,7 +25303,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceImportEnvelope" "401": *a1 - "403": *a61 + "403": *a63 "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. @@ -25310,7 +25334,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a65 + "422": *a67 "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`)." @@ -25318,7 +25342,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "502": *a64 + "502": *a66 /api/symptoms/definitions: get: tags: @@ -25342,7 +25366,7 @@ paths: schema: $ref: "#/components/schemas/ListSymptomDefinitionsEnvelope" "401": *a1 - "403": &a66 + "403": &a68 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 @@ -25386,7 +25410,7 @@ paths: schema: $ref: "#/components/schemas/CreateSymptomDefinitionEnvelope" "401": *a1 - "403": *a66 + "403": *a68 "409": *a7 "422": *a4 "429": *a5 @@ -25416,8 +25440,8 @@ paths: schema: $ref: "#/components/schemas/UpdateSymptomDefinitionEnvelope" "401": *a1 - "403": *a66 - "404": &a67 + "403": *a68 + "404": &a69 description: "`symptoms.definition.notFound`: no definition with this id on the record." content: application/json: @@ -25451,8 +25475,8 @@ paths: schema: $ref: "#/components/schemas/DeleteSymptomDefinitionEnvelope" "401": *a1 - "403": *a66 - "404": *a67 + "403": *a68 + "404": *a69 "422": *a4 "429": *a5 /api/symptoms/events: @@ -25501,7 +25525,7 @@ paths: schema: $ref: "#/components/schemas/ListSymptomEventsEnvelope" "401": *a1 - "403": *a66 + "403": *a68 "422": *a4 "429": *a5 post: @@ -25527,8 +25551,8 @@ paths: schema: $ref: "#/components/schemas/CreateSymptomEventEnvelope" "401": *a1 - "403": *a66 - "404": *a67 + "403": *a68 + "404": *a69 "409": *a7 "422": *a4 "429": *a5 @@ -25558,8 +25582,8 @@ paths: schema: $ref: "#/components/schemas/UpdateSymptomEventEnvelope" "401": *a1 - "403": *a66 - "404": &a68 + "403": *a68 + "404": &a70 description: "`symptoms.event.notFound`: no occurrence with this id on the record." content: application/json: @@ -25586,8 +25610,8 @@ paths: schema: $ref: "#/components/schemas/DeleteSymptomEventEnvelope" "401": *a1 - "403": *a66 - "404": *a68 + "403": *a68 + "404": *a70 "422": *a4 "429": *a5 /api/day/index: @@ -25620,7 +25644,7 @@ paths: schema: $ref: "#/components/schemas/DayIndexEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/day/notable: @@ -25655,7 +25679,7 @@ paths: schema: $ref: "#/components/schemas/DayNotableWindowEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/day/{date}: @@ -25685,7 +25709,7 @@ paths: schema: $ref: "#/components/schemas/DayEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/timeline: @@ -25738,7 +25762,7 @@ paths: schema: $ref: "#/components/schemas/TimelineEnvelope" "401": *a1 - "403": &a69 + "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: `module.disabled` with @@ -25777,7 +25801,7 @@ paths: schema: $ref: "#/components/schemas/TimelineReadinessEnvelope" "401": *a1 - "403": *a69 + "403": *a71 "422": *a4 "429": *a5 /api/life-events: @@ -25795,7 +25819,7 @@ paths: schema: $ref: "#/components/schemas/LifeEventListEnvelope" "401": *a1 - "403": &a70 + "403": &a72 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 @@ -25837,10 +25861,10 @@ paths: schema: $ref: "#/components/schemas/CreateLifeEventEnvelope" "401": *a1 - "403": *a70 + "403": *a72 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/life-events/{id}: patch: tags: @@ -25868,8 +25892,8 @@ paths: schema: $ref: "#/components/schemas/UpdateLifeEventEnvelope" "401": *a1 - "403": *a70 - "404": &a71 + "403": *a72 + "404": &a73 description: No such life event on this account. content: application/json: @@ -25896,8 +25920,8 @@ paths: schema: $ref: "#/components/schemas/DeleteLifeEventEnvelope" "401": *a1 - "403": *a70 - "404": *a71 + "403": *a72 + "404": *a73 "422": *a4 "429": *a5 /api/search: @@ -25943,7 +25967,7 @@ paths: schema: $ref: "#/components/schemas/SearchEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/auth/me/ai-usage: @@ -26072,7 +26096,7 @@ paths: required: - file responses: - "202": *a39 + "202": *a41 "401": *a1 "403": description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the @@ -26108,7 +26132,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "503": *a20 + "503": *a23 /api/anamnesis/life-circumstances: get: tags: @@ -26123,7 +26147,7 @@ paths: schema: $ref: "#/components/schemas/LifeCircumstancesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/anamnesis/life-circumstances/ai-sections: @@ -26212,7 +26236,7 @@ paths: schema: $ref: "#/components/schemas/AuditCCreatedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -26236,7 +26260,7 @@ paths: schema: $ref: "#/components/schemas/AuditCRemovedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: "`anamnesis.auditC.notFound`." content: @@ -26267,7 +26291,7 @@ paths: schema: $ref: "#/components/schemas/DawarichConnectEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/dawarich/status: @@ -26283,7 +26307,7 @@ paths: schema: $ref: "#/components/schemas/DawarichStatusEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/dawarich/places: @@ -26307,7 +26331,7 @@ paths: schema: $ref: "#/components/schemas/DawarichPlacesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: "`dawarich.not_connected`." content: @@ -26345,7 +26369,7 @@ paths: schema: $ref: "#/components/schemas/DawarichSettingsEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: "`dawarich.not_connected`." content: @@ -26377,7 +26401,7 @@ paths: schema: $ref: "#/components/schemas/DawarichProbeEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 "502": @@ -26413,7 +26437,7 @@ paths: schema: $ref: "#/components/schemas/DawarichSyncEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: "`dawarich.not_connected`." content: @@ -26448,7 +26472,7 @@ paths: schema: $ref: "#/components/schemas/DawarichDisconnectEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment/travel/suggestions: @@ -26466,7 +26490,7 @@ paths: schema: $ref: "#/components/schemas/TravelSuggestionsEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment/travel/suggestions/{id}/dismiss: @@ -26489,7 +26513,7 @@ paths: schema: $ref: "#/components/schemas/TravelSuggestionDismissEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "404": description: "`environment.travel_suggestion.not_found`." content: diff --git a/src/lib/openapi/routes/health-record.ts b/src/lib/openapi/routes/health-record.ts index 303055b89..ca515a797 100644 --- a/src/lib/openapi/routes/health-record.ts +++ b/src/lib/openapi/routes/health-record.ts @@ -15,10 +15,44 @@ import { import { dataEnvelope, errorEnvelope, + moduleDisabledResponse, recordRefusal, stdResponses, } from "./shared"; +// The FHIR face answers its own refusals as FHIR: a 400, 404 or 429 raised +// by a `/api/fhir/*` handler is an `OperationOutcome` served as +// `application/fhir+json` (`operationOutcome` in `@/lib/fhir/rest`), which is +// what a FHIR client parses on any non-2xx. The refusals that run before the +// handler's own body are the API's: the generic 401 / 403 envelopes and the +// 403 `module.disabled`, which keeps the errorCode every client already +// branches on for a disabled module. Declared here after `...stdResponses`, +// so the FHIR shapes replace the generic JSON ones. +const fhirOutcomeContent = { + "application/fhir+json": { + schema: z.string().meta({ format: "binary" }), + }, +}; + +const fhirDataRefusals = { + ...moduleDisabledResponse, + "429": { + description: + "Rate limit exceeded: the per-account `fhir:` bucket, 120 requests per hour, shared by every `/api/fhir/*` data route. The body is an `OperationOutcome` (`issue[0].code` = `throttled`) served as `application/fhir+json`, not the JSON error envelope. The `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers describe the bucket; back off on `Retry-After`.", + content: fhirOutcomeContent, + headers: stdResponses["429"].headers, + }, +}; + +const fhirSearchRefusals = { + ...fhirDataRefusals, + "400": { + description: + "A search parameter the server cannot honour, such as an `_offset` above 9007199254740991. The body is an `OperationOutcome` (`issue[0].code` = `invalid`, the reason in `diagnostics`) served as `application/fhir+json`.", + content: fhirOutcomeContent, + }, +}; + // The two request bodies this module publishes. `.meta()` CLONES in Zod 4 // rather than annotating in place, so the returned schema has to be captured // and referenced: a bare `schema.meta({...})` statement registers nothing and @@ -394,6 +428,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -426,6 +461,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -456,6 +492,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -486,6 +523,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -516,6 +554,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -546,6 +585,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -576,6 +616,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -606,6 +647,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -636,6 +678,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -666,6 +709,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -696,6 +740,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, diff --git a/tests/integration/routes/fhir-module-gate.test.ts b/tests/integration/routes/fhir-module-gate.test.ts index e018e8f66..fc5363f74 100644 --- a/tests/integration/routes/fhir-module-gate.test.ts +++ b/tests/integration/routes/fhir-module-gate.test.ts @@ -10,9 +10,6 @@ * record leaves; the static CapabilityStatement carries no user data and * stays reachable. The module is switched the way the settings switch it, * through the stored per-account preferences. - * - * The registry declares no 403 for these operations; the refusals below skip - * the contract check for that reason. */ import { beforeEach, describe, expect, it } from "vitest"; @@ -45,9 +42,6 @@ import { getPrismaClient, truncateAllTables } from "../setup"; const prisma = getPrismaClient(); const INSURANCE_NUMBER = "A123456789"; -const MODULE_OFF_SKIP = - "the FHIR operations declare no 403; the module gate's module.disabled envelope is undocumented on them"; - type Handler = Parameters[0]; /** Every data route, by the path a caller hits; the reads name a held id. */ @@ -135,7 +129,7 @@ async function seedOwner(doctorReport: boolean) { return user; } -async function callEach(auth: RouteAuth, skip: boolean) { +async function callEach(auth: RouteAuth) { const results = []; for (const { handler, route, params } of DATA_ROUTES) { const res = await callRoute(handler, { @@ -143,7 +137,6 @@ async function callEach(auth: RouteAuth, skip: boolean) { route, params, auth, - contract: skip ? { skip: MODULE_OFF_SKIP } : undefined, }); results.push({ route, res }); } @@ -158,7 +151,7 @@ describe("/api/fhir/* and the doctorReport module", () => { await makeToken(owner, { permissions: [FHIR_READ_SCOPE] }), await makeSession(owner), ]) { - for (const { route, res } of await callEach(auth, true)) { + for (const { route, res } of await callEach(auth)) { expect(res.status, route).toBe(403); expect(res.data, route).toBeNull(); expect(res.meta, route).toMatchObject({ @@ -177,7 +170,7 @@ describe("/api/fhir/* and the doctorReport module", () => { const owner = await seedOwner(true); const auth = await makeToken(owner, { permissions: [FHIR_READ_SCOPE] }); - for (const { route, res } of await callEach(auth, false)) { + for (const { route, res } of await callEach(auth)) { expect(res.status, route).toBe(200); expect(res.headers.get("content-type"), route).toContain( "application/fhir+json", diff --git a/tests/integration/routes/fhir.test.ts b/tests/integration/routes/fhir.test.ts index 03133023c..a094784b8 100644 --- a/tests/integration/routes/fhir.test.ts +++ b/tests/integration/routes/fhir.test.ts @@ -758,9 +758,6 @@ describe("FHIR paging", () => { route: "/api/fhir/Observation", query: { _offset: "1e20" }, auth, - contract: { - skip: "the registry declares no 400 on the FHIR searches, though the description names the 400 OperationOutcome", - }, }); expect(res.status).toBe(400); @@ -784,12 +781,11 @@ describe("FHIR paging", () => { method: "GET", route: "/api/fhir/Patient", auth, - contract: { - skip: "the registry declares the shared 429 as an application/json error envelope; the FHIR routes answer an application/fhir+json OperationOutcome", - }, }); expect(res.status).toBe(429); + expect(res.headers.get("content-type")).toContain("application/fhir+json"); + expect(Number(res.headers.get("retry-after"))).toBeGreaterThanOrEqual(1); expect(outcomeSchema.parse(res.body).issue[0].code).toBe("throttled"); expect(JSON.stringify(res.body)).not.toContain("Sample Patient"); });