security hardening
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user