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<Response | null> {
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;