From 67575180b86a82202d63a1cd9e3619475aace79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 02:22:12 +0200 Subject: [PATCH] fix(qa): apply pre-release multi-agent QA findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small follow-ups surfaced by the v1.4.6 QA pass: - Settings → KI: the "raw data" warning still used raw Tailwind orange tokens instead of the Dracula `dracula-orange` ones, breaking theme consistency and tipping below the WCAG-AA contrast floor on the card surface. - Admin status grid: the Backups card cta read "View backups" but jumped to the system-status section (the dedicated backups view does not exist yet — that's a v1.5 feature). Re-labelling the cta to "Open system status" and the Integrations cta to "Open integrations" makes the destination honest. The href targets themselves are unchanged. - `withIdempotency` body-content guard previously did `text.includes("sk-")` which silently broke caching for benign payloads containing common English compounds like "task-id", "risk-management", "disk-io". Tightened to a regex requiring the canonical secret prefix plus a realistic suffix length, so only actual API keys trip the guard. Three regression tests cover the false-positive cases. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/admin/status-card-grid.tsx | 4 ++-- src/components/settings/ai-section.tsx | 2 +- src/lib/__tests__/idempotency.test.ts | 18 ++++++++++++++++++ src/lib/idempotency.ts | 13 +++++++------ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/components/admin/status-card-grid.tsx b/src/components/admin/status-card-grid.tsx index edc819b15..47094c43c 100644 --- a/src/components/admin/status-card-grid.tsx +++ b/src/components/admin/status-card-grid.tsx @@ -157,7 +157,7 @@ export function buildCards(data: StatusOverview): StatusCardProps[] { { label: "Push", value: data.integrations.webPush }, ], href: "/admin#section-admin-umami", - cta: "Configure", + cta: "Open integrations", }, { title: "Monitoring", @@ -187,7 +187,7 @@ export function buildCards(data: StatusOverview): StatusCardProps[] { { label: "Retain", value: `${data.backups.retentionDays}d` }, ], href: "/admin#section-system-status", - cta: "View backups", + cta: "Open system status", }, { title: "Maintenance", diff --git a/src/components/settings/ai-section.tsx b/src/components/settings/ai-section.tsx index fafb8727d..e830c235a 100644 --- a/src/components/settings/ai-section.tsx +++ b/src/components/settings/ai-section.tsx @@ -362,7 +362,7 @@ function InsightsSettingsCard({ {settings?.privacyMode === "raw" && ( -
+
{t("settings.rawDataWarning")}
)} diff --git a/src/lib/__tests__/idempotency.test.ts b/src/lib/__tests__/idempotency.test.ts index 866e8762b..4d73e6eb0 100644 --- a/src/lib/__tests__/idempotency.test.ts +++ b/src/lib/__tests__/idempotency.test.ts @@ -300,6 +300,24 @@ describe("withIdempotency body-content exclusion (P12)", () => { expect(prisma.idempotencyKey.create).not.toHaveBeenCalled(); }); + it.each([ + ["task-id substring", '{"error":"task-id must not contain spaces"}'], + ["risk-management word", '{"data":{"note":"risk-management"}}'], + ["disk-io metric", '{"data":{"metric":"disk-io"}}'], + ])( + "still caches a benign payload whose text contains %s (no false positive)", + async (_label, body) => { + const handler = vi.fn( + async () => new NextResponse(body, { status: 422 }), + ); + const wrapped = withIdempotency<[NextRequest]>(handler, async () => "u-1"); + await wrapped( + makeRequest("POST", { "idempotency-key": "key-12345678" }), + ); + expect(prisma.idempotencyKey.create).toHaveBeenCalledTimes(1); + }, + ); + it("still caches a normal payload that does not carry a secret", async () => { const handler = vi.fn( async () => diff --git a/src/lib/idempotency.ts b/src/lib/idempotency.ts index 8c6131675..f9c3a8852 100644 --- a/src/lib/idempotency.ts +++ b/src/lib/idempotency.ts @@ -224,14 +224,15 @@ export function withIdempotency< // with, but if a future caller forgets we refuse to leak. // `hlk_` = our access tokens // `hlr_` = our refresh tokens - // `sk-` = OpenAI keys (also matches `sk-ant-` Anthropic keys) + // `sk-…` = OpenAI / Anthropic keys (full token form, not the + // raw substring — a 422 body explaining "task-id…" + // or any other word containing `sk-` would otherwise + // silently break idempotency for benign retries). + const SECRET_PATTERN = + /(?:\b(?:hlk_|hlr_)[A-Za-z0-9_-]+|\bsk-(?:ant-)?[A-Za-z0-9_-]{8,})/; const cloned = response.clone(); const text = await cloned.text(); - if ( - !text.includes("hlk_") && - !text.includes("hlr_") && - !text.includes("sk-") - ) { + if (!SECRET_PATTERN.test(text)) { await persistCached(ctx, response, text); } }