Files
HealthLog/src/lib/audit-dedup.ts
T
Marc-André Bombeck 6c7fc7dc84 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.
2026-05-21 23:13:21 +02:00

45 lines
1.5 KiB
TypeScript

/**
* In-process dedup helper for fire-and-forget audit-log rows.
*
* A misbehaving iOS client looping on a 422 can otherwise write
* thousands of identical audit rows per minute. Routes that emit a
* `<route>.validation-failed` audit row alongside a multi-issue Zod
* 422 envelope wrap the write through `shouldEmitAuditRow` to gate
* on a 60 s `(userId, action)` window — only the first 422 inside
* the window writes; subsequent 422s still return the full envelope
* but skip the breadcrumb.
*
* In-memory only — a process restart resets the map, which is the
* safe failure mode (worst case: one extra audit row per restart).
* Cluster deployments still get one row per process per minute,
* which is fine for the operator-grep use case the audit row
* exists for.
*/
const AUDIT_DEDUP_WINDOW_MS = 60_000;
const auditDedupMemo = new Map<string, number>();
export function shouldEmitAuditRow(
userId: string,
action: string,
now: number = Date.now(),
): boolean {
const key = `${userId}:${action}`;
const last = auditDedupMemo.get(key);
if (last !== undefined && now - last < AUDIT_DEDUP_WINDOW_MS) {
return false;
}
auditDedupMemo.set(key, now);
if (auditDedupMemo.size > 512) {
for (const [k, t] of auditDedupMemo) {
if (now - t >= AUDIT_DEDUP_WINDOW_MS) auditDedupMemo.delete(k);
}
}
return true;
}
/** @internal — exported for unit tests only. */
export function __resetAuditDedupMemoForTests(): void {
auditDedupMemo.clear();
}