← back to Trademarks Copyright
src/app/api/drops/cron/route.ts
52 lines
import { NextRequest, NextResponse } from "next/server";
import { timingSafeEqual } from "node:crypto";
import { composeDropForDate } from "@/lib/drops";
import { purgeExpiredRateLimits } from "@/lib/rateLimit";
export const runtime = "nodejs";
export const maxDuration = 300;
/**
* Daily cron tick. Secure via CRON_TOKEN env (provide as ?token=... or Authorization header).
* Step 1: compose today's drop if not already composed.
* Step 2: send it by POSTing to /api/drops/send.
*/
export async function POST(req: NextRequest) {
const requiredToken = process.env.CRON_TOKEN;
if (!requiredToken) {
return NextResponse.json({ error: "cron disabled — CRON_TOKEN not configured" }, { status: 503 });
}
const token = req.nextUrl.searchParams.get("token") || req.headers.get("authorization")?.replace(/^Bearer\s+/, "") || "";
// P1 fix 2026-05-04: timing-safe compare — was plain !== which leaks token
// length via response time; admin middleware uses HMAC compare, this should match.
const tBuf = Buffer.from(token);
const rBuf = Buffer.from(requiredToken);
const ok = tBuf.length === rBuf.length && timingSafeEqual(tBuf, rBuf);
if (!ok) {
return NextResponse.json({ error: "bad token" }, { status: 403 });
}
const today = new Date();
let composed: Awaited<ReturnType<typeof composeDropForDate>>;
try {
composed = await composeDropForDate({ date: today, itemsPerDrop: 10 });
} catch (e) {
return NextResponse.json({ error: (e as Error).message, step: "compose" }, { status: 500 });
}
const sendRes = await fetch(`${req.nextUrl.origin}/api/drops/send`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ dropId: composed.dropId }),
});
const sendJson = await sendRes.json();
// Daily housekeeping: honor the 30-day rate-limit retention we promise in the privacy policy.
const purged = await purgeExpiredRateLimits().catch(() => 0);
return NextResponse.json({ ok: true, composed, send: sendJson, purged });
}
export async function GET(req: NextRequest) { return POST(req); }