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

+9
View File
@@ -1,5 +1,6 @@
import { z } from "zod/v4";
import { isValidKvnr, normaliseKvnr } from "@/lib/validations/kvnr";
import { webauthnKeyNameSchema } from "@/lib/validations/mfa";
/** Minimum password length — must match checkPasswordStrength() in @/lib/auth/password.ts */
const PASSWORD_MIN_LENGTH = 12;
@@ -117,6 +118,14 @@ export const changePasswordSchema = z
path: ["confirmPassword"],
});
/**
* Rename a registered passkey — a PRIMARY sign-in credential, not the
* second-factor security key `mfaWebauthnRenameSchema` covers. The two carry
* the same label constraint and are served by different routes; they share the
* constraint rather than the schema so neither surface can drift the other.
*/
export const passkeyRenameSchema = z.object({ name: webauthnKeyNameSchema });
export type RegisterInput = z.infer<typeof registerSchema>;
export type LoginPasswordInput = z.infer<typeof loginPasswordSchema>;
export type ProfileInput = z.infer<typeof profileSchema>;
+39
View File
@@ -0,0 +1,39 @@
/**
* Request schemas for the per-user scalar preference endpoints under
* `/api/auth/me/*`. Single-sourced here so the runtime `safeParse` and the
* OpenAPI registry (`src/lib/openapi/routes/profile.ts`) describe the identical
* wire contract — the same reason `mfa.ts` exists next door.
*
* All three follow the same shape: a one-field PATCH that hard-sets the value
* and answers with the resolved next state, so a client can replace its
* optimistic update rather than re-read.
*/
import { z } from "zod/v4";
import {
injectionSiteEnum,
INJECTION_SITE_VALUES,
} from "@/lib/validations/medication";
/** `PATCH /api/auth/me/unit-preference` — metric/imperial display branch. */
export const unitPreferencePatchSchema = z.object({
unitPreference: z.enum(["metric", "imperial"]),
});
/** `PATCH /api/auth/me/documents-auto-ai-read` — the auto-read opt-in. */
export const documentsAutoAiReadPatchSchema = z.object({
documentsAutoAiRead: z.boolean(),
});
/** `PATCH /api/auth/me/injection-site-prefs` — the user-level deny-list. */
export const injectionSitePrefsPatchSchema = 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.",
}),
});