Publish the nineteen routes the drift check could not see

`openapi:check` compares the registry against the committed YAML, so a route
that was never registered produced no drift and no failure. These nineteen
had shipped without a contract: the health probe, the dashboard analytics
aggregate, the profile summary, personal records, the badge grid, the whole
environmental-context module, both onboarding writes, the Apple Health import
pair, the external medication ingest, the self-context adopt, the AI-provider
update and the AI connection probe.

Each entry is read off the route rather than inferred from a neighbour, so it
carries what a client cannot guess: that `/api/health` answers a bare object
and not the envelope, that `/api/analytics` records a score row and mints
pattern ids on a GET, that `/api/ai/test` reports a provider failure as 200
with `ok: false` because a 5xx comes back as somebody's HTML error page, that
the ingest route replays with 200 and creates with 201, and that a home
location re-stamps its effective-from instant on every write.

Three route-local request schemas move to `src/lib/validations/` so the
published shape and the runtime parse are one object rather than two copies —
a route module may only export handlers, which is why they could not be
imported where they were. The AI-provider PATCH has no schema to move: it
hand-rolls its parse field by field, so its entry says so and spells out that
a wrongly-typed key is skipped in silence.

The generated YAML is deliberately not in this commit.
This commit is contained in:
Marc-André Bombeck committed 2026-08-22 12:54:43 +02:00
1 parent 1c2e6c5c80
commit 49da15feaa
15 files changed
+2097 -68

No files matched your search

+2 -28
View File
@@ -1,5 +1,4 @@
import type { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
resolveProviderForTest,
@@ -16,6 +15,7 @@ import {
reserveBudget,
resolveDailyCap,
} from "@/lib/ai/coach/budget";
import { aiTestOverrideSchema } from "@/lib/validations/ai-provider";
/**
* Output ceiling of the probe below. Mirrors the `maxTokens` on the completion
@@ -39,32 +39,6 @@ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
export const dynamic = "force-dynamic";
const overrideSchema = z
.object({
provider: z
.enum([
"OPENAI",
"ANTHROPIC",
"LOCAL",
"OPENAI_COMPATIBLE",
"CHATGPT_OAUTH",
])
.optional()
.nullable(),
model: z.string().min(1).max(120).optional().nullable(),
baseUrl: z.string().url().max(2048).optional().nullable(),
anthropicKey: z.string().min(1).max(500).optional().nullable(),
localKey: z.string().min(1).max(500).optional().nullable(),
openaiKey: z.string().min(1).max(500).optional().nullable(),
// v1.33.1 (#470) — the OpenAI-compatible gateway's own fields, kept
// separate from `baseUrl` / `openaiKey` so testing an unsaved gateway
// config can never reach the pinned OpenAI arm and vice versa.
compatBaseUrl: z.string().url().max(2048).optional().nullable(),
compatKey: z.string().min(1).max(500).optional().nullable(),
compatModel: z.string().min(1).max(120).optional().nullable(),
})
.strict();
export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
annotate({ action: { name: "ai.test" } });
@@ -93,7 +67,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
maxBytes: 64 * 1024,
});
if (error) return error;
const parsed = overrideSchema.safeParse(data);
const parsed = aiTestOverrideSchema.safeParse(data);
if (!parsed.success) {
return apiError("Invalid override payload", 422);
}
+2 -12
View File
@@ -5,17 +5,7 @@ import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
import { setOnboardingPendingCookie } from "@/lib/auth/session";
import { NextRequest } from "next/server";
import { z } from "zod/v4";
const onboardingSchema = z.object({
displayName: z.string().trim().min(1).max(50).optional(),
heightCm: z.number().min(50).max(300).optional(),
dateOfBirth: z.string().optional(),
// Same three values the profile schema stores. A narrower enum here
// rejected the whole onboarding submission over a field the account
// is allowed to hold, and left the value unrecordable until the person
// found the setting again afterwards.
gender: z.enum(["MALE", "FEMALE", "OTHER"]).optional(),
});
import { onboardingCompleteSchema } from "@/lib/validations/onboarding";
/**
* Complete the onboarding flow. Saves optional profile data and marks
@@ -30,7 +20,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
if (jsonError) return jsonError;
const result = z.safeParse(onboardingSchema, body);
const result = z.safeParse(onboardingCompleteSchema, body);
if (!result.success) {
return apiError("Invalid input", 422, {
errorCode: "onboarding.complete.invalid",
+10 -19
View File
@@ -1,5 +1,4 @@
import type { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
@@ -14,10 +13,8 @@ import { prisma, toJson } from "@/lib/db";
import { Prisma } from "@/generated/prisma/client";
import { annotate } from "@/lib/logging/context";
import { checkRateLimit } from "@/lib/rate-limit";
import {
ONBOARDING_GOAL_SLUGS,
buildGoalSeededDashboardLayout,
} from "@/lib/onboarding/goals";
import { buildGoalSeededDashboardLayout } from "@/lib/onboarding/goals";
import { onboardingStepSchema } from "@/lib/validations/onboarding";
/**
* v1.4.25 W14b — POST /api/onboarding/step.
@@ -45,19 +42,13 @@ import {
* but tight enough to defang a stuck retry loop.
*/
const stepBodySchema = z.object({
step: z.number().int().min(1).max(4),
// v1.17.1 — optional goal slugs from the GoalsChipPicker (step 2
// submit). Validated against the closed slug enum so an unknown slug
// 422s; deduped + capped at the full set size. Persisted to
// `User.onboardingGoals` (field-by-field, no mass assignment) and on
// completion (step 4) seeds the dashboard layout when the user never
// customized it. Omitted leaves the stored goals untouched.
goals: z
.array(z.enum(ONBOARDING_GOAL_SLUGS))
.max(ONBOARDING_GOAL_SLUGS.length)
.optional(),
});
// v1.17.1 — the optional goal slugs from the GoalsChipPicker (step 2 submit)
// ride the same body. Validated against the closed slug enum so an unknown
// slug 422s; persisted to `User.onboardingGoals` field-by-field (no mass
// assignment) and, on completion (step 4), used to seed the dashboard layout
// when the user never customized it. Omitted leaves the stored goals
// untouched. The schema itself lives in `@/lib/validations/onboarding` so the
// OpenAPI registry reads the same object this parse does.
export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
@@ -83,7 +74,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
if (jsonError) return jsonError;
const parsed = stepBodySchema.safeParse(body);
const parsed = onboardingStepSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "onboarding.step" },