45 lines · 2.1 KB
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;