Merge branch 'feat/v146-server-parse' into feat/v146-server

This commit is contained in:
Marc-André Bombeck committed 2026-10-23 11:30:47 +02:00
commit 7ea247e193
47 files changed
+760 -1113

No files matched your search

+2 -47
View File
@@ -1,21 +1,15 @@
{
"$comment": "safeJson calls per route file under src/app; new body-reading routes use parseJsonBody. Maintained by src/__tests__/route-body-parse-ratchet.test.ts.",
"count": 106,
"total": 106,
"count": 61,
"total": 61,
"entries": {
"app/api/admin/ai-settings/route.ts": 1,
"app/api/admin/drain-per-sample-cumulative/route.ts": 1,
"app/api/admin/encryption/key-backup/confirm/route.ts": 1,
"app/api/admin/encryption/key-backup/verify/route.ts": 1,
"app/api/admin/invites/route.ts": 1,
"app/api/admin/notifications/reminder-check/route.ts": 1,
"app/api/admin/rollups/recompute/route.ts": 1,
"app/api/admin/settings/web-push-vapid/generate/route.ts": 1,
"app/api/admin/users/[id]/reset-password/route.ts": 1,
"app/api/ai/test/route.ts": 1,
"app/api/anamnesis/emergency/route.ts": 1,
"app/api/anamnesis/facts/[id]/route.ts": 1,
"app/api/anamnesis/facts/route.ts": 1,
"app/api/auth/login/route.ts": 1,
"app/api/auth/me/locale/route.ts": 1,
"app/api/auth/me/mfa/disable/route.ts": 1,
@@ -26,85 +20,46 @@
"app/api/auth/passkey/login-options/route.ts": 1,
"app/api/auth/profile/route.ts": 1,
"app/api/auth/refresh/route.ts": 1,
"app/api/biomarkers/[id]/route.ts": 1,
"app/api/biomarkers/route.ts": 1,
"app/api/coach/about-me/questions/route.ts": 1,
"app/api/coach/about-me/route.ts": 1,
"app/api/consent/ai/route.ts": 1,
"app/api/custom-metrics/[id]/entries/[entryId]/route.ts": 1,
"app/api/custom-metrics/[id]/entries/route.ts": 1,
"app/api/custom-metrics/[id]/route.ts": 1,
"app/api/custom-metrics/route.ts": 1,
"app/api/cycle/day-logs/bulk/route.ts": 1,
"app/api/cycle/day-logs/route.ts": 1,
"app/api/cycle/period/route.ts": 1,
"app/api/dashboard/chart-overlay-prefs/route.ts": 1,
"app/api/dashboard/widgets/route.ts": 1,
"app/api/dawarich/connect/route.ts": 1,
"app/api/dawarich/disconnect/route.ts": 1,
"app/api/dawarich/settings/route.ts": 1,
"app/api/dawarich/sync/route.ts": 1,
"app/api/devices/route.ts": 1,
"app/api/documents/inbound/[id]/extract/route.ts": 1,
"app/api/documents/inbound/[id]/index/route.ts": 1,
"app/api/documents/inbound/[id]/suggest/route.ts": 1,
"app/api/documents/inbound/[id]/summary/route.ts": 1,
"app/api/fitbit/credentials/route.ts": 1,
"app/api/google-health/credentials/route.ts": 1,
"app/api/illness/episodes/[id]/resolve/route.ts": 1,
"app/api/import/route.ts": 1,
"app/api/ingest/medication/route.ts": 1,
"app/api/insights/chat/messages/[id]/feedback/route.ts": 1,
"app/api/insights/ecg/route.ts": 1,
"app/api/insights/feedback/route.ts": 1,
"app/api/insights/generate/route.ts": 1,
"app/api/insights/layout/route.ts": 1,
"app/api/insights/provider-chain/route.ts": 1,
"app/api/integrations/healthkit/route.ts": 1,
"app/api/internal/deploy-webhook/route.ts": 1,
"app/api/labs/[id]/route.ts": 1,
"app/api/labs/ocr/commit/route.ts": 1,
"app/api/labs/ocr/extract/route.ts": 1,
"app/api/labs/route.ts": 1,
"app/api/measurement-reminders/[id]/route.ts": 1,
"app/api/measurement-reminders/[id]/snooze/route.ts": 1,
"app/api/measurement-reminders/route.ts": 1,
"app/api/measurements/[id]/route.ts": 1,
"app/api/measurements/batch/route.ts": 1,
"app/api/measurements/by-external-ids/route.ts": 1,
"app/api/measurements/route.ts": 1,
"app/api/medications/[id]/intake/[eventId]/route.ts": 1,
"app/api/medications/[id]/intake/import/route.ts": 1,
"app/api/medications/[id]/intake/route.ts": 1,
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts": 1,
"app/api/medications/[id]/schedule-revisions/route.ts": 1,
"app/api/medications/extract/route.ts": 1,
"app/api/medications/intake/bulk/route.ts": 1,
"app/api/medications/intake/route.ts": 1,
"app/api/medications/layout/route.ts": 1,
"app/api/medications/route.ts": 1,
"app/api/mental-health/assessments/route.ts": 1,
"app/api/mood-entries/[id]/route.ts": 1,
"app/api/mood-entries/bulk/route.ts": 1,
"app/api/mood-entries/route.ts": 1,
"app/api/mood/tags/layout/route.ts": 1,
"app/api/nightscout/connect/route.ts": 1,
"app/api/nutrients/batch/route.ts": 1,
"app/api/onboarding/answers/route.ts": 1,
"app/api/onboarding/complete/route.ts": 1,
"app/api/onboarding/disclaimer/route.ts": 1,
"app/api/onboarding/restart/route.ts": 1,
"app/api/onboarding/tour/route.ts": 1,
"app/api/oura/credentials/route.ts": 1,
"app/api/polar/credentials/route.ts": 1,
"app/api/record-settings/[family]/route.ts": 1,
"app/api/search/route.ts": 1,
"app/api/settings/email/route.ts": 1,
"app/api/settings/ntfy/route.ts": 1,
"app/api/settings/telegram/route.ts": 1,
"app/api/settings/webhook/route.ts": 1,
"app/api/strava/credentials/route.ts": 1,
"app/api/user/ai-provider/route.ts": 1,
"app/api/user/profile/route.ts": 1,
"app/api/whoop/credentials/route.ts": 1,
"app/api/withings/credentials/route.ts": 1,
@@ -16,7 +16,10 @@
* every `if (!<result>.success)` block whose identifier came from a `safeParse`
* in that file is read on its own, and only the blocks that answer 422 are
* judged — a 400, a 401 or a 403 raised off a parse result is a different
* refusal and is left alone.
* refusal and is left alone. A route that reads its body through
* `parseJsonBody` has no such block: each call is a site of its own, carrying
* the issue list by construction, and its `message` option is read as the
* refused string by the token sweep below.
*
* The second half of the same audit item is the dotted token in `error`, and
* it is checked by its own sweep at the bottom of this file rather than by the
@@ -94,11 +97,58 @@ interface RefusalSite {
carriesIssues: boolean;
}
/**
* Every `parseJsonBody(…)` call in a file, as its paren-balanced source text
* with the line it starts on. The helper reads, validates and refuses in one
* call (`src/lib/api/body.ts`), so a route that uses it has no
* `if (!x.success)` block for the matcher above to find; its refusal is the
* helper's envelope, which always carries `details.issues`.
*/
function parseJsonBodyCalls(src: string): { text: string; line: number }[] {
const calls: { text: string; line: number }[] = [];
for (const m of src.matchAll(/\bparseJsonBody\s*\(/g)) {
const start = m.index ?? 0;
let depth = 0;
let end = src.length;
for (let j = start + m[0].length - 1; j < src.length; j++) {
if (src[j] === "(") depth++;
else if (src[j] === ")") {
depth--;
if (depth === 0) {
end = j + 1;
break;
}
}
}
calls.push({
text: src.slice(start, end),
line: src.slice(0, start).split("\n").length,
});
}
return calls;
}
/** The status a `parseJsonBody` call refuses with: its `status` option, else 422. */
function parseJsonBodyStatus(call: string): number {
const m = /\bstatus:\s*(\d{3})\b/.exec(call);
return m ? Number(m[1]) : 422;
}
/** Every 422 refusal decided by a `safeParse` result, across the route tree. */
function scanRefusalSites(): RefusalSite[] {
const sites: RefusalSite[] = [];
for (const file of routeFiles(API_ROOT)) {
const src = readFileSync(file, "utf8");
// A `parseJsonBody` refusal is a 422 with the issue list unless the call
// names another status, which is then a different refusal, as below.
for (const call of parseJsonBodyCalls(src)) {
if (parseJsonBodyStatus(call.text) !== 422) continue;
sites.push({
route: relative(process.cwd(), file).split(sep).join("/"),
line: call.line,
carriesIssues: true,
});
}
// Identifiers bound to a parse result. Both call styles are in the tree:
// `schema.safeParse(body)` and the standalone `z.safeParse(schema, body)`.
const parsed = new Set<string>();
@@ -228,12 +278,20 @@ function tokenRefusals(): string[] {
/apiValidationError\(\s*"([^"]+)"/g,
/new HttpError\(\s*422\s*,\s*"([^"]+)"/g,
];
const refused: string[] = [];
for (const shape of shapes) {
for (const m of src.matchAll(shape)) {
if (!DOTTED_TOKEN.test(m[1])) continue;
const key = `${route} ${m[1]}`;
if (!found.includes(key)) found.push(key);
}
for (const m of src.matchAll(shape)) refused.push(m[1]);
}
// `parseJsonBody`'s `message` option is the 422's `error` string.
for (const call of parseJsonBodyCalls(src)) {
if (parseJsonBodyStatus(call.text) !== 422) continue;
const m = /\bmessage:\s*"([^"]+)"/.exec(call.text);
if (m) refused.push(m[1]);
}
for (const text of refused) {
if (!DOTTED_TOKEN.test(text)) continue;
const key = `${route} ${text}`;
if (!found.includes(key)) found.push(key);
}
}
return found;