Files
HealthLog/src/lib/openapi/routes/devices.ts
T
Marc-André Bombeck 6d8dd75fff feat(devices): accept a Live Activity push token on registration
Add an optional hex liveActivityPushToken to device registration. The iOS
client registers the current per-Activity ActivityKit push token so the
server can address a Live Activity update or end push on a
medication-intake mutation, and sends null to clear it when no Activity is
running. Following the medicationDelivery override convention, the field
is only touched when present, so an ordinary re-register keeps the prior
value. Regenerate the OpenAPI contract.
2026-06-14 23:58:27 +02:00

96 lines
3.3 KiB
TypeScript

/**
* OpenAPI route table — device (APNs) registration.
*
* Part of the OpenAPI route table; aggregated in `./index.ts`.
* Schemas come from `src/lib/validations/*` where shared with the
* runtime request parsing, so the wire contract stays single-source.
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
const deviceRegisterRequest = z
.object({
token: z
.string()
.min(8)
.max(512)
.regex(/^[A-Za-z0-9+/=._:-]+$/)
.describe("Generic device identifier (legacy; APNs token below)."),
bundleId: z.string().min(1).max(128),
locale: z.string().min(2).max(16).optional(),
appVersion: z.string().min(1).max(32).optional(),
model: z.string().min(1).max(64).optional(),
apnsToken: z
.string()
.min(8)
.max(256)
.regex(/^[A-Fa-f0-9]+$/)
.optional()
.describe(
"Hex-encoded APNs device token. Must be paired with `apnsEnvironment`.",
),
apnsEnvironment: z
.enum(["sandbox", "production"])
.optional()
.describe(
"Gateway the iOS client received `apnsToken` from. Server never auto-detects.",
),
medicationDelivery: z
.enum(["server", "client"])
.nullable()
.optional()
.describe(
'v1.7.0 per-device medication-delivery override. NULL / omitted = inherit the user-level roaming default. "server" forces server APNs for this device; "client" forces local. Stored + echoed; cron suppression stays user-level.',
),
liveActivityPushToken: z
.string()
.min(8)
.max(256)
.regex(/^[A-Fa-f0-9]+$/)
.nullable()
.optional()
.describe(
"v1.17.1 (#22) hex-encoded ActivityKit Live Activity push token (distinct from the device APNs token). The server addresses a Live Activity update / end push on a medication-intake mutation. Omitted = keep the prior value; null = clear it when the Activity ends.",
),
})
.meta({
id: "DeviceRegisterRequest",
description:
"Native device registration. Re-registering an APNs token belonging to another user returns 409 (cross-user-hijack guard).",
});
export const devicePaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/devices": {
post: {
tags: ["Devices"],
summary: "Register native device + APNs token",
description:
"Idempotent upsert by `token`. APNs token + environment are paired — supplying one without the other returns 422. Cross-user re-registration of either identifier returns 409.",
requestBody: {
required: true,
content: { "application/json": { schema: deviceRegisterRequest } },
},
responses: {
"201": {
description: "Device registered or refreshed.",
content: {
"application/json": {
schema: dataEnvelope(
z.object({ id: z.string() }),
"DeviceRegisterResponse",
),
},
},
},
"409": {
description:
"Device or APNs token already registered to another user.",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
},
},
},
};