mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
Merge branch 'fix/v1437-data' into release/v1.43.7
This commit is contained in:
commit
dee6a27ae4
100 files changed
+2314
-506
No files matched your search
@@ -81,6 +81,7 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
|
||||
"auth.reproof.too_weak",
|
||||
"auth.scope.insufficient",
|
||||
"auth.session.insecure_transport",
|
||||
"auth.session.unavailable",
|
||||
"auth.stepup.required",
|
||||
"auth.token.expired",
|
||||
"auth.token.invalid",
|
||||
@@ -210,6 +211,25 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
|
||||
"health_score_config.invalid",
|
||||
"health_score_config.too_narrow",
|
||||
],
|
||||
http: [
|
||||
"http.bad_gateway",
|
||||
"http.bad_request",
|
||||
"http.client_error",
|
||||
"http.conflict",
|
||||
"http.forbidden",
|
||||
"http.gateway_timeout",
|
||||
"http.gone",
|
||||
"http.internal",
|
||||
"http.invalid",
|
||||
"http.method_not_allowed",
|
||||
"http.not_found",
|
||||
"http.payload_too_large",
|
||||
"http.rate_limited",
|
||||
"http.server_error",
|
||||
"http.unauthenticated",
|
||||
"http.unavailable",
|
||||
"http.unsupported_media_type",
|
||||
],
|
||||
illness: [
|
||||
"illness.day-log.invalid",
|
||||
"illness.disabled",
|
||||
|
||||
@@ -44,7 +44,12 @@ import {
|
||||
} from "@/lib/validations/managed-profile-handover";
|
||||
import { onboardingStateResource } from "./onboarding";
|
||||
import { aiAccountBlock, moduleAccessMap } from "./profile";
|
||||
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
import {
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
/**
|
||||
@@ -877,7 +882,7 @@ export const accountSharingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The activity, and how far back it can see.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
recordActivityResponse,
|
||||
"RecordActivityEnvelope",
|
||||
),
|
||||
@@ -900,7 +905,7 @@ export const accountSharingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Grants given and received.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
grantListResponse,
|
||||
"AccountGrantListEnvelope",
|
||||
),
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
@@ -97,7 +98,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The caller's allergy records.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(z.array(allergy), "ListAllergiesEnvelope"),
|
||||
schema: listEnvelope(z.array(allergy), "ListAllergiesEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
errorEnvelope,
|
||||
loginPasswordSchema,
|
||||
validationIssue,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { passkeyLoginOptionsSchema } from "@/lib/validations/auth";
|
||||
import {
|
||||
@@ -1724,7 +1725,7 @@ export const authPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Recent security events for the caller.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
securityActivityResponse,
|
||||
"SecurityActivityEnvelope",
|
||||
),
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
measurementTypeEnum,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
@@ -190,7 +191,7 @@ export const awardsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The record's personal bests, newest first.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.array(personalRecord),
|
||||
"ListPersonalRecordsEnvelope",
|
||||
),
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
@@ -954,7 +955,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Cycle history.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
cycleHistoryResponse,
|
||||
"CycleHistoryEnvelope",
|
||||
),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
|
||||
@@ -88,7 +89,7 @@ export const familyHistoryPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The caller's family-history entries.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.array(familyHistoryEntry),
|
||||
"ListFamilyHistoryEnvelope",
|
||||
),
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
|
||||
@@ -298,7 +299,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The caller's illness episodes.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.array(illnessEpisode),
|
||||
"ListIllnessEpisodesEnvelope",
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "../shared";
|
||||
import {
|
||||
aiCheckFailedResponse,
|
||||
@@ -892,7 +893,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The ECG recording list (possibly empty).",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(ecgListResponse, "EcgListResponseEnvelope"),
|
||||
schema: listEnvelope(ecgListResponse, "EcgListResponseEnvelope"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -985,7 +986,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "The device-flagged event timeline (possibly empty).",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
rhythmEventsResponse,
|
||||
"RhythmEventsResponseEnvelope",
|
||||
),
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
recordRefusal,
|
||||
recordWriteRateLimitResponse,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "../shared";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
@@ -596,7 +597,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
"The entries, plus a `meta.total` that counts THIS PAGE rather than the whole log — it is the returned array's length, so it can never exceed `limit`.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
z.object({
|
||||
items: z.array(medicationSideEffect),
|
||||
meta: z.object({ total: z.number().int().nonnegative() }),
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
idempotentWrite,
|
||||
recordRefusal,
|
||||
stdResponses,
|
||||
listEnvelope,
|
||||
} from "./shared";
|
||||
|
||||
// Zod 4's `.meta()` returns a NEW instance carrying the id rather than
|
||||
@@ -121,7 +122,7 @@ export const mentalHealthPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
description: "Screener history.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
schema: listEnvelope(
|
||||
listResponse,
|
||||
"ListMentalHealthAssessmentsEnvelope",
|
||||
),
|
||||
|
||||
@@ -85,7 +85,7 @@ export const errorEnvelope = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Stable machine code for this refusal. Branch on it rather than on `error`, which is prose and may be reworded. Every code the API emits today is enumerated below, grouped by the surface that emits it; four naming conventions coexist and none of them will be renamed, because a code is a wire value a shipped client branches on. Treat an unlisted code the way you would treat an unlisted enum member — as a refusal you do not recognise, not as a malformed response — since the list grows with the surfaces. Three families are outside it on purpose: `assistant.disabled.<switch>` is built from a template so the last segment is open — it is one of `coach`, `briefing`, `insightStatus`, `documentAi` and `enabled`, and `assistant.disabled.coach` is the one the native client names; the integration-probe classes (`credentials_rejected`, `rate_limited`, `upstream_error`, `timeout`, `connection_failed` and the per-provider additions) are enumerated in each `/test` operation instead, where the differences can be stated; and a 401 raised by a route checking a credential of its own may carry no code at all. " +
|
||||
"Stable machine code for this refusal. Branch on it rather than on `error`, which is prose and may be reworded. Every code the API emits today is enumerated below, grouped by the surface that emits it; four naming conventions coexist and none of them will be renamed, because a code is a wire value a shipped client branches on. Every error envelope carries a code: when the route has no reason of its own to name, the code is the status's generic one from the `http` family (`http.not_found` for a 404, `http.internal` for a 500, `http.client_error` / `http.server_error` for a status without its own entry), so `http.*` says which kind of refusal it was and never why. Treat an unlisted code the way you would treat an unlisted enum member — as a refusal you do not recognise, not as a malformed response — since the list grows with the surfaces. Three families are outside it on purpose: `assistant.disabled.<switch>` is built from a template so the last segment is open — it is one of `coach`, `briefing`, `insightStatus`, `documentAi` and `enabled`, and `assistant.disabled.coach` is the one the native client names; the integration-probe classes (`credentials_rejected`, `rate_limited`, `upstream_error`, `timeout`, `connection_failed` and the per-provider additions) are enumerated in each `/test` operation instead, where the differences can be stated; and a 401 raised by a route checking a credential of its own carries the generic `http.unauthenticated`. " +
|
||||
renderErrorCodeCatalogue(),
|
||||
),
|
||||
capability: z
|
||||
@@ -118,7 +118,7 @@ export const errorEnvelope = z
|
||||
.meta({
|
||||
id: "ErrorEnvelope",
|
||||
description:
|
||||
"Standard error response: `data` is null, `error` is human prose. `meta.errorCode` carries the stable machine code where one exists, and `meta` may carry further per-refusal context. A 422 from the multi-issue validator additionally carries `details.issues` — every field the request got wrong, so a client can fix them in one round-trip.",
|
||||
"Standard error response: `data` is null, `error` is human prose. `meta.errorCode` always carries a stable machine code — the route's own where it names one, otherwise the status's generic `http.*` code — `meta.requestId` names the request for a support conversation, and `meta` may carry further per-refusal context. A 422 from the multi-issue validator additionally carries `details.issues` — every field the request got wrong, so a client can fix them in one round-trip.",
|
||||
});
|
||||
|
||||
export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
@@ -131,6 +131,42 @@ export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
.meta({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
* The success envelope of a list the server cuts at a ceiling (see
|
||||
* `src/lib/api/list-cap.ts`). `data` is the route's own payload, unchanged;
|
||||
* `meta.truncated` says whether rows past `meta.limit` exist.
|
||||
*/
|
||||
export const listCapMeta = z
|
||||
.object({
|
||||
requestId: z.string().optional(),
|
||||
truncated: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"True when more rows exist than this response carries: the list was cut at `limit`, newest or most relevant first as the operation states. False means the list is complete. Show the person that the list is partial rather than letting the last row read as the oldest there is.",
|
||||
),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(10_000)
|
||||
.describe("The most rows this response could carry."),
|
||||
})
|
||||
.meta({
|
||||
id: "ListCapMeta",
|
||||
description:
|
||||
"How a capped list was cut. Present on every response of an operation that declares it.",
|
||||
});
|
||||
|
||||
export function listEnvelope<T extends z.ZodType>(payload: T, id: string) {
|
||||
return z
|
||||
.object({
|
||||
data: payload,
|
||||
error: z.null(),
|
||||
meta: listCapMeta,
|
||||
})
|
||||
.meta({ id });
|
||||
}
|
||||
|
||||
// ── Schemas — annotated for spec emission ────────────────────────────
|
||||
//
|
||||
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
|
||||
@@ -279,7 +315,7 @@ export const malformedJsonResponse = {
|
||||
export const stdResponses = {
|
||||
"401": {
|
||||
description:
|
||||
"Authentication required or invalid credentials. The generic auth gates — the ones every route passes through before its own body runs — name the reason in `meta.errorCode`, and that is the field to branch on there; the English sentence beside it may be reworded or localised at any time. `auth.missing`: no session cookie and no Bearer header; send the user to sign in. `auth.token.expired`: the Bearer token's lifetime has passed; refresh and retry the request. `auth.token.invalid`: unknown, revoked, or the owning account is gone; discard the credential and sign in again — retrying will not help. `auth.mfa.code_invalid`: a second factor presented on a step-up flow did not verify; re-prompt for a code and keep the session. The related 403 refusals carry `auth.scope.insufficient` (the credential is valid but its scope does not reach this route, including any Bearer token against an admin surface) and `auth.admin.required` (a cookie session that is not an admin's). A route that checks a credential of its own — sign-in, the MFA challenge exchange, a password confirmation — may still answer 401 with prose and no code, so a missing `meta.errorCode` means the route refused the credential it was handed, not that the response is malformed.",
|
||||
"Authentication required or invalid credentials. The generic auth gates — the ones every route passes through before its own body runs — name the reason in `meta.errorCode`, and that is the field to branch on there; the English sentence beside it may be reworded or localised at any time. `auth.missing`: no session cookie and no Bearer header; send the user to sign in. `auth.token.expired`: the Bearer token's lifetime has passed; refresh and retry the request. `auth.token.invalid`: unknown, revoked, or the owning account is gone; discard the credential and sign in again — retrying will not help. `auth.mfa.code_invalid`: a second factor presented on a step-up flow did not verify; re-prompt for a code and keep the session. The related 403 refusals carry `auth.scope.insufficient` (the credential is valid but its scope does not reach this route, including any Bearer token against an admin surface) and `auth.admin.required` (a cookie session that is not an admin's). A route that checks a credential of its own — sign-in, the MFA challenge exchange, a password confirmation — may answer 401 with the generic `http.unauthenticated`, which means the route refused the credential it was handed. A session that could not be checked because the session store was unreachable is NOT a 401: it answers 503 with `auth.session.unavailable`, and the client keeps its credential and retries.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
"422": {
|
||||
|
||||
Reference in new issue
Block a user