refactor(api): read route bodies through parseJsonBody

132 route handlers spelled out safeJson, safeParse and the 422 refusal by
hand in one of six shapes. Each block becomes one parseJsonBody call with
the same size cap, wide-event action, issue count, error code, message and
status it had, so every response is the one it was. The comments inside
the replaced refusal blocks go with them.

A ratchet (route-body-parse-ratchet) counts the route files that still call
safeJson themselves; the count may only fall.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 11:12:39 +02:00
1 parent 07e1e83ee8
commit 5b49fa6680
134 files changed
+838 -1874

No files matched your search

@@ -0,0 +1,113 @@
{
"$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,
"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,
"app/api/auth/me/mfa/totp/confirm/route.ts": 1,
"app/api/auth/me/timezone/route.ts": 1,
"app/api/auth/mfa/verify/route.ts": 1,
"app/api/auth/mfa/webauthn/verify/route.ts": 1,
"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,
"app/api/workouts/batch/route.ts": 1
}
}
@@ -0,0 +1,87 @@
/**
* Route handlers read their JSON body through `parseJsonBody`.
*
* `parseJsonBody` (`src/lib/api/body.ts`) is `safeJson`, the Zod
* `safeParse`, the validation-failed wide-event action and the multi-issue
* 422 envelope in one call. Before it, every body-accepting route spelled
* those steps out by hand, and the copies drifted: some annotated the
* refusal and some did not, some answered a flat 400 where the rest answered
* the multi-issue 422.
*
* The routes that still call `safeJson` themselves are counted per file in
* `baselines/route-safe-json.json`. Most of them do something between
* reading and validating (an optimistic-concurrency token split off the body,
* a count cap answered before the schema runs, a refund of a spent quota on a
* malformed body), which is a reason to keep the read separate, not a reason
* to copy it into a new route. A new route file that calls `safeJson`, or a
* listed one that gains a call, fails. A file that sheds one fails until the
* lower count is written back with
* `RATCHET_WRITE=1 pnpm vitest run src/__tests__/route-body-parse-ratchet.test.ts`.
*/
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { runRatchet, writeBackHint } from "./helpers/ratchet";
import {
readSourceText,
requireFloor,
stripComments,
walkSourceFiles,
} from "./helpers/source-files";
const GUARD = "src/__tests__/route-body-parse-ratchet.test.ts";
const APP = join(process.cwd(), "src/app");
const SAFE_JSON_CALL = /\bsafeJson\s*(?:<[^>()]*>)?\s*\(/g;
const PARSE_JSON_BODY_CALL = /\bparseJsonBody\s*\(/g;
function count(text: string, pattern: RegExp): number {
return [...text.matchAll(pattern)].length;
}
describe("route body parsing", () => {
const routes = walkSourceFiles(APP, { floor: 400 }).filter((rel) =>
rel.endsWith("/route.ts"),
);
const safeJson: Record<string, number> = {};
const helper: string[] = [];
for (const rel of routes) {
const code = stripComments(readSourceText(join(APP, rel)));
const n = count(code, SAFE_JSON_CALL);
if (n > 0) safeJson[`app/${rel}`] = n;
if (count(code, PARSE_JSON_BODY_CALL) > 0) helper.push(rel);
}
it("finds the routes on both sides", () => {
requireFloor("route files", routes, 400);
requireFloor(
"routes reading their body through parseJsonBody",
helper,
100,
);
requireFloor("routes still calling safeJson", Object.keys(safeJson), 1);
});
it("adds no route that calls safeJson itself", () => {
const r = runRatchet("route-safe-json.json", safeJson, {
comment: `safeJson calls per route file under src/app; new body-reading routes use parseJsonBody. Maintained by ${GUARD}.`,
});
expect(
r.grew,
"A route that reads its body with safeJson. Use parseJsonBody " +
"(src/lib/api/body.ts); its options cover the size cap, the " +
"validation-failed action, the error code and the message.",
).toEqual([]);
expect(r.stale, writeBackHint(GUARD)).toEqual([]);
});
it("counts a call however it is spelled", () => {
expect(count("await safeJson(request)", SAFE_JSON_CALL)).toBe(1);
expect(
count("await safeJson<Body>(req, { maxBytes: 1 })", SAFE_JSON_CALL),
).toBe(1);
expect(count("await safeJson (\n request,\n)", SAFE_JSON_CALL)).toBe(1);
expect(count("import { safeJson } from", SAFE_JSON_CALL)).toBe(0);
});
});
+5 -16
View File
@@ -54,13 +54,8 @@ import {
requireRecentProof,
MFA_STEP_UP_MAX_AGE_SECONDS,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { getAuditLogRetentionDays } from "@/lib/jobs/audit-log-cleanup";
@@ -163,17 +158,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
return apiError("Too many invitations, try again later", 429);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, inviteGrantSchema, {
maxBytes: 8 * 1024,
errorCode: "sharing.invite.invalid",
});
if (jsonError) return jsonError;
const parsed = inviteGrantSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "sharing.invite.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { identifier, expiresAt, access, scope } = parsed.data;
+4 -10
View File
@@ -19,12 +19,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import {
decideHandover,
@@ -55,12 +51,10 @@ export const GET = apiHandler(async (_request: NextRequest) => {
export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, handoverDecisionSchema, {
maxBytes: 16 * 1024,
});
if (jsonError) return jsonError;
const parsed = handoverDecisionSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
let result;
try {
+5 -16
View File
@@ -55,13 +55,8 @@ import {
requireActorAuth,
SharingAccessDeniedError,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
@@ -83,17 +78,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, switchAccountSchema, {
maxBytes: 4 * 1024,
errorCode: "sharing.switch.invalid",
});
if (jsonError) return jsonError;
const parsed = switchAccountSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "sharing.switch.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { accountId, expectedEpoch } = parsed.data;
const ip = getClientIp(request);
+5 -14
View File
@@ -8,12 +8,8 @@
* (`activity_goals_json`) and the activity calendar reads them.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { Prisma } from "@/generated/prisma/client";
import { prisma, toJson } from "@/lib/db";
@@ -56,16 +52,11 @@ export const GET = apiHandler(async () => {
export const PATCH = apiHandler(async (request: Request) => {
const { user } = await requireAuth();
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, activityGoalsSchema, {
maxBytes: 1024,
action: "activity.goals.validation-failed",
});
if (jsonError) return jsonError;
const parsed = activityGoalsSchema.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "activity.goals.validation-failed" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = mergeActivityGoals(await readGoals(user.id), parsed.data);
await prisma.user.update({
@@ -15,13 +15,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
@@ -99,12 +94,10 @@ export const GET = apiHandler(async () => {
export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, pulseDayMeanActionSchema, {
maxBytes: 1024,
});
if (jsonError) return jsonError;
const parsed = pulseDayMeanActionSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const action = parsed.data.action;
annotate({
action: { name: "admin.pulse_day_mean.requested" },
@@ -52,13 +52,8 @@ import type { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { resolveAssistantFlags } from "@/lib/feature-flags";
@@ -180,16 +175,10 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.settings.assistant-flags.update" } });
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, assistantFlagsSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = assistantFlagsSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
// Field by field from the parsed body; the schema is strict, so every key
// here is one the route owns.
@@ -33,13 +33,8 @@ import type { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
@@ -95,15 +90,10 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.settings.module-availability.update" } });
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, moduleAvailabilitySchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = moduleAvailabilitySchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const patch: Record<string, boolean> = {};
for (const [key, value] of Object.entries(parsed.data)) {
+4 -15
View File
@@ -1,13 +1,8 @@
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { encrypt } from "@/lib/crypto";
import { adminSettingsSchema } from "@/lib/validations/admin";
@@ -109,16 +104,10 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.settings.update" } });
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, adminSettingsSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = adminSettingsSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const data = parsed.data;
const updates: Record<string, unknown> = {};
+4 -15
View File
@@ -1,13 +1,8 @@
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { NextRequest } from "next/server";
import { z } from "zod/v4";
@@ -49,16 +44,10 @@ export const PUT = apiHandler(
},
});
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, updateUserSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = updateUserSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const target = await prisma.user.findUnique({
where: { id },
+5 -16
View File
@@ -19,13 +19,8 @@ import {
destroyedDetails,
overwriteDetails,
} from "@/lib/sharing/audit-details";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { allergyUpdateSchema } from "@/lib/validations/allergy";
import { toAllergyDTO } from "@/lib/records/dto";
@@ -82,17 +77,11 @@ export const PATCH = apiHandler(
return apiError("Allergy not found", 404);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, allergyUpdateSchema, {
maxBytes: 16 * 1024,
errorCode: "allergy.invalid",
});
if (jsonError) return jsonError;
const parsed = allergyUpdateSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "allergy.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
// Field-by-field — only fields actually present in the body are written.
+6 -14
View File
@@ -18,8 +18,8 @@ import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { withIdempotency } from "@/lib/idempotency";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import {
@@ -92,21 +92,13 @@ async function postAllergy(request: NextRequest): Promise<Response> {
// regardless.
const { user } = await requireRecordAuth("manage", "profile");
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, allergyCreateSchema, {
maxBytes: 16 * 1024,
action: "allergy.validation-failed",
countIssues: true,
errorCode: "allergy.invalid",
});
if (jsonError) return jsonError;
const parsed = allergyCreateSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "allergy.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "allergy.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
+5 -15
View File
@@ -16,13 +16,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { withIdempotency } from "@/lib/idempotency";
import { annotate } from "@/lib/logging/context";
@@ -46,16 +41,11 @@ async function postAuditC(request: NextRequest): Promise<Response> {
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, auditCRequestSchema, {
maxBytes: 4 * 1024,
errorCode: "anamnesis.auditC.invalid",
});
if (jsonError) return jsonError;
const parsed = auditCRequestSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "anamnesis.auditC.invalid",
});
}
if (!parsed.ok) return parsed.error;
const input = parsed.data;
@@ -12,13 +12,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { annotate } from "@/lib/logging/context";
import { updateLifeAiSections } from "@/lib/profile/life-circumstances";
@@ -27,16 +22,11 @@ import { lifeAiSectionsPatchSchema } from "@/lib/validations/life-circumstances"
export const PATCH = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, lifeAiSectionsPatchSchema, {
maxBytes: 4 * 1024,
errorCode: "anamnesis.lifeSections.invalid",
});
if (jsonError) return jsonError;
const parsed = lifeAiSectionsPatchSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "anamnesis.lifeSections.invalid",
});
}
if (!parsed.ok) return parsed.error;
const outcome = await updateLifeAiSections(
user.id,
@@ -8,7 +8,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
import { apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { confirmLifeSection } from "@/lib/profile/life-circumstances";
import { lifeConfirmSchema } from "@/lib/validations/life-circumstances";
@@ -16,16 +17,11 @@ import { lifeConfirmSchema } from "@/lib/validations/life-circumstances";
export const POST = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, lifeConfirmSchema, {
maxBytes: 1024,
errorCode: "anamnesis.lifeConfirm.invalid",
});
if (jsonError) return jsonError;
const parsed = lifeConfirmSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "anamnesis.lifeConfirm.invalid",
});
}
if (!parsed.ok) return parsed.error;
const lastConfirmedAt = await confirmLifeSection(
user.id,
+4 -5
View File
@@ -14,7 +14,8 @@
import { NextRequest } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
import { apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { getSession } from "@/lib/auth/session";
import { annotate } from "@/lib/logging/context";
import {
@@ -35,12 +36,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
if (gate.refusal) return gate.refusal;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, claimPreviewSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = claimPreviewSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const preview = await previewHandover(parsed.data.token);
annotate({
+4 -10
View File
@@ -24,12 +24,8 @@
import { NextRequest } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { recordSignInDevice } from "@/lib/auth/login-alert";
import { checkPasswordStrength, hashPassword } from "@/lib/auth/password";
@@ -59,12 +55,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (gate.refusal) return gate.refusal;
const ip = gate.ip ?? "unknown";
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, claimSchema, {
maxBytes: 16 * 1024,
});
if (jsonError) return jsonError;
const parsed = claimSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const { token, username, email, password } = parsed.data;
// The link first. Without this the collision probe below would answer 409
+4 -10
View File
@@ -20,12 +20,8 @@
import { NextRequest } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { getSession } from "@/lib/auth/session";
import { prisma } from "@/lib/db";
@@ -44,12 +40,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
const gate = await claimGate(request, "sso", (await getSession()) !== null);
if (gate.refusal) return gate.refusal;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, claimSsoSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = claimSsoSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const { token, username } = parsed.data;
// The link first, so the username probe below is no oracle for anybody
+5 -15
View File
@@ -10,13 +10,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
@@ -40,16 +35,11 @@ export const PUT = apiHandler(async (request: NextRequest) => {
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, aiUsagePutSchema, {
maxBytes: 1024,
errorCode: "ai.usage.invalid",
});
if (jsonError) return jsonError;
const parsed = aiUsagePutSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "ai.usage.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { level } = parsed.data;
const settings = await loadAiUsageSettings();
+4 -12
View File
@@ -25,12 +25,8 @@
import { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -56,14 +52,10 @@ export const PATCH = apiHandler(
const { user } = await requireAuth();
const { id } = await context.params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, devicePatchSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = devicePatchSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
// Ownership boundary — scope the update by userId so a cross-user id
// can't be patched, and 404 rather than leak existence.
+5 -15
View File
@@ -20,13 +20,8 @@
import { z } from "zod";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -79,16 +74,11 @@ export const PATCH = apiHandler(async (req: Request) => {
// The body is a single boolean — bound the parse so a malformed or
// oversized payload is rejected before it is materialised.
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, patchBodySchema, {
maxBytes: 1024,
action: "auth.me.diabetes.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = patchBodySchema.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "auth.me.diabetes.patch.invalid_shape" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.hasDiabetes;
@@ -26,13 +26,8 @@
* resolved next state so the client can hard-set the optimistic update.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { getAiCapability } from "@/lib/ai/capabilities/gate";
@@ -134,18 +129,11 @@ export const PATCH = apiHandler(async (req: Request) => {
// The body is a single boolean — bound the parse so a malformed or oversized
// payload is rejected before it is materialised.
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, documentsAutoAiReadPatchSchema, {
maxBytes: 1024,
action: "auth.me.documentsAutoAiRead.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = documentsAutoAiReadPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.documentsAutoAiRead.patch.invalid_shape" },
});
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.documentsAutoAiRead;
+5 -15
View File
@@ -25,13 +25,8 @@
* update without an extra round-trip.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -81,16 +76,11 @@ export const PATCH = apiHandler(async (req: Request) => {
// The body is a single enum value — bound the parse so a malformed or
// oversized payload is rejected before it is materialised.
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, glucoseUnitPatchSchema, {
maxBytes: 1024,
action: "auth.me.glucose-unit.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = glucoseUnitPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "auth.me.glucose-unit.patch.invalid_shape" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.glucoseUnit;
@@ -19,13 +19,8 @@
* field-by-field write (no mass assignment).
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -77,18 +72,11 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, injectionSitePrefsPatchSchema, {
maxBytes: 64 * 1024,
action: "auth.me.injection-site-prefs.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = injectionSitePrefsPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.injection-site-prefs.patch.invalid_shape" },
});
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
// Dedup while preserving canonical order from the enum tuple.
const submitted = new Set(parsed.data.globalExcludedInjectionSites);
+5 -15
View File
@@ -17,13 +17,8 @@
* resolved next state so the client can hard-set the optimistic update.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -71,16 +66,11 @@ export const PATCH = apiHandler(async (req: Request) => {
// The body is a single boolean — bound the parse so a malformed or oversized
// payload is rejected before it is materialised.
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, labsLocalOcrPatchSchema, {
maxBytes: 1024,
action: "auth.me.labsLocalOcr.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = labsLocalOcrPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "auth.me.labsLocalOcr.patch.invalid_shape" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.labsLocalOcrEnabled;
+5 -19
View File
@@ -10,14 +10,8 @@
*/
import { NextRequest } from "next/server";
import { apiHandler, requireMfaManagementAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -34,19 +28,11 @@ export const PATCH = apiHandler(
const { user } = auth;
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, mfaWebauthnRenameSchema, {
maxBytes: 4 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = mfaWebauthnRenameSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const existing = await prisma.webauthnMfaCredential.findUnique({
where: { id },
@@ -8,14 +8,8 @@
*/
import { NextRequest } from "next/server";
import { apiHandler, requireMfaManagementAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -30,19 +24,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
const auth = await requireMfaManagementAuth();
const { user } = auth;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, mfaWebauthnRegisterVerifySchema, {
maxBytes: 64 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = mfaWebauthnRegisterVerifySchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const { challengeId, credential, name } = parsed.data;
const verification = await verifyMfaRegistration(
+5 -17
View File
@@ -25,13 +25,8 @@
* `res.ok` and never the status.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -85,18 +80,11 @@ export const PATCH = apiHandler(async (req: Request) => {
// The body is a single enum value — bound the parse so a malformed or
// oversized payload is rejected before it is materialised.
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, unitPreferencePatchSchema, {
maxBytes: 1024,
action: "auth.me.unit-preference.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = unitPreferencePatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.unit-preference.patch.invalid_shape" },
});
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.unitPreference;
+5 -17
View File
@@ -17,13 +17,8 @@
* assignment). Idempotent — always returns the resolved next state.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -69,18 +64,11 @@ export const PATCH = apiHandler(async (req: Request) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, useCentralCodexPatchSchema, {
maxBytes: 1024,
action: "auth.me.useCentralCodex.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = useCentralCodexPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({
action: { name: "auth.me.useCentralCodex.patch.invalid_shape" },
});
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.useCentralCodex;
+5 -15
View File
@@ -15,13 +15,8 @@
* the resolved next state in the response.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -67,16 +62,11 @@ export const PATCH = apiHandler(async (req: Request) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, workoutRoutesPatchSchema, {
maxBytes: 1024,
action: "auth.me.workoutRoutes.patch.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = workoutRoutesPatchSchema.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "auth.me.workoutRoutes.patch.invalid_shape" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const next = parsed.data.storeWorkoutRoutes;
@@ -13,13 +13,8 @@
*/
import { NextRequest } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { ensureDbCompatibility } from "@/lib/db-compat";
import { checkAuthSurfaceRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
@@ -44,19 +39,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
await ensureDbCompatibility();
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, mfaWebauthnLoginOptionsSchema, {
maxBytes: 16 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = mfaWebauthnLoginOptionsSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const challenge = await loadActiveChallenge(parsed.data.mfaTicket);
if (!challenge) {
+5 -17
View File
@@ -20,12 +20,8 @@
* `requireFreshMfa` step-up.
*/
import { NextRequest } from "next/server";
import {
apiError,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiHandler } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
@@ -81,19 +77,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
await ensureDbCompatibility();
// (3) Body — bounded, then the exact wire contract.
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, nativeHandoffTokenSchema, {
maxBytes: 16 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = nativeHandoffTokenSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const { code, codeVerifier } = parsed.data;
// (4) Lookup + flow check + replay/expiry/PKCE + atomic single-use consume.
+5 -17
View File
@@ -23,12 +23,8 @@
* `requireFreshMfa` step-up, so an SSO login can never satisfy step-up.
*/
import { NextRequest } from "next/server";
import {
apiError,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiHandler } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
@@ -87,19 +83,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
await ensureDbCompatibility();
// (3) Body — bounded, then the exact wire contract.
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, oidcNativeTokenSchema, {
maxBytes: 16 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = oidcNativeTokenSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const { code, codeVerifier } = parsed.data;
// (4–8) Lookup + replay/expiry/PKCE + atomic single-use consume.
+4 -19
View File
@@ -1,12 +1,7 @@
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { NextRequest } from "next/server";
import {
apiHandler,
@@ -24,20 +19,10 @@ export const PATCH = apiHandler(
const { user } = await requireAuth();
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, passkeyRenameSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = passkeyRenameSchema.safeParse(body);
if (!parsed.success) {
// Multi-issue, like every sibling that parses a body. The flat
// `apiError("Invalid request", 422)` this replaces discarded the issues,
// so a person renaming a passkey could not tell an empty name from one
// over the 64-character limit — the form had nothing to put beside the
// field.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const passkey = await prisma.passkey.findUnique({
where: { id },
+4 -15
View File
@@ -7,13 +7,8 @@ import {
} from "@/lib/auth/password";
import { changePasswordSchema } from "@/lib/validations/auth";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import {
recordReproofFailure,
refundReproof,
@@ -46,16 +41,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
const limited = await throttleReproof(user.id, ip, "password_change");
if (limited) return limited;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, changePasswordSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = changePasswordSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { currentPassword, newPassword } = parsed.data;
const credential = await prisma.user.findUnique({
+4 -15
View File
@@ -9,12 +9,8 @@ import { hashPassword, checkPasswordStrength } from "@/lib/auth/password";
import { createSession, getSession } from "@/lib/auth/session";
import { recordSignInDevice } from "@/lib/auth/login-alert";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
apiError,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { checkAuthSurfaceRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { ensureDbCompatibility } from "@/lib/db-compat";
import { NextRequest, NextResponse } from "next/server";
@@ -100,17 +96,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
} catch {
// Table may not exist yet; allow registration
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, registerSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = registerSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const {
email,
+5 -19
View File
@@ -42,14 +42,8 @@ import {
recentProofMethods,
requireCookieAuth,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { annotate } from "@/lib/logging/context";
@@ -68,19 +62,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
const { user, session } = await requireCookieAuth();
const ip = getClientIp(request);
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, stepUpMintSchema, {
maxBytes: 64 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = stepUpMintSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const proof = parsed.data;
// Refuse a proof that could not satisfy the gate BEFORE spending a guess on
+5 -18
View File
@@ -18,13 +18,8 @@
*/
import { NextRequest } from "next/server";
import { apiHandler, requireBearerAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { checkAuthSurfaceRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
@@ -49,19 +44,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, stepUpOptionsSchema, {
maxBytes: 4 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = stepUpOptionsSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
if (parsed.data.method === "webauthn") {
const result = await createMfaAuthenticationOptions(user.id);
+5 -19
View File
@@ -30,14 +30,8 @@
*/
import { NextRequest } from "next/server";
import { apiHandler, requireBearerAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
@@ -110,19 +104,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, stepUpMintSchema, {
maxBytes: 64 * 1024,
message: "Invalid request",
});
if (jsonError) return jsonError;
const parsed = stepUpMintSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const method: StepUpMethod = parsed.data.method;
// The shared verifier: the same challenge ownership and ceremony checks, and
+5 -15
View File
@@ -19,13 +19,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
import { withIdempotency } from "@/lib/idempotency";
@@ -47,16 +42,11 @@ async function mergeBiomarkers(
const { user } = await requireRecordAuth("manage", "labs");
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, biomarkerMergeSchema, {
maxBytes: 4 * 1024,
errorCode: "labs.biomarker.merge.invalid",
});
if (jsonError) return jsonError;
const parsed = biomarkerMergeSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.biomarker.merge.invalid",
});
}
if (!parsed.ok) return parsed.error;
if (parsed.data.sourceIds.includes(id)) {
return apiError("The target cannot be one of the sources", 422, {
errorCode: "labs.biomarker.merge.invalid",
+4 -11
View File
@@ -23,12 +23,8 @@ import { NextRequest } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { checkAuthSurfaceRateLimit } from "@/lib/rate-limit";
import { shouldEmitSecureCookie } from "@/lib/auth/secure-cookie";
import { resolveShareGateState } from "@/lib/clinician-share/resolve-share-token";
@@ -69,13 +65,10 @@ export const POST = apiHandler(
return apiError("Too many attempts", 429);
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, unlockShareLinkSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = unlockShareLinkSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error);
if (!parsed.ok) return parsed.error;
// A token with no live gate, or one carrying no passphrase, cannot be
// unlocked. `verifyPassphrase` is constant-time and returns false for a
+4 -14
View File
@@ -28,13 +28,8 @@
* prose and must not land in plaintext audit details.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
@@ -111,15 +106,10 @@ export const POST = apiHandler(async (req: Request) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, aboutMeAdoptSchema, {
maxBytes: 16 * 1024,
});
if (jsonError) return jsonError;
const parsed = aboutMeAdoptSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { question, answer } = parsed.data;
const normAnswer = normalise(answer);
+4 -13
View File
@@ -25,12 +25,8 @@
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { requireModuleEnabled } from "@/lib/modules/gate";
@@ -77,15 +73,10 @@ export const PATCH = apiHandler(async (req: NextRequest, ctx: RouteCtx) => {
const { id } = await ctx.params;
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, coachPlanPatchSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = coachPlanPatchSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
// Field-by-field data assembly (no mass assignment): only the lifecycle
// fields the body carried are written; the metric + encrypted text are never
@@ -24,13 +24,8 @@ import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit } from "@/lib/rate-limit";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import {
@@ -65,13 +60,12 @@ async function postAction(request: NextRequest): Promise<Response> {
return apiError("coach.suggestion.rateLimited", 429);
}
const { data: body, error: jsonError } = await safeJson(request, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = coachReminderSuggestionActionSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
const parsed = await parseJsonBody(
request,
coachReminderSuggestionActionSchema,
{ maxBytes: 4 * 1024 },
);
if (!parsed.ok) return parsed.error;
const { cadenceId, action } = parsed.data;
if (!isCadenceId(cadenceId)) {
+4 -11
View File
@@ -23,12 +23,8 @@
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { requireModuleEnabled } from "@/lib/modules/gate";
@@ -71,13 +67,10 @@ export const PATCH = apiHandler(async (req: NextRequest, ctx: RouteCtx) => {
const { id } = await ctx.params;
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, coachReminderPatchSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = coachReminderPatchSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
// Field-by-field (no mass assignment): only the lifecycle / timing fields the
// body carried are written; the note + encrypted text are never touched here.
+4 -11
View File
@@ -22,12 +22,8 @@
* / due / surfaced), soonest-due first.
*/
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, returnAllZodIssues } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { requireModuleEnabled } from "@/lib/modules/gate";
@@ -151,13 +147,10 @@ export const POST = apiHandler(async (req: Request) => {
);
if (!rate.allowed) return apiError("Too many requests", 429);
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, coachReminderCreateSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = coachReminderCreateSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
// Resolve the optional `when` against the closed grammar; an invalid token
// 422s rather than silently dropping the timing the user asked for.
+4 -12
View File
@@ -26,13 +26,8 @@ import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit } from "@/lib/rate-limit";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
@@ -63,13 +58,10 @@ async function postAction(request: NextRequest): Promise<Response> {
);
if (!rate.allowed) return apiError("coach.action.rateLimited", 429);
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, coachSuggestedActionSchema, {
maxBytes: 4 * 1024,
});
if (jsonError) return jsonError;
const parsed = coachSuggestedActionSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const action = parsed.data;
// ── checkup.create → a preventive-care Vorsorge reminder ─────
+5 -13
View File
@@ -17,12 +17,8 @@
* grants.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { checkConsentRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
@@ -52,15 +48,11 @@ export const POST = apiHandler(async (req: Request) => {
let confirmed = false;
const contentType = req.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, webConsentGrantBody, {
maxBytes: 1024,
action: "consent.ai.web-grant.invalid_shape",
});
if (jsonError) return jsonError;
const parsed = webConsentGrantBody.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "consent.ai.web-grant.invalid_shape" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
confirmed = parsed.data.confirm === true;
}
if (!confirmed) {
@@ -18,12 +18,8 @@ import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { invalidateUserCorrelationPatterns } from "@/lib/cache/invalidate";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
@@ -43,15 +39,10 @@ export const POST = apiHandler(
const { user } = await requireAuth();
const { id } = await params;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, restoreSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = restoreSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { count } = await prisma.customMetricEntry.updateMany({
where: {
+7 -20
View File
@@ -17,13 +17,8 @@ import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { overwriteDetails } from "@/lib/sharing/audit-details";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { requireCycleEnabled } from "@/lib/cycle/gate";
import {
ensureCycleForBleedingDay,
@@ -68,21 +63,13 @@ export const PATCH = apiHandler(
return apiError("Day-log not found", 404);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, cycleDayLogPatchSchema, {
maxBytes: 64 * 1024,
action: "cycle.day-log.patch.validation-failed",
countIssues: true,
errorCode: "cycle.day-log.invalid",
});
if (jsonError) return jsonError;
const parsed = cycleDayLogPatchSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "cycle.day-log.patch.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "cycle.day-log.invalid",
});
}
if (!parsed.ok) return parsed.error;
const body = parsed.data;
@@ -9,13 +9,8 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
@@ -53,16 +48,11 @@ export const PATCH = apiHandler(
const { key } = await params;
if (!isCustomSymptomKey(key)) return apiError("Not a custom symptom", 404);
const { data: rawJsonBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, updateCustomSymptomSchema, {
maxBytes: 64 * 1024,
errorCode: "cycle.symptom.custom.invalid",
});
if (jsonError) return jsonError;
const parsed = updateCustomSymptomSchema.safeParse(rawJsonBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "cycle.symptom.custom.invalid",
});
}
if (!parsed.ok) return parsed.error;
const owned = await prisma.cycleSymptom.findFirst({
where: { key, userId: user.id },
+5 -15
View File
@@ -17,13 +17,8 @@
import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { withIdempotency } from "@/lib/idempotency";
@@ -107,16 +102,11 @@ async function postCustomSymptom(request: NextRequest): Promise<Response> {
return apiError("Too many requests, try again later", 429);
}
const { data: rawJsonBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, createCustomSymptomSchema, {
maxBytes: 64 * 1024,
errorCode: "cycle.symptom.custom.invalid",
});
if (jsonError) return jsonError;
const parsed = createCustomSymptomSchema.safeParse(rawJsonBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "cycle.symptom.custom.invalid",
});
}
if (!parsed.ok) return parsed.error;
const activeCount = await prisma.cycleSymptom.count({
where: { userId: user.id, isActive: true },
+4 -13
View File
@@ -18,12 +18,8 @@
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
@@ -52,15 +48,10 @@ export const POST = apiHandler(async (req: NextRequest) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, dismissPriorityItemSchema, {
maxBytes: 1024,
});
if (jsonError) return jsonError;
const parsed = dismissPriorityItemSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { itemKey } = parsed.data;
@@ -29,9 +29,9 @@ import {
apiError,
apiSuccess,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
@@ -101,21 +101,12 @@ export const POST = apiHandler(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, documentChatRequestSchema, {
maxBytes: 64 * 1024,
errorCode: "documents.chat.invalid",
message: "Invalid document chat request",
});
if (jsonError) return jsonError;
const parsed = documentChatRequestSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid document chat request",
sanitiseZodIssues(parsed.error.issues),
422,
{
errorCode: "documents.chat.invalid",
},
);
}
if (!parsed.ok) return parsed.error;
const { conversationId, message, locale: bodyLocale } = parsed.data;
const rl = await checkRateLimit(
@@ -17,13 +17,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { commitApprovedFact, FactCommitError } from "@/lib/documents/commit";
@@ -56,13 +51,10 @@ async function confirmDocument(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, inboundConfirmSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = inboundConfirmSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const approved: { factId: string; recordType: string; recordId: string }[] =
[];
@@ -11,12 +11,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { prisma } from "@/lib/db";
import {
encryptFactData,
@@ -100,13 +96,10 @@ export const PATCH = apiHandler(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, inboundFactEditSchema, {
maxBytes: 16 * 1024,
});
if (jsonError) return jsonError;
const parsed = inboundFactEditSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
// The edit cannot change the fact's resource type.
if (parsed.data.factType !== fact.factType) {
+7 -22
View File
@@ -10,14 +10,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import {
@@ -164,20 +158,11 @@ export const PATCH = apiHandler(
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request);
if (jsonError) return jsonError;
const parsed = documentUpdateSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid document update",
sanitiseZodIssues(parsed.error.issues),
422,
{
errorCode: "documents.inbound.invalidUpdate",
},
);
}
const parsed = await parseJsonBody(request, documentUpdateSchema, {
errorCode: "documents.inbound.invalidUpdate",
message: "Invalid document update",
});
if (!parsed.ok) return parsed.error;
const existing = await prisma.inboundDocument.findFirst({
where: { id, userId: user.id, deletedAt: null },
+4 -14
View File
@@ -13,13 +13,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { linkTargets, unlinkTargets } from "@/lib/links";
@@ -61,15 +56,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, documentBulkSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = documentBulkSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { ids, action, kind, episodeId, encounterId } = parsed.data;
const uniqueIds = [...new Set(ids)];
@@ -14,7 +14,8 @@
import { z } from "zod/v4";
import { apiHandler, requireAuth, requireRecordAuth } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
import { apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { prisma, toJson } from "@/lib/db";
import {
DOCUMENTS_LAYOUT_ARRANGEMENTS,
@@ -67,16 +68,11 @@ export const PUT = apiHandler(async (request: Request) => {
const gate = await requireModuleEnabled(user.id, "inboundDocuments");
if (!gate.enabled) return gate.response;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, layoutPutSchema, {
maxBytes: 1024,
action: "documents.layout.validation-failed",
});
if (jsonError) return jsonError;
const parsed = layoutPutSchema.safeParse(body);
if (!parsed.success) {
annotate({ action: { name: "documents.layout.validation-failed" } });
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
// Preserve-when-absent: one stored read fills whichever field the body
// left out. Only fields the user has chosen are written, so a field never
@@ -36,13 +36,8 @@ import { Buffer } from "node:buffer";
import { NextRequest, NextResponse } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { getAiCapability } from "@/lib/ai/capabilities/gate";
import { prisma } from "@/lib/db";
import {
@@ -176,18 +171,11 @@ export const POST = apiHandler(
if (system instanceof Response) return system;
const userId = auth.user.id;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, documentSourceImportSchema, {
maxBytes: 4 * 1024,
message: "Invalid import request",
});
if (jsonError) return jsonError;
const parsed = documentSourceImportSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid import request",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const { sourceId, kind, link } = parsed.data;
// Only an id this system could have issued goes into a request path.
if (!isSourceDocumentId(system, sourceId)) {
@@ -18,14 +18,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
getClientIp,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { decrypt, encrypt } from "@/lib/crypto";
import { prisma } from "@/lib/db";
@@ -70,18 +64,11 @@ export const PUT = apiHandler(
);
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, documentSourceSaveSchema, {
maxBytes: 8 * 1024,
message: "Invalid connection details",
});
if (jsonError) return jsonError;
const parsed = documentSourceSaveSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid connection details",
sanitiseZodIssues(parsed.error.issues),
422,
);
}
if (!parsed.ok) return parsed.error;
const verdict = evaluateSourceBaseUrl(parsed.data.baseUrl);
if (!verdict.ok) {
+5 -15
View File
@@ -20,12 +20,8 @@ import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import {
apiSuccess,
apiError,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { encounterLinkSchema } from "@/lib/validations/encounters";
import { linkTargets, unlinkTargets } from "@/lib/links";
import { loadEncounterLinks } from "@/lib/encounters/service";
@@ -42,17 +38,11 @@ async function handle(
const { id } = await params;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, encounterLinkSchema, {
maxBytes: 16 * 1024,
errorCode: "encounter.link.invalid",
});
if (jsonError) return jsonError;
const parsed = encounterLinkSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "encounter.link.invalid",
});
}
if (!parsed.ok) return parsed.error;
const apply = verb === "link" ? linkTargets : unlinkTargets;
const result = await apply(prisma, {
+5 -16
View File
@@ -22,13 +22,8 @@ import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { overwriteDetails } from "@/lib/sharing/audit-details";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { encounterUpdateSchema } from "@/lib/validations/encounters";
import { encounterKindLabel } from "@/lib/encounters/kind-label";
@@ -107,17 +102,11 @@ export const PATCH = apiHandler(
return apiError("Visit not found", 404);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, encounterUpdateSchema, {
maxBytes: 32 * 1024,
errorCode: "encounter.invalid",
});
if (jsonError) return jsonError;
const parsed = encounterUpdateSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "encounter.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
const { timezone, locale } = await resolveOwnerNotificationContext(user.id);
+6 -14
View File
@@ -23,8 +23,8 @@ import {
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiListSuccess } from "@/lib/api/list-cap";
import { combinedCap, listPage } from "@/lib/http/list-query";
import { withIdempotency } from "@/lib/idempotency";
@@ -180,21 +180,13 @@ export const POST = apiHandler(withIdempotency<[NextRequest]>(postEncounter));
async function postEncounter(request: NextRequest): Promise<Response> {
const { user, grantId } = await requireRecordAuth("write", "profile");
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, encounterCreateSchema, {
maxBytes: 32 * 1024,
action: "encounter.visit.validation-failed",
countIssues: true,
errorCode: "encounter.invalid",
});
if (jsonError) return jsonError;
const parsed = encounterCreateSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "encounter.visit.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "encounter.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
const occurredAt = new Date(entry.occurredAt);
+5 -15
View File
@@ -15,12 +15,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import {
@@ -49,17 +45,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
return apiError("Too many requests. Please retry shortly.", 429);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, environmentBackfillSchema, {
maxBytes: 4 * 1024,
errorCode: "environment.invalid",
});
if (jsonError) return jsonError;
const parsed = environmentBackfillSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "environment.invalid",
});
}
if (!parsed.ok) return parsed.error;
const profile = await prisma.user.findUnique({
where: { id: user.id },
+7 -14
View File
@@ -10,7 +10,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
import { apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { prisma } from "@/lib/db";
@@ -26,21 +27,13 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const gate = await requireModuleEnabled(user.id, "environment");
if (!gate.enabled) return gate.response;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, homeLocationSchema, {
maxBytes: 4 * 1024,
action: "environment.home.validation-failed",
countIssues: true,
errorCode: "environment.invalid",
});
if (jsonError) return jsonError;
const parsed = homeLocationSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "environment.home.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "environment.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { lat, lon, label, timezone } = parsed.data;
// Stamp the effective-from instant: from now on this home resolves days; days
+5 -10
View File
@@ -13,7 +13,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
import { apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { prisma } from "@/lib/db";
@@ -27,17 +28,11 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
const gate = await requireModuleEnabled(user.id, "environment");
if (!gate.enabled) return gate.response;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, environmentPreferencesSchema, {
maxBytes: 1024,
errorCode: "environment.invalid",
});
if (jsonError) return jsonError;
const parsed = environmentPreferencesSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "environment.invalid",
});
}
if (!parsed.ok) return parsed.error;
const before = await prisma.user.findUnique({
where: { id: user.id },
+7 -14
View File
@@ -14,7 +14,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, returnAllZodIssues, safeJson } from "@/lib/api-response";
import { apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { prisma } from "@/lib/db";
@@ -29,21 +30,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
const gate = await requireModuleEnabled(user.id, "environment");
if (!gate.enabled) return gate.response;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, travelLocationSchema, {
maxBytes: 4 * 1024,
action: "environment.travel.validation-failed",
countIssues: true,
errorCode: "environment.invalid",
});
if (jsonError) return jsonError;
const parsed = travelLocationSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "environment.travel.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "environment.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
const location = {
+4 -13
View File
@@ -31,12 +31,8 @@ import { z } from "zod/v4";
import { apiHandler, requireRecentProof } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { checkRateLimit } from "@/lib/rate-limit";
import { randomBytes } from "node:crypto";
import { createReadStream } from "node:fs";
@@ -105,15 +101,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
// The body is a single passphrase string — small and bounded. 8 KB is far
// above any legitimate passphrase while rejecting a large body before parse.
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, encryptedExportSchema, {
maxBytes: 8 * 1024,
});
if (jsonError) return jsonError;
const parsed = encryptedExportSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error);
}
if (!parsed.ok) return parsed.error;
const { passphrase } = parsed.data;
await auth.commitElevation();
+4 -13
View File
@@ -29,12 +29,8 @@ import {
} from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { checkRateLimit } from "@/lib/rate-limit";
import { requireModuleEnabled } from "@/lib/modules/gate";
import { prisma, toJson } from "@/lib/db";
@@ -122,15 +118,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
// The selection payload is small and bounded. 64 KB is far above any
// legitimate one while still rejecting a multi-megabyte body before it
// reaches `JSON.parse`. A DoS ceiling, not a tight bound.
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, exportSelectionSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = exportSelectionSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error);
}
if (!parsed.ok) return parsed.error;
const input = parsed.data;
const minted = selectionFromRequest(input.selection);
+5 -16
View File
@@ -14,13 +14,8 @@ import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { overwriteDetails } from "@/lib/sharing/audit-details";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { familyHistoryUpdateSchema } from "@/lib/validations/family-history";
import { toFamilyHistoryEntryDTO } from "@/lib/records/dto";
@@ -76,17 +71,11 @@ export const PATCH = apiHandler(
return apiError("Family history entry not found", 404);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, familyHistoryUpdateSchema, {
maxBytes: 16 * 1024,
errorCode: "familyHistory.invalid",
});
if (jsonError) return jsonError;
const parsed = familyHistoryUpdateSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "familyHistory.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
// Field-by-field — only fields actually present in the body are written.
+6 -14
View File
@@ -19,8 +19,8 @@ import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { withIdempotency } from "@/lib/idempotency";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import {
@@ -80,21 +80,13 @@ async function postFamilyHistory(request: NextRequest): Promise<Response> {
// it (`recordSharing.anamnesis.relativeNote`).
const { user } = await requireRecordAuth("manage", "profile");
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, familyHistoryCreateSchema, {
maxBytes: 16 * 1024,
action: "family-history.validation-failed",
countIssues: true,
errorCode: "familyHistory.invalid",
});
if (jsonError) return jsonError;
const parsed = familyHistoryCreateSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "family-history.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "familyHistory.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
@@ -24,8 +24,8 @@ import {
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { requireIllnessEnabled } from "@/lib/illness/gate";
import {
illnessDayLogInputSchema,
@@ -152,21 +152,13 @@ export const POST = apiHandler(
const owned = await loadOwnedEpisode(id, user.id);
if (!owned.ok) return owned.response;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, illnessDayLogInputSchema, {
maxBytes: 16 * 1024,
action: "illness.day-log.validation-failed",
countIssues: true,
errorCode: "illness.day-log.invalid",
});
if (jsonError) return jsonError;
const parsed = illnessDayLogInputSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "illness.day-log.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "illness.day-log.invalid",
});
}
if (!parsed.ok) return parsed.error;
const tz = user.timezone ?? DEFAULT_TIMEZONE;
const result = await upsertIllnessDayLog(user.id, id, parsed.data, tz);
+5 -16
View File
@@ -15,13 +15,8 @@ import { invalidateUserHealthContext } from "@/lib/cache/invalidate";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { overwriteDetails } from "@/lib/sharing/audit-details";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { requireIllnessEnabled } from "@/lib/illness/gate";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { illnessEpisodeUpdateSchema } from "@/lib/validations/illness";
@@ -98,17 +93,11 @@ export const PATCH = apiHandler(
return apiError("Episode not found", 404);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, illnessEpisodeUpdateSchema, {
maxBytes: 16 * 1024,
errorCode: "illness.episode.invalid",
});
if (jsonError) return jsonError;
const parsed = illnessEpisodeUpdateSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "illness.episode.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
// Window invariant against the MERGED state: a partial edit touching only
+6 -14
View File
@@ -22,8 +22,8 @@ import {
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { requireIllnessEnabled } from "@/lib/illness/gate";
import { withIdempotency } from "@/lib/idempotency";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
@@ -108,21 +108,13 @@ async function postEpisode(request: NextRequest): Promise<Response> {
const gate = await requireIllnessEnabled(user.id);
if (!gate.enabled) return gate.response;
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, illnessEpisodeCreateSchema, {
maxBytes: 16 * 1024,
action: "illness.episode.validation-failed",
countIssues: true,
errorCode: "illness.episode.invalid",
});
if (jsonError) return jsonError;
const parsed = illnessEpisodeCreateSchema.safeParse(rawBody);
if (!parsed.success) {
annotate({
action: { name: "illness.episode.validation-failed" },
meta: { issue_count: parsed.error.issues.length },
});
return returnAllZodIssues(parsed.error, 422, {
errorCode: "illness.episode.invalid",
});
}
if (!parsed.ok) return parsed.error;
const entry = parsed.data;
@@ -11,13 +11,8 @@ import { type NextRequest } from "next/server";
import { requireAiCapability } from "@/lib/ai/capabilities/gate";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
@@ -72,21 +67,12 @@ export const POST = apiHandler(
return response;
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, coachAttachmentCreateSchema, {
maxBytes: 4 * 1024,
errorCode: "coach.fenced.invalid",
message: "Invalid attachment request",
});
if (jsonError) return jsonError;
const parsed = coachAttachmentCreateSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid attachment request",
sanitiseZodIssues(parsed.error.issues),
422,
{
errorCode: "coach.fenced.invalid",
},
);
}
if (!parsed.ok) return parsed.error;
const { documentId } = parsed.data;
// Ownership of the conversation — NOT filtered on `documentScoped` (this is
+5 -16
View File
@@ -13,13 +13,8 @@ import type { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiSuccess,
apiError,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { COACH_CONVERSATION_TITLE_MAX } from "@/lib/ai/coach/types";
@@ -75,17 +70,11 @@ export const PATCH = apiHandler(async (request: NextRequest, ctx: RouteCtx) => {
const { id } = await ctx.params;
if (!id) return apiError("coach.conversation.notFound", 404);
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, renameConversationSchema, {
maxBytes: 4 * 1024,
errorCode: "coach.conversation.invalidTitle",
});
if (jsonError) return jsonError;
const parsed = renameConversationSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "coach.conversation.invalidTitle",
});
}
if (!parsed.ok) return parsed.error;
const renamed = await renameConversation(auth.user.id, id, parsed.data.title);
if (!renamed) {
+6 -19
View File
@@ -17,12 +17,8 @@ import { type NextRequest } from "next/server";
import { requireAiCapability } from "@/lib/ai/capabilities/gate";
import { apiHandler, requireAuth, HttpError } from "@/lib/api-handler";
import {
apiError,
apiValidationError,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { apiError } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
@@ -65,21 +61,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
await requireAiCapability("coach", { pickDecides: true });
await requireAiCapability("documentAi", { pickDecides: true });
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, fencedChatRequestSchema, {
maxBytes: 64 * 1024,
errorCode: "coach.fenced.invalid",
message: "Invalid fenced chat request",
});
if (jsonError) return jsonError;
const parsed = fencedChatRequestSchema.safeParse(body);
if (!parsed.success) {
return apiValidationError(
"Invalid fenced chat request",
sanitiseZodIssues(parsed.error.issues),
422,
{
errorCode: "coach.fenced.invalid",
},
);
}
if (!parsed.ok) return parsed.error;
const {
conversationId,
message,
+4 -10
View File
@@ -30,12 +30,8 @@
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
@@ -73,12 +69,10 @@ export const PATCH = apiHandler(async (req: NextRequest, ctx: RouteCtx) => {
}
const { id } = await ctx.params;
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, coachFactPatchSchema, {
maxBytes: 2 * 1024,
});
if (jsonError) return jsonError;
const parsed = coachFactPatchSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const text = parsed.data.fact.replace(/\s+/g, " ");
const live = {
+4 -10
View File
@@ -31,12 +31,8 @@
* Erasure that depends on the feature being on is no erasure.
*/
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec";
@@ -142,12 +138,10 @@ export const POST = apiHandler(async (req: Request) => {
return response;
}
const { data: body, error: jsonError } = await safeJson(req, {
const parsed = await parseJsonBody(req, coachFactCreateSchema, {
maxBytes: 1024,
});
if (jsonError) return jsonError;
const parsed = coachFactCreateSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const remembered = await rememberMessageAsFact({
userId: user.id,
+7 -8
View File
@@ -7,8 +7,8 @@ import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { invalidateUserCorrelationPatterns } from "@/lib/cache/invalidate";
import { prisma } from "@/lib/db";
@@ -28,13 +28,12 @@ export const PATCH = apiHandler(
const parsedId = patternIdSchema.safeParse((await params).id);
if (!parsedId.success) return returnAllZodIssues(parsedId.error, 422);
const id = parsedId.data;
const { data: body, error: jsonError } = await safeJson(request, {
maxBytes: 1024,
});
if (jsonError) return jsonError;
const parsed = updateCorrelationPatternSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
const parsed = await parseJsonBody(
request,
updateCorrelationPatternSchema,
{ maxBytes: 1024 },
);
if (!parsed.ok) return parsed.error;
const pattern = await prisma.correlationPattern.findFirst({
where: { id, userId: user.id, isCurrent: true },
+5 -14
View File
@@ -11,12 +11,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { prisma } from "@/lib/db";
import { linkTargets, unlinkTargets } from "@/lib/links";
import { annotate } from "@/lib/logging/context";
@@ -32,16 +28,11 @@ async function handle(
const { user } = await requireAuth();
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, labResultLinkSchema, {
maxBytes: 16 * 1024,
errorCode: "labs.link.invalid",
});
if (jsonError) return jsonError;
const parsed = labResultLinkSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.link.invalid",
});
}
if (!parsed.ok) return parsed.error;
const apply = verb === "link" ? linkTargets : unlinkTargets;
const result = await apply(prisma, {
+5 -16
View File
@@ -15,13 +15,8 @@ import { z } from "zod/v4";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { prisma } from "@/lib/db";
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
import { withIdempotency } from "@/lib/idempotency";
@@ -59,17 +54,11 @@ async function postRestore(request: NextRequest): Promise<Response> {
return apiError("Too many restore requests, try again later", 429);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, restoreSchema, {
maxBytes: 64 * 1024,
errorCode: "labs.restore.invalid",
});
if (jsonError) return jsonError;
const parsed = restoreSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "labs.restore.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { ids } = parsed.data;
+4 -10
View File
@@ -16,8 +16,8 @@ import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import {
lifeEventIdPathSchema,
@@ -48,17 +48,11 @@ export const PATCH = apiHandler(
const existing = await findOwnLifeEvent(user.id, path.data.id);
if (!existing) return notFound();
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, lifeEventUpdateSchema, {
maxBytes: 16 * 1024,
errorCode: "life_event.invalid",
});
if (jsonError) return jsonError;
const parsed = lifeEventUpdateSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "life_event.invalid",
});
}
if (!parsed.ok) return parsed.error;
const merged = mergeLifeEventEdit(existing, parsed.data);
if (!merged.ok) {
return returnAllZodIssues(merged.error, 422, {
+5 -15
View File
@@ -20,12 +20,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import {
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { lifeEventCreateSchema } from "@/lib/day/wire-schemas";
import { withIdempotency } from "@/lib/idempotency";
@@ -51,17 +47,11 @@ export const POST = apiHandler(withIdempotency<[NextRequest]>(postLifeEvent));
async function postLifeEvent(request: NextRequest): Promise<Response> {
const { user } = await requireAuth();
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, lifeEventCreateSchema, {
maxBytes: 16 * 1024,
errorCode: "life_event.invalid",
});
if (jsonError) return jsonError;
const parsed = lifeEventCreateSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "life_event.invalid",
});
}
if (!parsed.ok) return parsed.error;
const created = await createLifeEvent(user.id, parsed.data);
@@ -6,12 +6,8 @@ import {
requireCookieAuth,
requireFreshMfa,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { prisma } from "@/lib/db";
import { annotate } from "@/lib/logging/context";
import {
@@ -92,12 +88,12 @@ export const POST = apiHandler(
if (!rateLimit.allowed) {
return apiError("Too many invitations, try again later", 429);
}
const { data: body, error: jsonError } = await safeJson(request, {
maxBytes: 8 * 1024,
});
if (jsonError) return jsonError;
const parsed = inviteManagedProfileGuardianSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
const parsed = await parseJsonBody(
request,
inviteManagedProfileGuardianSchema,
{ maxBytes: 8 * 1024 },
);
if (!parsed.ok) return parsed.error;
const { id } = await params;
const invitee = await prisma.user.findFirst({
@@ -23,12 +23,8 @@ import {
requireCookieAuth,
requireFreshMfa,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { buildHandoverUrl } from "@/lib/auth/handover-token";
import { annotate } from "@/lib/logging/context";
import { handoverMethods } from "@/lib/managed-profiles/claim-gate";
@@ -111,12 +107,10 @@ export const POST = apiHandler(
if (handoverMethods().length === 0) return noMethodRefusal();
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, createHandoverSchema, {
maxBytes: 16 * 1024,
});
if (jsonError) return jsonError;
const parsed = createHandoverSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
let created;
try {
+4 -11
View File
@@ -6,12 +6,8 @@ import {
requireCookieAuth,
requireFreshMfa,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import {
deleteManagedProfile,
ManagedProfileLifecycleError,
@@ -132,13 +128,10 @@ export const PATCH = apiHandler(
}
const { id } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, updateManagedProfileSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = updateManagedProfileSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
// The parsed patch goes to the service, which assembles the Prisma payload
// field by field. One assembly rather than one here and one there: two
+4 -11
View File
@@ -5,12 +5,8 @@ import {
MFA_STEP_UP_MAX_AGE_SECONDS,
requireFreshMfa,
} from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { createManagedProfile } from "@/lib/managed-profiles/create";
import { toManagedProfileView } from "@/lib/managed-profiles/lifecycle";
@@ -48,13 +44,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (!rateLimit.allowed) {
return apiError("Too many profiles created, try again later", 429);
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, createManagedProfileSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = createManagedProfileSchema.safeParse(body);
if (!parsed.success) return returnAllZodIssues(parsed.error, 422);
if (!parsed.ok) return parsed.error;
const { profile, creatorGrant } = await createManagedProfile({
creatorId: user.id,
+4 -13
View File
@@ -22,12 +22,8 @@
import { NextRequest } from "next/server";
import { apiHandler, requireAuth, requireRecentProof } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { issueApiToken } from "@/lib/auth/issue-token";
@@ -84,15 +80,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
return apiError("API is globally disabled", 403);
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, createMcpTokenSchema, {
maxBytes: 16 * 1024,
});
if (jsonError) return jsonError;
const parsed = createMcpTokenSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
// Build the permission array explicitly from the closed scope choice —
// never spread, never wildcard. `read` → read-only; `read_write` → read plus
+5 -16
View File
@@ -20,13 +20,8 @@ import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { withIdempotency } from "@/lib/idempotency";
import { checkRateLimit } from "@/lib/rate-limit";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
@@ -64,17 +59,11 @@ async function postBulkDelete(request: NextRequest): Promise<Response> {
return apiError("Too many bulk deletions, try again later", 429);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, bulkDeleteSchema, {
maxBytes: 256 * 1024,
errorCode: "measurement.bulk-delete.invalid",
});
if (jsonError) return jsonError;
const parsed = bulkDeleteSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "measurement.bulk-delete.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { ids } = parsed.data;
const now = new Date();
+5 -16
View File
@@ -22,13 +22,8 @@ import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { withIdempotency } from "@/lib/idempotency";
import { checkRateLimit } from "@/lib/rate-limit";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
@@ -65,17 +60,11 @@ async function postRestore(request: NextRequest): Promise<Response> {
return apiError("Too many restore requests, try again later", 429);
}
const { data: rawBody, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, restoreSchema, {
maxBytes: 256 * 1024,
errorCode: "measurement.restore.invalid",
});
if (jsonError) return jsonError;
const parsed = restoreSchema.safeParse(rawBody);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "measurement.restore.invalid",
});
}
if (!parsed.ok) return parsed.error;
const { ids } = parsed.data;
@@ -5,13 +5,8 @@
*/
import { NextRequest } from "next/server";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import { invalidateUserMedications } from "@/lib/cache/invalidate";
@@ -44,16 +39,11 @@ export const PATCH = apiHandler(
const { user } = await requireAuth();
const { id, courseId } = await params;
const { data: raw, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, updateMedicationCourseSchema, {
maxBytes: 16 * 1024,
errorCode: "medications.course.invalid",
});
if (jsonError) return jsonError;
const parsed = updateMedicationCourseSchema.safeParse(raw);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "medications.course.invalid",
});
}
if (!parsed.ok) return parsed.error;
try {
const updated = await updateCourse({
+5 -15
View File
@@ -13,13 +13,8 @@
*/
import { NextRequest } from "next/server";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import { invalidateUserMedications } from "@/lib/cache/invalidate";
@@ -71,16 +66,11 @@ async function postCourse(
const { user } = await requireAuth();
const { id } = await params;
const { data: raw, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, createMedicationCourseSchema, {
maxBytes: 16 * 1024,
errorCode: "medications.course.invalid",
});
if (jsonError) return jsonError;
const parsed = createMedicationCourseSchema.safeParse(raw);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "medications.course.invalid",
});
}
if (!parsed.ok) return parsed.error;
try {
const created = await createCourse({
@@ -1,12 +1,8 @@
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import {
apiSuccess,
apiError,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiSuccess, apiError } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
import { efficacyTargetOverrideSchema } from "@/lib/validations/medication-efficacy";
@@ -32,15 +28,10 @@ export const PUT = apiHandler(
const guard = await assertMedicationOwnership(id, user.id);
if (guard) return guard;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, efficacyTargetOverrideSchema, {
maxBytes: 8 * 1024,
});
if (jsonError) return jsonError;
const parsed = efficacyTargetOverrideSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { clear, measurementType, biomarkerId, primary } = parsed.data;
// Clearing reverts to the derived resolution — drop every override row.
+4 -15
View File
@@ -18,13 +18,8 @@ import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import { annotate } from "@/lib/logging/context";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { glp1PostBodySchema } from "@/lib/validations/medication";
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
@@ -283,16 +278,10 @@ async function postGlp1Update(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, glp1PostBodySchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = glp1PostBodySchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const ip = getClientIp(request);
@@ -17,13 +17,8 @@ import { NextRequest } from "next/server";
import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { auditLog } from "@/lib/auth/audit";
import { invalidateUserMedications } from "@/lib/cache/invalidate";
import { withIdempotency } from "@/lib/idempotency";
@@ -91,16 +86,11 @@ async function putIngredients(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, medicationIngredientsPutSchema, {
maxBytes: 16 * 1024,
errorCode: "medications.ingredients.invalid",
});
if (jsonError) return jsonError;
const parsed = medicationIngredientsPutSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422, {
errorCode: "medications.ingredients.invalid",
});
}
if (!parsed.ok) return parsed.error;
const check = canonicalComposition(parsed.data.ingredients);
if (!check.ok) {
@@ -2,13 +2,8 @@ import { prisma } from "@/lib/db";
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { bulkDeleteIntakeEventsSchema } from "@/lib/validations/medication";
import { restoreForIntake } from "@/lib/medications/inventory/consumption";
@@ -84,14 +79,10 @@ export const POST = apiHandler(
});
}
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, bulkDeleteIntakeEventsSchema, {
maxBytes: 256 * 1024,
});
if (jsonError) return jsonError;
const parsed = bulkDeleteIntakeEventsSchema.safeParse(body);
if (!parsed.success) {
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const { eventIds } = parsed.data;
// Tz the rollups are anchored on. The detail-page caller persists
@@ -28,13 +28,8 @@ import {
overwriteDetails,
} from "@/lib/sharing/audit-details";
import { annotate } from "@/lib/logging/context";
import {
apiError,
apiSuccess,
getClientIp,
returnAllZodIssues,
safeJson,
} from "@/lib/api-response";
import { apiError, apiSuccess, getClientIp } from "@/lib/api-response";
import { parseJsonBody } from "@/lib/api/body";
import { updateInventoryItemSchema } from "@/lib/validations/medication";
import {
computeExpiresAt,
@@ -53,16 +48,10 @@ export const PATCH = apiHandler(
const { user } = await requireRecordAuth("manage", "medications");
const { id, itemId } = await params;
const { data: body, error: jsonError } = await safeJson(request, {
const parsed = await parseJsonBody(request, updateInventoryItemSchema, {
maxBytes: 64 * 1024,
});
if (jsonError) return jsonError;
const parsed = updateInventoryItemSchema.safeParse(body);
if (!parsed.success) {
// v1.4.43 — multi-issue 422.
return returnAllZodIssues(parsed.error, 422);
}
if (!parsed.ok) return parsed.error;
const {
markAsFirstUseAt,
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more