feat: lay down the shared v1.42 contract

Land every file the v1.42 work shares before any of it is built, so the
parts built side by side never edit the same file.

Schema and migrations 0379-0386: the HEALTH_CONNECT measurement source,
ImportJob.kind, the air-quality columns and the per-user switch on the
environment tables, sealed environment locations with nullable plaintext,
managed-profile handover links, the per-type HealthKit arrival ledger, lab
unit-conversion provenance, and user-defined vaccines. Every new column is
annotated for the column-reader sweep.

HEALTH_CONNECT reaches every exhaustive list: the source labels, the
read enum (not the write allowlist), the source-priority ladder cap, now
the number of sources rather than a fixed eight, and the write-path smoke
test. RMSSD gets its own priority class and a 300 ms ceiling. The batch
syncTrigger gains `manual`, with its label on the Apple Health card.

Queues for the Health Connect import, the compaction-tombstone purge and
the measurement-table maintenance are registered with stub handlers;
host-metric sampling moves to every five minutes. Environment and work_mem
variables reach the compose whitelist, the production example and the
manifest. The proxy admits the handover link and redirects it at the edge,
and the Health Connect upload bypasses the proxy body limit.

The new routes answer 501 behind their final authentication and are
published in the contract; backup, wipe and key rotation classify the new
models and columns; the seven message bundles carry the base copy. Guards
that list release-pending keys and columns fail once an entry is wired, so
those lists empty themselves before the tag.

Also re-anchor a login-throttle fixture to the wall clock, where its
expiry is compared, and re-base the aggregate bundle cap on a measured
12 KB of new locale strings.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 07:56:59 +02:00
1 parent 125b15bfb7
commit e63be67b5a
82 files changed
+5099 -727

No files matched your search

