import { revalidateTag } from 'next/cache'; import { NextRequest, NextResponse } from 'next/server'; import { timingSafeEqual } from 'crypto'; // Constant-time string comparison to avoid leaking the secret via response timing. function secretsMatch(provided: unknown, expected: string): boolean { if (typeof provided !== 'string' || provided.length === 0) return false; const a = Buffer.from(provided); const b = Buffer.from(expected); if (a.length !== b.length) return false; return timingSafeEqual(a, b); } export async function POST(request: NextRequest) { try { const body = await request.json(); const { secret, tag } = body; // Validate the revalidation secret. Reject if it is unset or left at an insecure default. const revalidateSecret = process.env.REVALIDATE_SECRET; if (!revalidateSecret || revalidateSecret === 'change-me' || revalidateSecret.length < 16) { return NextResponse.json({ error: 'Revalidation is not configured' }, { status: 503 }); } if (!secretsMatch(secret, revalidateSecret)) { return NextResponse.json({ error: 'Invalid secret' }, { status: 401 }); } // Validate tag(s) - supports single tag or array of tags const allowedTags = ['events-sitemap', 'next-event']; const tags: string[] = Array.isArray(tag) ? tag : [tag]; const invalidTags = tags.filter((t: string) => !allowedTags.includes(t)); if (tags.length === 0 || invalidTags.length > 0) { return NextResponse.json({ error: 'Invalid tag' }, { status: 400 }); } for (const t of tags) { revalidateTag(t); } return NextResponse.json({ revalidated: true, tags, now: Date.now() }); } catch { return NextResponse.json({ error: 'Failed to revalidate' }, { status: 500 }); } }