Register the sixteen auth, settings and discovery routes the spec never had

`pnpm openapi:check` compares the registry against the committed YAML and
never the routes against the registry, so a route that was simply never
registered stayed invisible to the gate. Sixteen of them are called by the
native client and describe nothing: sign-out, the three per-user preference
scalars, the two pre-session discovery reads, passkey login options, the
passkey rename and delete, registration status, the consent latest-receipt
reader and its revoke, account deletion, the instance service switches, the
API-token list and revoke, the threshold overrides, the version read, and the
assistant feature matrix.

Each contract is derived from the handler rather than from its docblock, so
the published statuses are the ones the code can actually produce — the 413 on
a 1 KB body cap, the 400 the consent family answers where the rest of the API
answers 422, the 403 an instance-wide API switch produces on a token READ, the
last-admin and last-Guardian refusals on account deletion, and the fresh-factor
arm that a Bearer transport cannot clear at any scope.

Four request bodies moved to the validation modules the handlers now parse
with, so the published wire cannot drift from the runtime one: the three
`/api/auth/me` preference scalars into a new `user-prefs` module, and the
passkey rename beside the login schemas. Behaviour is unchanged.

The generated YAML is left out; it is regenerated once the remaining groups
land.
This commit is contained in:
Marc-André Bombeck committed 2026-08-22 12:24:52 +02:00
1 parent a6c239d952
commit b6efc6cb5a
11 files changed
+1109 -40

No files matched your search

@@ -20,8 +20,6 @@
* field-by-field write (no mass assignment). Idempotent — always returns the
* resolved next state so the client can hard-set the optimistic update.
*/
import { z } from "zod";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
@@ -36,10 +34,7 @@ import { ensureWebAiConsentReceipt } from "@/lib/consent/web-grant";
import { prisma } from "@/lib/db";
import { enqueueSummaryCatchUp } from "@/lib/jobs/document-summary-catchup";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
const patchBodySchema = z.object({
documentsAutoAiRead: z.boolean(),
});
import { documentsAutoAiReadPatchSchema } from "@/lib/validations/user-prefs";
export const dynamic = "force-dynamic";
@@ -87,7 +82,7 @@ export const PATCH = apiHandler(async (req: Request) => {
});
if (jsonError) return jsonError;
const parsed = patchBodySchema.safeParse(body);
const parsed = documentsAutoAiReadPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.documentsAutoAiRead.patch.invalid_shape" },
@@ -18,8 +18,6 @@
* limit, Zod safeParse → 422 via `returnAllZodIssues`, audit-log row,
* field-by-field write (no mass assignment).
*/
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
@@ -33,10 +31,10 @@ import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import {
injectionSiteEnum,
INJECTION_SITE_VALUES,
type InjectionSiteValue,
} from "@/lib/validations/medication";
import { injectionSitePrefsPatchSchema } from "@/lib/validations/user-prefs";
import { NextRequest } from "next/server";
export const dynamic = "force-dynamic";
@@ -44,18 +42,6 @@ export const dynamic = "force-dynamic";
const PATCH_RATE_LIMIT = 60;
const PATCH_WINDOW_MS = 60_000;
const patchBodySchema = z.object({
// Dedup at write time; cap at the full enum size. An empty array
// clears the exclusion.
globalExcludedInjectionSites: z
.array(injectionSiteEnum)
.max(INJECTION_SITE_VALUES.length)
.meta({
description:
"User-level injection-site deny-list. Sites here are never offered for any medication and rejected at intake (deny wins). Empty array clears the exclusion.",
}),
});
interface InjectionSitePrefsResponse {
globalExcludedInjectionSites: InjectionSiteValue[];
}
@@ -96,7 +82,7 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
});
if (jsonError) return jsonError;
const parsed = patchBodySchema.safeParse(body);
const parsed = injectionSitePrefsPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.injection-site-prefs.patch.invalid_shape" },
+2 -7
View File
@@ -16,8 +16,6 @@
* limit, Zod safeParse → 422 via `returnAllZodIssues`, audit-log row,
* field-by-field write (no mass assignment).
*/
import { z } from "zod";
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
import {
apiError,
@@ -30,10 +28,7 @@ import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { DEFAULT_UNIT_PREFERENCE } from "@/lib/measurements/display-transform";
const patchBodySchema = z.object({
unitPreference: z.enum(["metric", "imperial"]),
});
import { unitPreferencePatchSchema } from "@/lib/validations/user-prefs";
export const dynamic = "force-dynamic";
@@ -85,7 +80,7 @@ export const PATCH = apiHandler(async (req: Request) => {
throw new HttpError(422, "unit-preference.body.invalid_json");
}
const parsed = patchBodySchema.safeParse(body);
const parsed = unitPreferencePatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.unit-preference.patch.invalid_shape" },
+1 -4
View File
@@ -9,10 +9,7 @@ import {
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { webauthnKeyNameSchema } from "@/lib/validations/mfa";
import { z } from "zod/v4";
const passkeyRenameSchema = z.object({ name: webauthnKeyNameSchema });
import { passkeyRenameSchema } from "@/lib/validations/auth";
export const PATCH = apiHandler(
async (