+1
View File
@@ -403,6 +403,7 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
"modules_conflict",
"mood_tag_layout_conflict",
"not_configured",
"not_implemented",
"notification_prefs_conflict",
"oidc_only",
"private_origin_not_approved",
+112 -1
View File
@@ -38,7 +38,12 @@ import {
} from "@/lib/validations/managed-profiles";
import { onboardingStateResource } from "./onboarding";
import { aiAccountBlock, moduleAccessMap } from "./profile";
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
import {
dataEnvelope,
errorEnvelope,
notImplementedResponse,
stdResponses,
} from "./shared";
/**
* The four request bodies this table publishes, and the one thing to know about
@@ -545,7 +550,113 @@ const managedProfileEnvelope = dataEnvelope(
"ManagedProfileEnvelope",
);
// v1.42 (#959) — handing a managed profile over to the person it describes.
const handoverProposal = z
.enum(["end", "read", "manage"])
.describe(
"What a guardian's access becomes after the handover: ended, view only, or still managing. The new owner confirms or changes it when claiming.",
)
.meta({ id: "ManagedProfileHandoverProposal" });
const createHandoverRequest = z
.object({
expiresInDays: z
.union([z.literal(1), z.literal(7), z.literal(14)])
.default(7)
.describe("How long the link stays valid."),
proposals: z
.array(
z.object({
grantId: z.string().describe("The guardian's MANAGE grant."),
proposal: handoverProposal,
}),
)
.describe(
"One proposal per active guardian. A guardian left out is proposed `read`.",
),
})
.strict()
.meta({ id: "CreateManagedProfileHandoverRequest" });
const managedProfileHandover = z
.object({
id: z.string(),
token: z
.string()
.describe(
"The one-time `hlp_` token. Returned only in this response and never again; only its hash is stored.",
),
url: z.string().describe("The link to hand over: `<app>/claim/<token>`."),
expiresAt: z.iso.datetime({ offset: true }),
})
.meta({ id: "ManagedProfileHandover" });
export const accountSharingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/managed-profiles/{id}/handover": {
post: {
tags: ["Account sharing"],
summary: "Create a handover link for a managed profile",
description:
"Mints the one-time link that lets the person a managed profile describes take it over as their own account. No data moves: claiming sets credentials on the profile's own record. Each guardian's access afterwards follows the proposal recorded here unless the new owner changes it. Creating a new link withdraws an open one. Pending MANAGE invitations on the profile are withdrawn when it is claimed. Cookie-only and step-up gated, like every act on a managed profile. Refused on an instance that allows single sign-on only.",
requestParams: { path: z.object({ id: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: createHandoverRequest } },
},
responses: {
...stdResponses,
"201": {
description: "The link, shown once.",
content: {
"application/json": {
schema: dataEnvelope(
managedProfileHandover,
"ManagedProfileHandoverEnvelope",
),
},
},
},
"403": {
description:
"The instance allows single sign-on only (`meta.errorCode: profile_claim.oidc_only_unsupported`).",
content: { "application/json": { schema: errorEnvelope } },
},
"404": {
description:
"No such managed profile, or the caller is not one of its Guardians (`meta.errorCode: managed_profile.not_found`).",
content: { "application/json": { schema: errorEnvelope } },
},
...notImplementedResponse,
},
},
delete: {
tags: ["Account sharing"],
summary: "Withdraw the open handover link",
description:
"Withdraws the profile's open handover link, if there is one; the link then answers like any unknown one. Cookie-only; no step-up, because withdrawing only takes something away.",
requestParams: { path: z.object({ id: z.string() }) },
responses: {
...stdResponses,
"200": {
description: "Withdrawn, or there was no open link.",
content: {
"application/json": {
schema: dataEnvelope(
z.object({ revoked: z.boolean() }),
"RevokeManagedProfileHandoverEnvelope",
),
},
},
},
"404": {
description:
"No such managed profile, or the caller is not one of its Guardians (`meta.errorCode: managed_profile.not_found`).",
content: { "application/json": { schema: errorEnvelope } },
},
...notImplementedResponse,
},
},
},
"/api/auth/me": {
get: {
tags: ["Auth"],
+71 -1
View File
@@ -9,7 +9,12 @@ import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { inviteCreateSchema } from "@/lib/validations/invite";
import { adminReminderCheckSchema } from "@/lib/validations/notifications";
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
import {
dataEnvelope,
errorEnvelope,
notImplementedResponse,
stdResponses,
} from "./shared";
// v1.4.48 H-APNs-1 — admin diagnostic endpoint for the notification
// subsystem. Mirrors the runtime types in
@@ -243,7 +248,72 @@ const adminReminderCheckResult = z
"What the sweep found and what it sent. `notificationsSent` counts DISPATCHES, not deliveries — each one enters the channel cascade and the per-channel verdict lands in the `push_attempts` ledger the notification diagnostic surfaces.",
});
// v1.42 — operator-triggered maintenance of the measurements table.
const adminMeasurementMaintenanceRequest = z
.object({
vacuum: z
.boolean()
.default(true)
.describe("Run `VACUUM (ANALYZE)` on `measurements`."),
reindex: z
.boolean()
.default(true)
.describe(
"Rebuild every index of `measurements` with `REINDEX INDEX CONCURRENTLY`, largest first, one at a time.",
),
})
.strict()
.meta({ id: "AdminMeasurementMaintenanceRequest" });
const adminMeasurementMaintenanceResult = z
.object({
enqueued: z
.boolean()
.describe(
"False when a maintenance run is already queued or running; nothing new was queued.",
),
})
.meta({ id: "AdminMeasurementMaintenanceResult" });
export const adminDiagnosticPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/admin/maintenance/measurements": {
post: {
tags: ["Admin"],
summary: "Queue measurement table maintenance",
description:
"Queues one `VACUUM (ANALYZE)` and `REINDEX INDEX CONCURRENTLY` pass over the `measurements` table, for after a large purge. Runs in the background worker, one statement at a time; pick a quiet window, because a rebuild needs free disk the size of the index it rebuilds. At most one run at a time. Cookie auth only: `requireAdmin()` refuses every Bearer caller. Runbook: `docs/ops/measurement-maintenance.md`.",
requestBody: {
required: false,
content: {
"application/json": { schema: adminMeasurementMaintenanceRequest },
},
},
responses: {
"202": {
description: "Queued, or already queued.",
content: {
"application/json": {
schema: dataEnvelope(
adminMeasurementMaintenanceResult,
"AdminMeasurementMaintenanceResponse",
),
},
},
},
"403": {
description: "Caller is not an admin.",
content: { "application/json": { schema: errorEnvelope } },
},
"503": {
description:
"No background worker is bound, so nothing would run it.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
},
"/api/admin/notifications/reminder-check": {
post: {
tags: ["Admin"],
+131
View File
@@ -46,6 +46,7 @@ import {
stdResponses,
errorEnvelope,
loginPasswordSchema,
notImplementedResponse,
} from "./shared";
import { passkeyLoginOptionsSchema } from "@/lib/validations/auth";
@@ -697,7 +698,137 @@ const codexDevicePollResponse = z
})
.meta({ id: "CodexDevicePollResponse" });
// v1.42 (#959) — claiming a managed profile through its handover link.
const claimTokenField = z
.string()
.describe(
"The `hlp_` token from the handover link. Sent in the body, never in a URL.",
);
const claimPreviewRequest = z
.object({ token: claimTokenField })
.strict()
.meta({ id: "ProfileClaimPreviewRequest" });
const claimPreview = z
.object({
displayName: z.string(),
expiresAt: z.iso.datetime({ offset: true }),
guardians: z.array(
z.object({
grantId: z.string(),
displayName: z.string(),
proposal: z.enum(["end", "read", "manage"]),
}),
),
})
.meta({ id: "ProfileClaimPreview" });
const claimRequest = z
.object({
token: claimTokenField,
username: z.string(),
email: z.string().describe("Required: the account needs a way back in."),
password: z.string(),
guardians: z
.array(
z.object({
grantId: z.string(),
decision: z.enum(["end", "read", "manage"]),
}),
)
.describe("The new owner's decision per guardian."),
})
.strict()
.meta({ id: "ProfileClaimRequest" });
export const authPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/auth/claim/preview": {
post: {
// No credential: the person claiming the profile has none yet. The
// one-time token in the body is the credential.
security: [],
tags: ["Auth"],
summary: "Preview a managed-profile handover link",
description:
"What a handover link would hand over: the profile's display name, the link's expiry, and each guardian's proposal. Anonymous; the token travels in the body. An unknown, expired, used or withdrawn token, and a link whose creator lost access, all answer the same 404. Rate-limited per source address.",
requestBody: {
required: true,
content: { "application/json": { schema: claimPreviewRequest } },
},
responses: {
"200": {
description: "The preview.",
content: {
"application/json": {
schema: dataEnvelope(claimPreview, "ProfileClaimPreviewEnvelope"),
},
},
},
"403": {
description:
"The instance allows single sign-on only (`meta.errorCode: profile_claim.oidc_only_unsupported`).",
content: { "application/json": { schema: errorEnvelope } },
},
"404": {
description:
"No usable link behind this token (`meta.errorCode: profile_claim.invalid`). Every failure class answers identically.",
content: { "application/json": { schema: errorEnvelope } },
},
"409": {
description:
"The request carries a signed-in session (`meta.errorCode: auth.already_authenticated`); sign out first.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
},
"/api/auth/claim": {
post: {
// No credential: see the preview above.
security: [],
tags: ["Auth"],
summary: "Claim a managed profile",
description:
"Turns the managed profile behind a handover link into the claimant's own account: sets the username, email and password on the profile's record, clears the managed marker, settles each guardian's access by the claimant's decision, withdraws pending MANAGE invitations, and resets the disclaimer, onboarding and AI consent so the new owner gives them personally. No data moves. Signs the new owner in. Anonymous; the token travels in the body, and every token failure answers the same 404.",
requestBody: {
required: true,
content: { "application/json": { schema: claimRequest } },
},
responses: {
"201": {
description: "Claimed; the response sets the session cookie.",
content: {
"application/json": {
schema: dataEnvelope(
z.object({ userId: z.string(), username: z.string() }),
"ProfileClaimEnvelope",
),
},
},
},
"403": {
description:
"The instance allows single sign-on only (`meta.errorCode: profile_claim.oidc_only_unsupported`).",
content: { "application/json": { schema: errorEnvelope } },
},
"404": {
description:
"No usable link behind this token (`meta.errorCode: profile_claim.invalid`).",
content: { "application/json": { schema: errorEnvelope } },
},
"409": {
description:
"The request carries a signed-in session (`meta.errorCode: auth.already_authenticated`), or the username or email is taken.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
},
"/api/auth/login": {
post: {
// No credential: this operation is reachable before one exists.
+52
View File
@@ -30,6 +30,7 @@ import {
dataEnvelope,
errorEnvelope,
moduleDisabledResponse,
notImplementedResponse,
stdResponses,
} from "./shared";
@@ -159,7 +160,58 @@ const geocodeResult = z
})
.meta({ id: "EnvironmentGeocodeResult" });
// v1.42 (#615) — the per-user environment switches.
const environmentPreferencesRequest = z
.object({
airQualityEnabled: z
.boolean()
.describe("Fetch and show air quality, pollen and UV for this account."),
})
.strict()
.meta({ id: "EnvironmentPreferencesRequest" });
const environmentPreferences = z
.object({
airQualityEnabled: z.boolean(),
operatorDisabled: z
.boolean()
.describe(
"True when the operator turned air quality off for the whole instance; the account switch then has no effect.",
),
})
.meta({ id: "EnvironmentPreferences" });
export const environmentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/environment/preferences": {
patch: {
tags: ["Environment"],
summary: "Change the environment switches",
description:
"Turns the air-quality part of the module on or off for this account (on by default). Off means the nightly fetch skips the air-quality feed and the overview leaves the fields out; days already stored are kept. Module-gated. Strict body.",
requestBody: {
required: true,
content: {
"application/json": { schema: environmentPreferencesRequest },
},
},
responses: {
"200": {
description: "The stored switches.",
content: {
"application/json": {
schema: dataEnvelope(
environmentPreferences,
"EnvironmentPreferencesEnvelope",
),
},
},
},
...moduleDisabledResponse,
...stdResponses,
...notImplementedResponse,
},
},
},
"/api/environment": {
get: {
tags: ["Environment"],
+118
View File
@@ -20,6 +20,7 @@ import {
dataEnvelope,
errorEnvelope,
measurementTypeEnum,
notImplementedResponse,
stdResponses,
} from "./shared";
@@ -177,7 +178,124 @@ const jsonImportStats = z
description: "Per-section outcome counts of a JSON import.",
});
// v1.42 (#972) — Android Health Connect export import.
const healthConnectImportJob = z
.object({
jobId: z.string(),
status: z
.string()
.describe(
"`queued`, `unpacking`, `parsing`, `upserting`, `done` or `failed`. A plain string, like the Apple Health status.",
),
startedAt: z.iso.datetime({ offset: true }),
completedAt: z.iso.datetime({ offset: true }).nullable(),
uploadBytes: z.number().int(),
progress: z
.record(z.string(), z.unknown())
.describe("Free-form phase progress. Shape is not part of the contract."),
result: z
.record(z.string(), z.unknown())
.nullable()
.describe("The worker's outcome summary, null until it finishes."),
failureReason: z.string().nullable(),
})
.meta({ id: "HealthConnectImportJob" });
export const importPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/import/health-connect-export": {
post: {
tags: ["Import"],
summary: "Upload a Health Connect export for background import",
description:
"Takes the ZIP the Android Health Connect app writes (it holds `health_connect_export.db`) and hands it to a background worker. The response is **202 with a `jobId`**; poll `GET /api/import/health-connect-export/status`. Body is `multipart/form-data` with the file under `file`, streamed to disk after authentication like the Apple Health upload. Re-uploading the same bytes resolves to the still-viable job and returns `idempotent: true`. Imported rows carry `source = HEALTH_CONNECT`. Cookie or wildcard Bearer.",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: z.object({
file: z
.string()
.describe(
"The Health Connect export ZIP, under the field name `file`.",
),
}),
},
},
},
responses: {
"202": {
description:
"Accepted and queued, or matched to a job already carrying these bytes.",
content: {
"application/json": {
schema: dataEnvelope(
z.object({
jobId: z.string(),
status: z.enum([
"queued",
"unpacking",
"parsing",
"upserting",
"done",
]),
idempotent: z.boolean().optional(),
}),
"HealthConnectImportKickoffEnvelope",
),
},
},
},
"400": {
description: "The request carried no body at all.",
content: { "application/json": { schema: errorEnvelope } },
},
"409": {
description:
"An import for this account is already queued or running with different bytes. The upload was discarded.",
content: { "application/json": { schema: errorEnvelope } },
},
"413": {
description: "The declared `Content-Length` exceeds the upload cap.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
"422": {
description:
"The upload is not a Health Connect export the importer can read: no `file` part, no export database in the ZIP, or a database version older than the importer supports.",
content: { "application/json": { schema: errorEnvelope } },
},
"503": {
description:
"No background worker is bound, so nothing would run the import.",
content: { "application/json": { schema: errorEnvelope } },
},
...notImplementedResponse,
},
},
},
"/api/import/health-connect-export/status": {
get: {
tags: ["Import"],
summary: "The caller's latest Health Connect import",
description:
"The most recent Health Connect import job of the caller, or `null` when there has never been one.",
responses: {
"200": {
description: "The latest job, or null.",
content: {
"application/json": {
schema: dataEnvelope(
z.object({ job: healthConnectImportJob.nullable() }),
"HealthConnectImportStatusEnvelope",
),
},
},
},
...stdResponses,
...notImplementedResponse,
},
},
},
"/api/import/csv": {
post: {
tags: ["Import"],
+1 -1
View File
@@ -49,7 +49,7 @@ const healthKitDirectionEnum = z
// Mirror the batch route's `syncTriggerEnum` — what woke the client for a sync.
const healthKitSyncTriggerEnum = z
.enum(["foreground", "background", "push"])
.enum(["foreground", "background", "push", "manual"])
.describe("What triggered a HealthKit batch.");
// Resolved entry (defaults merged): `kind` + `enabled` are always present.
+2 -2
View File
@@ -127,10 +127,10 @@ const batchPayloadSchema = z
// used in attribution — sending it or omitting it produces byte-identical
// row outcomes. Omit on any client that does not track its wake reason.
syncTrigger: z
.enum(["foreground", "background", "push"])
.enum(["foreground", "background", "push", "manual"])
.optional()
.describe(
"Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake). Recorded on the ingest wide event and kept on the account as `lastSyncTrigger` — plus `lastBackgroundSyncAt` for a `background` or `push` trigger — both readable from `GET /api/integrations/healthkit`. It does not affect dedup, attribution, or how any sample is stored. Optional and backward-compatible: pre-#66 clients omit it.",
'Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, a push wake, or a user-initiated `manual` "Sync all" run, since v1.42). Recorded on the ingest wide event and kept on the account as `lastSyncTrigger` — plus `lastBackgroundSyncAt` for a `background` or `push` trigger — both readable from `GET /api/integrations/healthkit`. It does not affect dedup, attribution, or how any sample is stored. Optional and backward-compatible: pre-#66 clients omit it.',
),
})
.meta({
+15
View File
@@ -269,6 +269,21 @@ export const malformedJsonResponse = {
},
};
/**
* The 501 a route answers while its contract is published ahead of its
* implementation on a release branch. Every stub carries it; the owner of the
* route drops it with the stub. No tagged release answers it: the error-code
* catalogue check fails once `not_implemented` is listed and nothing emits
* it, and fails while something still does and it is not listed.
*/
export const notImplementedResponse = {
"501": {
description:
"Not implemented yet: the contract is published ahead of the implementation on a release branch. `meta.errorCode: not_implemented`.",
content: { "application/json": { schema: errorEnvelope } },
},
} as const;
// ── Standard 401 / 422 / 429 responses ───────────────────────────────
export const stdResponses = {
+144
View File
@@ -37,6 +37,7 @@ import {
errorEnvelope,
idempotencyKeyParameter,
idempotentWrite,
notImplementedResponse,
recordRefusal,
recordWriteRateLimitResponse,
stdResponses,
@@ -243,7 +244,150 @@ const vaccinationNotFound = {
const idPath = { path: z.object({ id: z.string() }) };
// v1.42 (#1005) — the record's own vaccine definitions.
const customVaccine = z
.object({
id: z.string(),
name: z.string(),
components: z
.array(z.string())
.describe("Catalogue antigen slugs the product covers."),
typicalSeriesDoses: z.number().int().nullable(),
boosterIntervalMonths: z.number().int().nullable(),
createdAt: z.iso.datetime({ offset: true }),
updatedAt: z.iso.datetime({ offset: true }),
})
.meta({ id: "CustomVaccine" });
const customVaccineCreateRequest = z
.object({
name: z.string(),
components: z.array(z.string()),
typicalSeriesDoses: z.number().int().nullable().optional(),
boosterIntervalMonths: z.number().int().nullable().optional(),
})
.strict()
.meta({ id: "CreateCustomVaccineRequest" });
const customVaccineUpdateRequest = customVaccineCreateRequest
.partial()
.meta({ id: "UpdateCustomVaccineRequest" });
export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/vaccinations/custom": {
get: {
tags: ["Records"],
summary: "List the record's own vaccines",
description:
"Vaccine definitions the record's owner added for products the shipped catalogue does not list. Soft-deleted definitions are excluded.",
responses: {
"200": {
description: "The definitions, by name.",
content: {
"application/json": {
schema: dataEnvelope(
z.array(customVaccine),
"ListCustomVaccinesEnvelope",
),
},
},
},
...stdResponses,
...notImplementedResponse,
},
},
post: {
tags: ["Records"],
summary: "Add an own vaccine",
description:
"Defines a vaccine in the same shape a catalogue entry resolves to, so series and booster logic treat both alike. Names are unique per record.",
requestBody: {
required: true,
content: { "application/json": { schema: customVaccineCreateRequest } },
},
responses: {
"201": {
description: "Created.",
content: {
"application/json": {
schema: dataEnvelope(
customVaccine,
"CreateCustomVaccineEnvelope",
),
},
},
},
"409": {
description: "A live definition with this name already exists.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...recordWriteRateLimitResponse,
...notImplementedResponse,
},
},
},
"/api/vaccinations/custom/{id}": {
patch: {
tags: ["Records"],
summary: "Edit an own vaccine",
description: "Changes one definition. Owner-scoped; an unknown id 404s.",
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: customVaccineUpdateRequest } },
},
responses: {
"200": {
description: "Updated.",
content: {
"application/json": {
schema: dataEnvelope(
customVaccine,
"UpdateCustomVaccineEnvelope",
),
},
},
},
"404": {
description: "No such definition on this record.",
content: { "application/json": { schema: errorEnvelope } },
},
"409": {
description: "A live definition with this name already exists.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
delete: {
tags: ["Records"],
summary: "Remove an own vaccine",
description:
"Soft-deletes one definition. Doses logged against it stay, on their `vaccineName`.",
requestParams: idPath,
responses: {
"200": {
description: "Removed.",
content: {
"application/json": {
schema: dataEnvelope(
z.object({ deleted: z.boolean() }),
"DeleteCustomVaccineEnvelope",
),
},
},
},
"404": {
description: "No such definition on this record.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
},
"/api/vaccinations": {
get: {
tags: ["Records"],