chore(release): v1.4.45 — analytics 9 s perf fix + audit-marathon closure + v1.4.44 hotfix merged

Audit-marathon close: 14 waves + W10 QA + reconcile. Perf fix (analytics 9 s -> 2-3 s), Zod multi-issue rollout (41 routes), Withings parked-state automation, mobile-UI WCAG 2.5.5 close, QoL i18n + offline banner + account delete, security PII drop + replay-injection scan. Migration 0075 required. Tests 4815 -> 5076.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-21 23:13:21 +02:00
1 parent 324d4bb411
commit 6c7fc7dc84
167 files changed
+11233 -371

No files matched your search

@@ -12,6 +12,9 @@ vi.mock("@/lib/db", () => ({
notificationChannel: {
upsert: vi.fn(),
},
auditLog: {
create: vi.fn(),
},
},
}));
@@ -67,6 +70,7 @@ beforeEach(() => {
vi.mocked(prisma.notificationChannel.upsert).mockResolvedValue({
id: "ch-1",
} as never);
vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never);
});
describe("POST /api/devices", () => {
@@ -231,3 +235,74 @@ describe("POST /api/devices", () => {
expect(res.status).toBe(201);
});
});
describe("POST /api/devices — 422 multi-issue envelope (v1.4.43 W6)", () => {
it("surfaces TWO simultaneous validation errors under details.issues", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
// Missing `token` (required) + invalid `apnsEnvironment` enum.
const res = await POST(
req({ bundleId: "io.healthlog.app", apnsEnvironment: "tomorrow" }),
);
expect(res.status).toBe(422);
const body = (await res.json()) as {
data: null;
error: string;
details: {
issues: Array<{ path: string; code: string; message: string }>;
};
};
expect(body.data).toBeNull();
expect(body.error).toBe("Validation failed");
expect(body.details.issues.length).toBeGreaterThanOrEqual(2);
for (const issue of body.details.issues) {
expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]);
}
});
it("surfaces THREE simultaneous validation errors", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
// Missing `token` + invalid `apnsToken` (non-hex) + invalid `bundleId` (number, not string).
const res = await POST(
req({
bundleId: 42,
apnsToken: "not-hex-z",
apnsEnvironment: "sandbox",
}),
);
expect(res.status).toBe(422);
const body = (await res.json()) as {
details: { issues: Array<{ path: string; code: string }> };
};
expect(body.details.issues.length).toBeGreaterThanOrEqual(3);
});
it("writes one audit-ledger row keyed devices.register.validation-failed", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
const res = await POST(
req({ bundleId: "io.healthlog.app", apnsEnvironment: "tomorrow" }),
);
expect(res.status).toBe(422);
await new Promise((r) => setTimeout(r, 5));
expect(prisma.auditLog.create).toHaveBeenCalledTimes(1);
const call = vi.mocked(prisma.auditLog.create).mock.calls[0]?.[0] as {
data: { userId: string; action: string; details: string };
};
expect(call.data.userId).toBe("user-1");
expect(call.data.action).toBe("devices.register.validation-failed");
});
it("does not block the 422 when the audit-row write rejects", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.auditLog.create).mockRejectedValueOnce(
new Error("db down"),
);
const res = await POST(
req({ bundleId: "io.healthlog.app", apnsEnvironment: "tomorrow" }),
);
expect(res.status).toBe(422);
const body = (await res.json()) as {
details: { issues: Array<unknown> };
};
expect(body.details.issues.length).toBeGreaterThanOrEqual(2);
});
});
+27 -2
View File
@@ -24,7 +24,13 @@
import { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiError, apiSuccess, safeJson } from "@/lib/api-response";
import {
apiError,
apiSuccess,
returnAllZodIssues,
safeJson,
sanitiseZodIssues,
} from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
@@ -71,7 +77,26 @@ export const POST = apiHandler(async (request: NextRequest) => {
const parsed = deviceSchema.safeParse(body);
if (!parsed.success) {
return apiError(parsed.error.issues[0].message, 422);
// v1.4.43 W6 — iOS device-registration multi-issue 422 + audit
// breadcrumb. APNs-token + bundleId + environment + model + locale
// can all fail simultaneously on a misconfigured client build.
const issues = sanitiseZodIssues(parsed.error.issues);
annotate({
action: { name: "devices.register.validation-failed" },
meta: { issue_count: issues.length },
});
prisma.auditLog
.create({
data: {
userId: user.id,
action: "devices.register.validation-failed",
details: JSON.stringify({ issues }),
},
})
.catch(() => {
/* swallow — 422 response is the contract */
});
return returnAllZodIssues(parsed.error, 422);
}
const {