import { createHash, createHmac, randomUUID } from "crypto"; import { getVercelOidcToken } from "@vercel/oidc"; import { QueueClient } from "@vercel/queue"; const TRIGGER_PATH = "/api/v1/internal/queue/vercel/trigger"; const BODY_LIMIT = 16 * 1024; export async function forwardVercelQueueTrigger(topicName: unknown, message: unknown): Promise { const url = process.env.QUEUE_WORKER_TRIGGER_URL; const secret = process.env.QUEUE_VERCEL_FORWARD_SECRET; if (!url || !secret || Buffer.byteLength(secret) < 32) { throw new Error("QUEUE_WORKER_TRIGGER_URL and QUEUE_VERCEL_FORWARD_SECRET are required"); } if (typeof topicName !== "string" || topicName === "") { throw new Error("Vercel queue topic is missing"); } const rawBody = JSON.stringify({ topicName, message }); if (Buffer.byteLength(rawBody) > BODY_LIMIT) { throw new Error("Vercel queue trigger body is too large"); } const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = randomUUID(); const token = await getVercelOidcToken(); const bodyHash = createHash("sha256").update(rawBody).digest("hex"); const signature = createHmac("sha256", secret) .update(`POST\n${TRIGGER_PATH}\n${timestamp}\n${nonce}\n${bodyHash}`) .digest("hex"); let response: { ok: boolean; status: number }; try { response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "X-CS-Queue-Timestamp": timestamp, "X-CS-Queue-Nonce": nonce, "X-CS-Queue-Signature": signature }, body: rawBody, signal: AbortSignal.timeout(10000) }); } catch (error) { throw error instanceof Error ? error : new Error("Core worker trigger request failed"); } if (!response.ok) { throw new Error(`Core worker trigger failed with status ${response.status}`); } } const client = new QueueClient({ region: process.env.QUEUE_VERCEL_REGION || "sin1" }); // queue/v2beta in vercel.json is the Vercel Queue trigger protocol identifier, not a CS queue version. export default client.handleNodeCallback(async (message, metadata) => { await forwardVercelQueueTrigger(metadata && metadata.topicName, message); }, { visibilityTimeoutSeconds: 300 });