Files
linumiq-invoice/app/services/webhooks/cleanup.server.ts
T
Gerhard Scheikl 01b4734477 security hardening
2026-05-31 09:35:31 +02:00

64 lines
2.1 KiB
TypeScript

import db from "../../db.server";
/**
* Periodic TTL cleanup for the `ProcessedWebhook` idempotency table.
*
* The table grows by one row per Shopify webhook delivery and is never read
* after the retry window closes, so without pruning it grows unbounded —
* eventually a disk/space DoS. We only need rows for as long as Shopify might
* retry a delivery (hours), so a generous retention window of a few days is
* ample while keeping the table small.
*/
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const INTERVAL_MS = 60 * 60 * 1000; // hourly
export interface CleanupDeps {
db: {
processedWebhook: {
deleteMany: (args: {
where: { receivedAt: { lt: Date } };
}) => Promise<{ count: number }>;
};
};
}
let scheduled = false;
async function runCleanup(deps: CleanupDeps): Promise<void> {
try {
const cutoff = new Date(Date.now() - RETENTION_MS);
const { count } = await deps.db.processedWebhook.deleteMany({
where: { receivedAt: { lt: cutoff } },
});
if (count > 0) {
console.log(`webhook-cleanup: removed ${count} ProcessedWebhook row(s) older than 7d`);
}
} catch (err) {
// Best-effort housekeeping — never throw into the caller.
console.warn("webhook-cleanup: prune failed:", err);
}
}
/**
* Idempotently schedule the hourly cleanup. Safe to call on every webhook —
* the first call starts a single unref'd interval and runs an immediate
* sweep; subsequent calls are no-ops.
*
* Because this is only ever invoked while handling a live webhook request, it
* never runs during `prisma generate` / `react-router build` or other CLI
* contexts. The interval is `unref`'d so it can never keep the process alive.
*/
export function ensureWebhookCleanupScheduled(deps: CleanupDeps = { db }): void {
if (scheduled) return;
scheduled = true;
const timer = setInterval(() => {
void runCleanup(deps);
}, INTERVAL_MS);
// Don't let the housekeeping interval keep the event loop alive on shutdown.
if (typeof timer.unref === "function") timer.unref();
// Kick off an immediate sweep so a long-lived process prunes promptly.
void runCleanup(deps);
}