import { env } from "cloudflare:workers"; import { isTestKey, stripe, stripeEnabled, webCrypto } from "./config"; import { HANDLED_EVENTS, handleStripeWebhook, STRIPE_WEBHOOK_PATH } from "./webhook"; export const STRIPE_DEV_REPLAY_PATH = "/__dev/stripe/replay"; const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]); const REPLAY_LIMIT = 50; /** * Local development without the Stripe CLI or a tunnel: pulls recent test-mode events from the Stripe API, * signs each with STRIPE_WEBHOOK_SECRET exactly as Stripe would, and posts it through the real webhook * handler (signature check and idempotency included). Loopback only, test keys only, opt-in. */ export async function handleStripeDevRoutes(request: Request, url: URL): Promise { if (url.pathname !== STRIPE_DEV_REPLAY_PATH) return null; const allowed = env.STRIPE_DEV_REPLAY === "true" && stripeEnabled() && isTestKey(env.STRIPE_SECRET_KEY) && Boolean(env.STRIPE_WEBHOOK_SECRET) && LOCAL_HOSTS.has(url.hostname.toLowerCase()); if (!allowed) return new Response("Not found\n", { status: 404 }); if (request.method !== "POST") return new Response("Method not allowed\n", { status: 405 }); const events = await stripe().events.list({ limit: REPLAY_LIMIT, types: [...HANDLED_EVENTS] }); const outcomes: { id: string; type: string; status: number; outcome: unknown }[] = []; for (const event of [...events.data].reverse()) { const payload = JSON.stringify(event); const signature = await stripe().webhooks.generateTestHeaderStringAsync({ payload, secret: env.STRIPE_WEBHOOK_SECRET ?? "", cryptoProvider: webCrypto, }); const response = await handleStripeWebhook( new Request(new URL(STRIPE_WEBHOOK_PATH, url), { method: "POST", headers: { "stripe-signature": signature, "content-type": "application/json" }, body: payload, }), ); const body = (await response.json().catch(() => null)) as { outcome?: unknown } | null; outcomes.push({ id: event.id, type: event.type, status: response.status, outcome: body?.outcome ?? null }); } return Response.json({ replayed: outcomes.length, outcomes }); }