From e2366bdde7f0dd7d331df016399780dcedfd4635 Mon Sep 17 00:00:00 2001 From: imeepos Date: Thu, 2 Jul 2026 03:36:17 -0700 Subject: [PATCH] fix: trace stripe webhooks on cloudflare --- .../stripe/handleStripeWebhookEvent.ts | 40 +++ .../external/stripe/stripeWebhookRouter.ts | 3 - .../setupCheckoutSessionCompletedContext.ts | 33 ++ .../handleCheckoutSessionMetadataV2.ts | 50 ++- .../withClaimedCheckoutSessionMetadata.ts | 50 +++ .../stripeIdempotencyMiddleware.ts | 32 ++ .../stripeLegacySeederMiddleware.ts | 88 ++++- .../webhookTrace/recordStripeWebhookTrace.ts | 81 +++++ server/src/honoMiddlewares/baseMiddleware.ts | 21 +- server/src/index.ts | 302 ++++++++++++++++++ .../internal/misc/configs/configsRouter.ts | 2 + .../handlers/handleDebugStripeCheckout.ts | 88 +++++ server/src/internal/orgs/orgUtils.ts | 4 + ...stripe-webhook-no-early-ack-router.test.ts | 16 + .../stripe-webhook-preserves-raw-body.test.ts | 18 ++ .../stripe-webhook-trace-kv.test.ts | 30 ++ .../stripe-webhook-worker-entry-trace.test.ts | 29 ++ .../webhooks/stripe-webhook-secret.test.ts | 36 +++ server/worker-configuration.d.ts | 9 +- server/wrangler.jsonc | 24 +- shared/models/otherModels/metadataTable.ts | 1 + 21 files changed, 934 insertions(+), 23 deletions(-) create mode 100644 server/src/external/stripe/webhookTrace/recordStripeWebhookTrace.ts create mode 100644 server/src/internal/misc/configs/handlers/handleDebugStripeCheckout.ts create mode 100644 server/tests/unit/cloudflare/stripe-webhook-no-early-ack-router.test.ts create mode 100644 server/tests/unit/cloudflare/stripe-webhook-preserves-raw-body.test.ts create mode 100644 server/tests/unit/cloudflare/stripe-webhook-trace-kv.test.ts create mode 100644 server/tests/unit/cloudflare/stripe-webhook-worker-entry-trace.test.ts create mode 100644 server/tests/unit/webhooks/stripe-webhook-secret.test.ts diff --git a/server/src/external/stripe/handleStripeWebhookEvent.ts b/server/src/external/stripe/handleStripeWebhookEvent.ts index f2a7011bd..922ce3604 100644 --- a/server/src/external/stripe/handleStripeWebhookEvent.ts +++ b/server/src/external/stripe/handleStripeWebhookEvent.ts @@ -20,6 +20,7 @@ import type { StripeWebhookContext, StripeWebhookHonoEnv, } from "./webhookMiddlewares/stripeWebhookContext.js"; +import { recordStripeWebhookTrace } from "./webhookTrace/recordStripeWebhookTrace.js"; /** * Hono handler for Stripe webhook events @@ -33,6 +34,19 @@ export const handleStripeWebhookEvent = async ( const event = stripeEvent; try { + await recordStripeWebhookTrace({ + ctx, + stage: "handler_start", + }); + logger.info("stripe_webhook_trace", { + stage: "handler_start", + event_id: event.id, + event_type: event.type, + object_id: (event.data.object as { id?: string }).id, + metadata_id: (event.data.object as { metadata?: Record }) + .metadata?.autumn_metadata_id, + }); + switch (event.type) { case "customer.updated": await handleStripeCustomerUpdated({ ctx, event }); @@ -95,7 +109,33 @@ export const handleStripeWebhookEvent = async ( break; } } + + await recordStripeWebhookTrace({ + ctx, + stage: "handler_success", + }); + logger.info("stripe_webhook_trace", { + stage: "handler_success", + event_id: event.id, + event_type: event.type, + }); } catch (error) { + await recordStripeWebhookTrace({ + ctx, + stage: "handler_error", + data: { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }, + }); + logger.error("stripe_webhook_trace", { + stage: "handler_error", + event_id: event.id, + event_type: event.type, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + captureException(error, { tags: getSentryTags({ ctx, diff --git a/server/src/external/stripe/stripeWebhookRouter.ts b/server/src/external/stripe/stripeWebhookRouter.ts index 9a42c9cdc..963addaaa 100644 --- a/server/src/external/stripe/stripeWebhookRouter.ts +++ b/server/src/external/stripe/stripeWebhookRouter.ts @@ -9,7 +9,6 @@ import { stripeLegacySeederMiddleware } from "./webhookMiddlewares/stripeLegacyS import { stripeSyncMiddleware } from "./webhookMiddlewares/stripeSyncMiddleware.js"; import { stripeToAutumnCustomerMiddleware } from "./webhookMiddlewares/stripeToAutumnCustomerMiddleware.js"; import type { StripeWebhookHonoEnv } from "./webhookMiddlewares/stripeWebhookContext.js"; -import { stripeWebhookEarlyAckMiddleware } from "./webhookMiddlewares/stripeWebhookEarlyAckMiddleware.js"; import { stripeWebhookRefreshMiddleware } from "./webhookMiddlewares/stripeWebhookRefreshMiddleware.js"; export const createStripeWebhookRouter = () => { @@ -28,7 +27,6 @@ export const createStripeWebhookRouter = () => { stripeLegacySeederMiddleware, stripeToAutumnCustomerMiddleware, stripeIdempotencyMiddleware, - stripeWebhookEarlyAckMiddleware, stripeWebhookRefreshMiddleware, stripeSyncMiddleware, stripeLoggerMiddleware, @@ -42,7 +40,6 @@ export const createStripeWebhookRouter = () => { stripeConnectSeederMiddleware, stripeToAutumnCustomerMiddleware, stripeIdempotencyMiddleware, - stripeWebhookEarlyAckMiddleware, stripeWebhookRefreshMiddleware, stripeSyncMiddleware, stripeLoggerMiddleware, diff --git a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts index 32dedecee..98c345d02 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts @@ -11,6 +11,7 @@ import { getExpandedStripeSubscription, } from "@/external/stripe/subscriptions/operations/getExpandedStripeSubscription"; import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js"; +import { recordStripeWebhookTrace } from "../../webhookTrace/recordStripeWebhookTrace.js"; import type { StripeWebhookContext } from "../../webhookMiddlewares/stripeWebhookContext.js"; type CheckoutSessionExpansions = ["line_items"]; @@ -63,6 +64,38 @@ export const setupCheckoutSessionCompletedContext = async ({ : undefined, ]); + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_context_ready", + data: { + checkout_session_id: stripeCheckoutSession.id, + stripe_customer_id: + typeof stripeCheckoutSession.customer === "string" + ? stripeCheckoutSession.customer + : stripeCheckoutSession.customer?.id, + stripe_subscription_id: subscriptionId, + stripe_invoice_id: invoiceId, + metadata_id: stripeCheckoutSession.metadata?.autumn_metadata_id, + metadata_found: !!metadata, + metadata_type: metadata?.type, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "checkout_context_ready", + event_id: event.id, + event_type: event.type, + checkout_session_id: stripeCheckoutSession.id, + stripe_customer_id: + typeof stripeCheckoutSession.customer === "string" + ? stripeCheckoutSession.customer + : stripeCheckoutSession.customer?.id, + stripe_subscription_id: subscriptionId, + stripe_invoice_id: invoiceId, + metadata_id: stripeCheckoutSession.metadata?.autumn_metadata_id, + metadata_found: !!metadata, + metadata_type: metadata?.type, + }); + return { stripeCheckoutSession, stripeSubscription, diff --git a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts index 1124d1669..79f807f48 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/handleCheckoutSessionMetadataV2.ts @@ -3,6 +3,7 @@ import { MetadataType, } from "@autumn/shared"; import type { CheckoutSessionCompletedContext } from "@/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext"; +import { recordStripeWebhookTrace } from "@/external/stripe/webhookTrace/recordStripeWebhookTrace"; import { createStripeScheduleFromCheckout } from "@/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionEnabledImmediately/createStripeScheduleFromCheckout"; import { modifyStripeSubscriptionFromCheckout } from "@/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/modifyStripeSubscriptionFromCheckout"; import { syncSubscriptionItemMetadataFromCheckout } from "@/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/syncSubscriptionItemMetadataFromCheckout"; @@ -28,11 +29,45 @@ export const handleCheckoutSessionMetadataV2 = async ({ }): Promise => { const { metadata } = checkoutContext; - if (metadata?.type !== MetadataType.CheckoutSessionV2) return; + if (metadata?.type !== MetadataType.CheckoutSessionV2) { + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_v2_skip", + data: { + metadata_id: metadata?.id, + metadata_type: metadata?.type, + expected_type: MetadataType.CheckoutSessionV2, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "checkout_v2_skip", + metadata_id: metadata?.id, + metadata_type: metadata?.type, + expected_type: MetadataType.CheckoutSessionV2, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }); + return; + } ctx.logger.info( `[checkout.completed] Handling checkout session metadata V2: ${metadata.id}`, ); + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_v2_start", + data: { + metadata_id: metadata.id, + metadata_type: metadata.type, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "checkout_v2_start", + metadata_id: metadata.id, + metadata_type: metadata.type, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }); await withClaimedCheckoutSessionMetadata({ ctx, @@ -130,6 +165,19 @@ const executeCheckoutSessionMetadataV2 = async ({ // Delete metadata after successful execution await MetadataService.delete({ db: ctx.db, id: metadata.id }); + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_v2_metadata_deleted", + data: { + metadata_id: metadata.id, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "checkout_v2_metadata_deleted", + metadata_id: metadata.id, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }); const newCustomerProducts = updatedDeferredData.billingPlan.autumn.insertCustomerProducts; diff --git a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/withClaimedCheckoutSessionMetadata.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/withClaimedCheckoutSessionMetadata.ts index 03ec44dcc..71db4b642 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/withClaimedCheckoutSessionMetadata.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/tasks/handleCheckoutSessionMetadataV2/withClaimedCheckoutSessionMetadata.ts @@ -5,6 +5,7 @@ import { import { setStripeSubscriptionLock } from "@/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils"; import type { CheckoutSessionCompletedContext } from "@/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext"; import type { StripeWebhookContext } from "@/external/stripe/webhookMiddlewares/stripeWebhookContext"; +import { recordStripeWebhookTrace } from "@/external/stripe/webhookTrace/recordStripeWebhookTrace"; import { checkoutSessionLock } from "@/internal/billing/v2/actions/locks/checkoutSessionLock/checkoutSessionLock"; import { MetadataService } from "@/internal/metadata/MetadataService"; @@ -36,8 +37,40 @@ export const withClaimedCheckoutSessionMetadata = async ({ ctx.logger.info( `[checkout.completed] Metadata ${metadata.id} already claimed by another executor, skipping`, ); + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_v2_claim_failed", + data: { + metadata_id: metadata.id, + metadata_type: metadata.type, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "checkout_v2_claim_failed", + metadata_id: metadata.id, + metadata_type: metadata.type, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + }); return; } + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_v2_claimed", + data: { + metadata_id: metadata.id, + metadata_type: metadata.type, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + stripe_subscription_id: checkoutContext.stripeSubscription?.id, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "checkout_v2_claimed", + metadata_id: metadata.id, + metadata_type: metadata.type, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + stripe_subscription_id: checkoutContext.stripeSubscription?.id, + }); const deferredData = metadata.data as DeferredAutumnBillingPlanData; const lockCustomerId = @@ -55,6 +88,23 @@ export const withClaimedCheckoutSessionMetadata = async ({ await execute(); } catch (error) { + await recordStripeWebhookTrace({ + ctx, + stage: "checkout_v2_execute_error", + data: { + metadata_id: metadata.id, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }, + }); + ctx.logger.error("stripe_webhook_trace", { + stage: "checkout_v2_execute_error", + metadata_id: metadata.id, + checkout_session_id: checkoutContext.stripeCheckoutSession.id, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); await revertMetadataClaim({ ctx, metadataId: metadata.id }); throw error; } finally { diff --git a/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts b/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts index f498422ed..608ec8e0c 100644 --- a/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts +++ b/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts @@ -1,5 +1,6 @@ import type { Context, Next } from "hono"; import { resolveRequestIdempotencyStore } from "@/internal/misc/idempotency/resolveRequestIdempotencyStore.js"; +import { recordStripeWebhookTrace } from "../webhookTrace/recordStripeWebhookTrace.js"; import type { StripeWebhookHonoEnv } from "./stripeWebhookContext"; const IDEMPOTENCY_TTL_MS = 5 * 60 * 1000; // 5 minutes @@ -34,6 +35,24 @@ export const stripeIdempotencyMiddleware = async ( key: idempotencyKey, ttlMs: IDEMPOTENCY_TTL_MS, }); + await recordStripeWebhookTrace({ + ctx, + stage: "idempotency_acquire", + data: { + result, + idempotency_key: idempotencyKey, + }, + }); + ctx.logger.info("stripe_webhook_trace", { + stage: "idempotency_acquire", + event_id: stripeEvent.id, + event_type: stripeEvent.type, + result, + idempotency_key: idempotencyKey, + metadata_id: ( + stripeEvent.data.object as { metadata?: Record } + ).metadata?.autumn_metadata_id, + }); } catch (error) { ctx.logger.warn( `[stripeIdempotencyMiddleware] Store error, allowing through: ${error}`, @@ -51,8 +70,21 @@ export const stripeIdempotencyMiddleware = async ( } if (result === "duplicate") { + await recordStripeWebhookTrace({ + ctx, + stage: "idempotency_duplicate", + data: { + idempotency_key: idempotencyKey, + }, + }); ctx.logger.info( `[stripeIdempotencyMiddleware] Duplicate webhook event detected, skipping: ${stripeEvent.id}`, + { + stage: "idempotency_duplicate", + event_id: stripeEvent.id, + event_type: stripeEvent.type, + idempotency_key: idempotencyKey, + }, ); return c.json({ received: true, duplicate: true }, 200); } diff --git a/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts b/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts index 39b68c9d3..fd2e1ecf9 100644 --- a/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts +++ b/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts @@ -11,6 +11,7 @@ import type { StripeWebhookContext, StripeWebhookHonoEnv, } from "./stripeWebhookContext.js"; +import { recordStripeWebhookTrace } from "../webhookTrace/recordStripeWebhookTrace.js"; /** * Seeder middleware for legacy Stripe webhooks (orgs that pasted their secret keys) @@ -43,6 +44,11 @@ export const stripeLegacySeederMiddleware = async ( } const { org, features } = data; + ctx.org = org; + ctx.features = features; + ctx.env = appEnv; + ctx.workerEnv = workerEnv; + ctx.authType = AuthType.Stripe; // Step 2: Check if org is connected to Stripe if (!isStripeConnected({ org, env: appEnv })) { @@ -56,6 +62,30 @@ export const stripeLegacySeederMiddleware = async ( // Step 3: Verify webhook signature const rawBody = await c.req.text(); const signature = c.req.header("stripe-signature") || ""; + const unverifiedEvent = (() => { + try { + return JSON.parse(rawBody) as Stripe.Event; + } catch { + return undefined; + } + })(); + await recordStripeWebhookTrace({ + ctx, + stage: "legacy_received", + data: { + event_id: unverifiedEvent?.id, + event_type: unverifiedEvent?.type, + object_id: unverifiedEvent + ? (unverifiedEvent.data.object as { id?: string }).id + : undefined, + metadata_id: unverifiedEvent + ? (unverifiedEvent.data.object as { metadata?: Record }) + .metadata?.autumn_metadata_id + : undefined, + raw_body_length: rawBody.length, + has_signature: !!signature, + }, + }); const skipVerify = workerEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" && @@ -70,6 +100,17 @@ export const stripeLegacySeederMiddleware = async ( event = JSON.parse(rawBody) as Stripe.Event; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); + await recordStripeWebhookTrace({ + ctx, + stage: "legacy_parse_failed", + data: { + event_id: unverifiedEvent?.id, + event_type: unverifiedEvent?.type, + error: message, + raw_body_length: rawBody.length, + has_signature: !!signature, + }, + }); logger.warn( `Stripe legacy webhook body parse error (skip-verify): ${message}`, ); @@ -85,6 +126,27 @@ export const stripeLegacySeederMiddleware = async ( ); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); + await recordStripeWebhookTrace({ + ctx, + stage: "legacy_signature_failed", + data: { + event_id: unverifiedEvent?.id, + event_type: unverifiedEvent?.type, + object_id: unverifiedEvent + ? (unverifiedEvent.data.object as { id?: string }).id + : undefined, + metadata_id: unverifiedEvent + ? ( + unverifiedEvent.data.object as { + metadata?: Record; + } + ).metadata?.autumn_metadata_id + : undefined, + error: message, + raw_body_length: rawBody.length, + has_signature: !!signature, + }, + }); logger.warn( `Stripe legacy webhook signature verification failed: ${message}`, ); @@ -93,13 +155,29 @@ export const stripeLegacySeederMiddleware = async ( } // Step 4: Set up context - ctx.org = org; - ctx.features = features; - ctx.env = appEnv; - ctx.workerEnv = workerEnv; - ctx.authType = AuthType.Stripe; ctx.stripeEvent = event; ctx.stripeCli = createStripeCli({ org, env: appEnv, workerEnv }); + await recordStripeWebhookTrace({ + ctx, + stage: "legacy_seeded", + data: { + object_id: (event.data.object as { id?: string }).id, + metadata_id: (event.data.object as { metadata?: Record }) + .metadata?.autumn_metadata_id, + }, + }); + logger.info("stripe_webhook_trace", { + stage: "legacy_seeded", + event_id: event.id, + event_type: event.type, + org_id: org.id, + org_slug: org.slug, + env: appEnv, + object_id: (event.data.object as { id?: string }).id, + metadata_id: (event.data.object as { metadata?: Record }) + .metadata?.autumn_metadata_id, + }); + await next(); }; diff --git a/server/src/external/stripe/webhookTrace/recordStripeWebhookTrace.ts b/server/src/external/stripe/webhookTrace/recordStripeWebhookTrace.ts new file mode 100644 index 000000000..25f579287 --- /dev/null +++ b/server/src/external/stripe/webhookTrace/recordStripeWebhookTrace.ts @@ -0,0 +1,81 @@ +import type { StripeWebhookContext } from "@/external/stripe/webhookMiddlewares/stripeWebhookContext"; + +const TRACE_TTL_SECONDS = 60 * 60; +const MAX_TRACE_ENTRIES = 100; + +export type StripeWebhookTraceEntry = { + stage: string; + event_id?: string; + event_type?: string; + object_id?: string; + metadata_id?: string; + org_id?: string; + org_slug?: string; + env?: string; + created_at: number; + data: Record; +}; + +export const getStripeWebhookTraceKey = (eventId: string) => + `stripe_webhook_trace:${eventId}`; + +export const recordStripeWebhookTrace = async ({ + ctx, + stage, + data = {}, +}: { + ctx: StripeWebhookContext; + stage: string; + data?: Record; +}) => { + const event = ctx.stripeEvent; + + try { + const traceKv = ctx.workerEnv?.STRIPE_WEBHOOK_TRACE_KV; + if (!traceKv) { + ctx.logger.warn("stripe_webhook_trace_kv_missing", { stage }); + return; + } + + const dataEventId = data.event_id as string | undefined; + const dataEventType = data.event_type as string | undefined; + const dataObjectId = data.object_id as string | undefined; + const dataMetadataId = data.metadata_id as string | undefined; + + const eventId = event?.id ?? dataEventId; + if (!eventId) { + ctx.logger.warn("stripe_webhook_trace_event_id_missing", { stage }); + return; + } + + const entry: StripeWebhookTraceEntry = { + stage, + event_id: eventId, + event_type: event?.type ?? dataEventType, + object_id: event ? (event.data.object as { id?: string }).id : dataObjectId, + metadata_id: event + ? (event.data.object as { metadata?: Record }).metadata + ?.autumn_metadata_id + : dataMetadataId, + org_id: ctx.org?.id, + org_slug: ctx.org?.slug, + env: ctx.env, + created_at: Date.now(), + data, + }; + + const key = getStripeWebhookTraceKey(eventId); + const existing = + (await traceKv.get(key, "json")) ?? []; + const entries = [...existing, entry].slice(-MAX_TRACE_ENTRIES); + + await traceKv.put(key, JSON.stringify(entries), { + expirationTtl: TRACE_TTL_SECONDS, + }); + } catch (error) { + ctx.logger.warn("stripe_webhook_trace_persist_failed", { + stage, + error: error instanceof Error ? error.message : String(error), + }); + } +}; diff --git a/server/src/honoMiddlewares/baseMiddleware.ts b/server/src/honoMiddlewares/baseMiddleware.ts index ce266e68e..d76ad63d8 100644 --- a/server/src/honoMiddlewares/baseMiddleware.ts +++ b/server/src/honoMiddlewares/baseMiddleware.ts @@ -36,6 +36,17 @@ const redactSensitiveRequestBody = ({ body }: { body: unknown }): unknown => { ); }; +const shouldSkipRequestBodyParse = ({ + method, + path, +}: { + method: string; + path: string; +}) => { + if (method === "GET" || method === "HEAD") return true; + return path.startsWith("/webhooks/stripe/") || path.startsWith("/webhooks/connect/"); +}; + /** * Base middleware that sets up the request context * Sets up: db, logger, id, timestamp @@ -52,10 +63,12 @@ export const baseMiddleware = async (c: Context, next: Next) => { const timestamp = Date.now(); - const { data: body } = - c.req.method !== "GET" && c.req.method !== "HEAD" - ? await tryCatch(c.req.json()) - : { data: undefined }; + const { data: body } = shouldSkipRequestBodyParse({ + method: c.req.method, + path: c.req.path, + }) + ? { data: undefined } + : await tryCatch(c.req.json()); const customerId = resolveCustomerId({ method: c.req.method, diff --git a/server/src/index.ts b/server/src/index.ts index 00b96fa30..270ddd617 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,6 +3,10 @@ import { createRequestScopedDb } from "./db/initDrizzle.js"; import { initTinybirdClient } from "./external/tinybird/initTinybird.js"; import { initTinybirdConfig } from "./external/tinybird/tinybirdUtils.js"; import { createCacheStore } from "./external/storage/cache/createCacheStore.js"; +import { + getStripeWebhookTraceKey, + type StripeWebhookTraceEntry, +} from "./external/stripe/webhookTrace/recordStripeWebhookTrace.js"; import { getRequestCacheStoreResolution } from "./external/storage/cache/resolveRequestCacheStore.js"; import { setAllEdgeConfigEnvs } from "./internal/misc/edgeConfig/edgeConfigRegistry.js"; import { createHonoApp } from "./initHono.js"; @@ -22,6 +26,13 @@ const initWorkerRuntimeModules = (env: Env) => { const SNAPSHOT_CACHE_DIAGNOSTIC_SUFFIX = "/snapshot-cache"; const STARTUP_DIAGNOSTIC_SUFFIX = "/startup"; +const STRIPE_WEBHOOK_TRACE_KV_DIAGNOSTIC_SUFFIX = "/stripe-webhook-trace-kv"; +const STRIPE_WEBHOOK_INGRESS_DIAGNOSTIC_SUFFIX = "/stripe-webhook-ingress"; +const STRIPE_WEBHOOK_TRACE_TTL_SECONDS = 60 * 60; +const STRIPE_WEBHOOK_TRACE_MAX_ENTRIES = 100; +const STRIPE_WEBHOOK_LAST_INGRESS_TRACE_KEY = + "stripe_webhook_trace:__last_ingress"; +const STRIPE_WEBHOOK_LAST_POST_TRACE_KEY = "stripe_webhook_trace:__last_post"; const constantTimeEqual = (left: string, right: string): boolean => { const maxLength = Math.max(left.length, right.length); @@ -48,6 +59,24 @@ const getStartupDiagnosticToken = (request: Request): string | null => { }); }; +const getStripeWebhookTraceKvDiagnosticToken = ( + request: Request, +): string | null => { + return getReadyDiagnosticToken({ + request, + suffix: STRIPE_WEBHOOK_TRACE_KV_DIAGNOSTIC_SUFFIX, + }); +}; + +const getStripeWebhookIngressDiagnosticToken = ( + request: Request, +): string | null => { + return getReadyDiagnosticToken({ + request, + suffix: STRIPE_WEBHOOK_INGRESS_DIAGNOSTIC_SUFFIX, + }); +}; + const getReadyDiagnosticToken = ({ request, suffix, @@ -79,6 +108,151 @@ const isAuthorizedReadyDiagnostic = ({ return !!expectedToken && constantTimeEqual(expectedToken, providedToken); }; +const isStripeWebhookIngressRequest = (request: Request): boolean => { + if (request.method !== "POST") return false; + const { pathname } = new URL(request.url); + return ( + pathname.startsWith("/webhooks/stripe/") || + pathname.startsWith("/webhooks/connect/") + ); +}; + +const getWebhookTraceObject = (event: unknown) => { + if (!event || typeof event !== "object") return undefined; + const data = (event as { data?: unknown }).data; + if (!data || typeof data !== "object") return undefined; + const object = (data as { object?: unknown }).object; + return object && typeof object === "object" + ? (object as { + id?: string; + metadata?: Record; + }) + : undefined; +}; + +const appendStripeWebhookIngressTrace = async ({ + kv, + key, + entry, +}: { + kv: KVNamespace; + key: string; + entry: StripeWebhookTraceEntry; +}) => { + const existing = (await kv.get(key, "json")) ?? []; + const entries = [...existing, entry].slice(-STRIPE_WEBHOOK_TRACE_MAX_ENTRIES); + await kv.put(key, JSON.stringify(entries), { + expirationTtl: STRIPE_WEBHOOK_TRACE_TTL_SECONDS, + }); +}; + +const recordStripeWebhookIngressTrace = async ( + request: Request, + env: Env, +) => { + if (!isStripeWebhookIngressRequest(request)) return; + + const traceKv = env.STRIPE_WEBHOOK_TRACE_KV; + const url = new URL(request.url); + const receivedAt = Date.now(); + const baseData = { + method: request.method, + pathname: url.pathname, + has_signature: Boolean(request.headers.get("stripe-signature")), + content_length: request.headers.get("content-length"), + cf_ray: request.headers.get("cf-ray"), + user_agent: request.headers.get("user-agent"), + }; + + try { + if (traceKv) { + await appendStripeWebhookIngressTrace({ + kv: traceKv, + key: STRIPE_WEBHOOK_LAST_INGRESS_TRACE_KEY, + entry: { + stage: "worker_ingress_matched", + created_at: receivedAt, + data: baseData, + }, + }); + } + + const rawBody = await request.clone().text(); + const event = JSON.parse(rawBody) as { + id?: string; + type?: string; + data?: unknown; + }; + const object = getWebhookTraceObject(event); + const eventId = event.id; + const entry: StripeWebhookTraceEntry = { + stage: "worker_received", + event_id: eventId, + event_type: event.type, + object_id: object?.id, + metadata_id: object?.metadata?.autumn_metadata_id, + created_at: receivedAt, + data: { + ...baseData, + raw_body_length: rawBody.length, + }, + }; + + console.log("stripe_webhook_ingress_trace", entry); + + if (traceKv && eventId) { + await appendStripeWebhookIngressTrace({ + kv: traceKv, + key: getStripeWebhookTraceKey(eventId), + entry, + }); + } + } catch (error) { + const entry: StripeWebhookTraceEntry = { + stage: "worker_received_parse_failed", + created_at: receivedAt, + data: { + ...baseData, + error: error instanceof Error ? error.message : String(error), + }, + }; + + console.log("stripe_webhook_ingress_trace", entry); + + if (traceKv) { + await appendStripeWebhookIngressTrace({ + kv: traceKv, + key: getStripeWebhookTraceKey(`parse_failed:${crypto.randomUUID()}`), + entry, + }); + } + } +}; + +const recordLastPostIngressDiagnostic = async (request: Request, env: Env) => { + if (request.method !== "POST") return; + const traceKv = env.STRIPE_WEBHOOK_TRACE_KV; + if (!traceKv) return; + + const url = new URL(request.url); + await appendStripeWebhookIngressTrace({ + kv: traceKv, + key: STRIPE_WEBHOOK_LAST_POST_TRACE_KEY, + entry: { + stage: "worker_post_received", + created_at: Date.now(), + data: { + method: request.method, + pathname: url.pathname, + has_signature: Boolean(request.headers.get("stripe-signature")), + content_length: request.headers.get("content-length"), + cf_ray: request.headers.get("cf-ray"), + user_agent: request.headers.get("user-agent"), + }, + }, + }); +}; + const handleSnapshotCacheDiagnostic = async ( request: Request, env: Env, @@ -130,6 +304,123 @@ const handleSnapshotCacheDiagnostic = async ( ); }; +const handleStripeWebhookTraceKvDiagnostic = async ( + request: Request, + env: Env, +): Promise => { + const providedToken = getStripeWebhookTraceKvDiagnosticToken(request); + if (!providedToken) return null; + + if (!isAuthorizedReadyDiagnostic({ providedToken, env })) { + return new Response("Not Found", { status: 404 }); + } + + const traceKv = env.STRIPE_WEBHOOK_TRACE_KV; + if (!traceKv) { + return Response.json( + { + ok: false, + checks: { + stripeWebhookTraceKv: { + configured: false, + }, + }, + }, + { status: 503 }, + ); + } + + const key = getStripeWebhookTraceKey(`diagnostic:${crypto.randomUUID()}`); + const value: StripeWebhookTraceEntry[] = [ + { + stage: "diagnostic_round_trip", + created_at: Date.now(), + data: { + pathname: new URL(request.url).pathname, + }, + }, + ]; + + await traceKv.put(key, JSON.stringify(value), { + expirationTtl: 60, + }); + const readValue = await traceKv.get(key, "json"); + await traceKv.delete(key); + + return Response.json( + { + ok: readValue?.[0]?.stage === "diagnostic_round_trip", + checks: { + stripeWebhookTraceKv: { + configured: true, + roundTrip: readValue?.[0]?.stage === "diagnostic_round_trip", + }, + }, + }, + { status: readValue?.[0]?.stage === "diagnostic_round_trip" ? 200 : 503 }, + ); +}; + +const handleStripeWebhookIngressDiagnostic = async ( + request: Request, + env: Env, +): Promise => { + const providedToken = getStripeWebhookIngressDiagnosticToken(request); + if (!providedToken) return null; + + if (!isAuthorizedReadyDiagnostic({ providedToken, env })) { + return new Response("Not Found", { status: 404 }); + } + + const eventId = `evt_diagnostic_${crypto.randomUUID()}`; + const diagnosticRequest = new Request( + new URL("/webhooks/stripe/diagnostic/sandbox", request.url), + { + method: "POST", + headers: { + "content-type": "application/json", + "stripe-signature": "t=1,v1=diagnostic", + }, + body: JSON.stringify({ + id: eventId, + object: "event", + type: "checkout.session.completed", + data: { + object: { + id: "cs_diagnostic", + object: "checkout.session", + metadata: { + autumn_metadata_id: "meta_diagnostic", + }, + }, + }, + }), + }, + ); + + await recordStripeWebhookIngressTrace(diagnosticRequest, env); + const traceKv = env.STRIPE_WEBHOOK_TRACE_KV; + const traces = traceKv + ? await traceKv.get( + getStripeWebhookTraceKey(eventId), + "json", + ) + : null; + + return Response.json( + { + ok: Boolean(traces?.some((trace) => trace.stage === "worker_received")), + event_id: eventId, + traces: traces ?? [], + }, + { + status: traces?.some((trace) => trace.stage === "worker_received") + ? 200 + : 503, + }, + ); +}; + const serializeDiagnosticError = (error: unknown) => { if (!(error instanceof Error)) { return { @@ -213,6 +504,9 @@ const handleStartupDiagnostic = async ( export default { async fetch(request: Request, env: Env, executionCtx: ExecutionContext) { + await recordLastPostIngressDiagnostic(request, env); + await recordStripeWebhookIngressTrace(request, env); + const startupDiagnostic = await handleStartupDiagnostic( request, env, @@ -226,6 +520,14 @@ export default { ); if (snapshotCacheDiagnostic) return snapshotCacheDiagnostic; + const stripeWebhookTraceKvDiagnostic = + await handleStripeWebhookTraceKvDiagnostic(request, env); + if (stripeWebhookTraceKvDiagnostic) return stripeWebhookTraceKvDiagnostic; + + const stripeWebhookIngressDiagnostic = + await handleStripeWebhookIngressDiagnostic(request, env); + if (stripeWebhookIngressDiagnostic) return stripeWebhookIngressDiagnostic; + initWorkerRuntimeModules(env); return createHonoApp(env).fetch(request, env, executionCtx); }, diff --git a/server/src/internal/misc/configs/configsRouter.ts b/server/src/internal/misc/configs/configsRouter.ts index 4f9f49053..38c0c0481 100644 --- a/server/src/internal/misc/configs/configsRouter.ts +++ b/server/src/internal/misc/configs/configsRouter.ts @@ -1,5 +1,6 @@ import { Hono } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { handleDebugStripeCheckout } from "./handlers/handleDebugStripeCheckout"; import { handleNukeOrganisationConfiguration } from "./handlers/handleNukeOrganisationConfiguration"; import { handlePushOrganisationConfiguration } from "./handlers/handlePushOrganisationConfiguration"; @@ -7,3 +8,4 @@ export const configsRouter = new Hono(); configsRouter.post("/push", ...handlePushOrganisationConfiguration); configsRouter.delete("/nuke", ...handleNukeOrganisationConfiguration); +configsRouter.post("/debug/stripe-checkout", ...handleDebugStripeCheckout); diff --git a/server/src/internal/misc/configs/handlers/handleDebugStripeCheckout.ts b/server/src/internal/misc/configs/handlers/handleDebugStripeCheckout.ts new file mode 100644 index 000000000..02c894b64 --- /dev/null +++ b/server/src/internal/misc/configs/handlers/handleDebugStripeCheckout.ts @@ -0,0 +1,88 @@ +import { customerProducts, metadata, Scopes } from "@autumn/shared"; +import { eq, sql } from "drizzle-orm"; +import { z } from "zod/v4"; +import { + getStripeWebhookTraceKey, + type StripeWebhookTraceEntry, +} from "@/external/stripe/webhookTrace/recordStripeWebhookTrace"; +import { createRoute } from "@/honoMiddlewares/routeHandler"; +import { MetadataService } from "@/internal/metadata/MetadataService"; + +const DebugStripeCheckoutSchema = z.object({ + metadata_id: z.string().min(1), + stripe_checkout_session_id: z.string().min(1), + event_id: z.string().min(1).optional(), +}); + +export const handleDebugStripeCheckout = createRoute({ + scopes: [Scopes.Plans.Read], + body: DebugStripeCheckoutSchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + + const meta = await MetadataService.get({ + db: ctx.db, + id: body.metadata_id, + }); + + const linkedCustomerProducts = await ctx.db + .select({ + id: customerProducts.id, + product_id: customerProducts.product_id, + status: customerProducts.status, + subscription_ids: customerProducts.subscription_ids, + stripe_checkout_session_id: customerProducts.stripe_checkout_session_id, + }) + .from(customerProducts) + .where( + eq( + customerProducts.stripe_checkout_session_id, + body.stripe_checkout_session_id, + ), + ); + + const metadataRowsForCheckout = await ctx.db + .select({ + id: metadata.id, + type: metadata.type, + stripe_checkout_session_id: metadata.stripe_checkout_session_id, + created_at: metadata.created_at, + expires_at: metadata.expires_at, + }) + .from(metadata) + .where( + sql`${metadata.data}->'billingPlan'->'stripe'->'stripeCheckoutSession'->>'id' = ${body.stripe_checkout_session_id}`, + ); + + const traceKv = ctx.workerEnv?.STRIPE_WEBHOOK_TRACE_KV; + const traces = + body.event_id && traceKv + ? ((await traceKv.get( + getStripeWebhookTraceKey(body.event_id), + "json", + )) ?? []) + : []; + + return c.json({ + success: true, + trace_storage: { + backend: "kv", + configured: Boolean(traceKv), + }, + metadata: meta + ? { + id: meta.id, + type: meta.type, + stripe_checkout_session_id: meta.stripe_checkout_session_id, + stripe_invoice_id: meta.stripe_invoice_id, + created_at: meta.created_at, + expires_at: meta.expires_at, + } + : null, + linked_customer_products: linkedCustomerProducts, + metadata_rows_for_checkout: metadataRowsForCheckout, + traces, + }); + }, +}); diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts index 161c0169f..a470b9bf2 100644 --- a/server/src/internal/orgs/orgUtils.ts +++ b/server/src/internal/orgs/orgUtils.ts @@ -184,6 +184,10 @@ export const getStripeWebhookSecret = ( }); } + if (webhookSecret.startsWith("whsec_")) { + return webhookSecret; + } + return decryptData(webhookSecret, workerEnv); }; diff --git a/server/tests/unit/cloudflare/stripe-webhook-no-early-ack-router.test.ts b/server/tests/unit/cloudflare/stripe-webhook-no-early-ack-router.test.ts new file mode 100644 index 000000000..f921f46e0 --- /dev/null +++ b/server/tests/unit/cloudflare/stripe-webhook-no-early-ack-router.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const routerPath = join( + import.meta.dir, + "../../../src/external/stripe/stripeWebhookRouter.ts", +); + +describe("Stripe webhook router", () => { + test("does not early-ack before request-scoped DB work completes", () => { + const source = readFileSync(routerPath, "utf8"); + + expect(source).not.toContain("stripeWebhookEarlyAckMiddleware"); + }); +}); diff --git a/server/tests/unit/cloudflare/stripe-webhook-preserves-raw-body.test.ts b/server/tests/unit/cloudflare/stripe-webhook-preserves-raw-body.test.ts new file mode 100644 index 000000000..cc1683896 --- /dev/null +++ b/server/tests/unit/cloudflare/stripe-webhook-preserves-raw-body.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const baseMiddlewarePath = join( + import.meta.dir, + "../../../src/honoMiddlewares/baseMiddleware.ts", +); + +describe("Stripe webhook raw body handling", () => { + test("base middleware does not consume Stripe webhook bodies before signature verification", () => { + const source = readFileSync(baseMiddlewarePath, "utf8"); + + expect(source).toContain("shouldSkipRequestBodyParse"); + expect(source).toContain("/webhooks/stripe/"); + expect(source).toContain("/webhooks/connect/"); + }); +}); diff --git a/server/tests/unit/cloudflare/stripe-webhook-trace-kv.test.ts b/server/tests/unit/cloudflare/stripe-webhook-trace-kv.test.ts new file mode 100644 index 000000000..b1647ae79 --- /dev/null +++ b/server/tests/unit/cloudflare/stripe-webhook-trace-kv.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const tracePath = join( + import.meta.dir, + "../../../src/external/stripe/webhookTrace/recordStripeWebhookTrace.ts", +); +const debugPath = join( + import.meta.dir, + "../../../src/internal/misc/configs/handlers/handleDebugStripeCheckout.ts", +); + +describe("Stripe webhook KV trace persistence", () => { + test("records webhook traces through KV instead of request-scoped Postgres", () => { + const source = readFileSync(tracePath, "utf8"); + + expect(source).toContain("STRIPE_WEBHOOK_TRACE_KV"); + expect(source).toContain(".put("); + expect(source).not.toContain("MetadataService.insert"); + expect(source).not.toContain("db: ctx.db"); + }); + + test("debug checkout endpoint reads webhook traces from KV", () => { + const source = readFileSync(debugPath, "utf8"); + + expect(source).toContain("STRIPE_WEBHOOK_TRACE_KV"); + expect(source).toContain(".get"); + }); +}); diff --git a/server/tests/unit/cloudflare/stripe-webhook-worker-entry-trace.test.ts b/server/tests/unit/cloudflare/stripe-webhook-worker-entry-trace.test.ts new file mode 100644 index 000000000..909a17382 --- /dev/null +++ b/server/tests/unit/cloudflare/stripe-webhook-worker-entry-trace.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const workerEntryPath = join(import.meta.dir, "../../../src/index.ts"); + +describe("Stripe webhook Worker entry trace", () => { + test("records Stripe webhook ingress before Hono middleware runs", () => { + const source = readFileSync(workerEntryPath, "utf8"); + + expect(source).toContain("recordStripeWebhookIngressTrace"); + expect(source).toContain("isStripeWebhookIngressRequest"); + expect(source).toContain("request.clone()"); + expect(source).toContain("STRIPE_WEBHOOK_TRACE_KV"); + expect(source).toContain("worker_received"); + expect(source).toContain("stripe_webhook_trace:__last_ingress"); + expect(source).toContain("stripe_webhook_trace:__last_post"); + }); + + test("exposes a ready-token guarded KV round-trip diagnostic", () => { + const source = readFileSync(workerEntryPath, "utf8"); + + expect(source).toContain("handleStripeWebhookTraceKvDiagnostic"); + expect(source).toContain("/stripe-webhook-trace-kv"); + expect(source).toContain("diagnostic_round_trip"); + expect(source).toContain("handleStripeWebhookIngressDiagnostic"); + expect(source).toContain("/stripe-webhook-ingress"); + }); +}); diff --git a/server/tests/unit/webhooks/stripe-webhook-secret.test.ts b/server/tests/unit/webhooks/stripe-webhook-secret.test.ts new file mode 100644 index 000000000..4e0da0048 --- /dev/null +++ b/server/tests/unit/webhooks/stripe-webhook-secret.test.ts @@ -0,0 +1,36 @@ +import { AppEnv } from "@autumn/shared"; +import { describe, expect, test } from "bun:test"; +import { getStripeWebhookSecret } from "@/internal/orgs/orgUtils.js"; +import { encryptData } from "@/utils/encryptUtils.js"; + +const workerEnv = { + ENCRYPTION_PASSWORD: "test-password", +} as Env; + +const createOrg = (stripeConfig: Record) => + ({ + id: "org_test", + stripe_config: stripeConfig, + }) as never; + +describe("getStripeWebhookSecret", () => { + test("decrypts encrypted legacy Stripe webhook secrets", () => { + const org = createOrg({ + test_webhook_secret: encryptData("whsec_encrypted", workerEnv), + }); + + expect(getStripeWebhookSecret(org, AppEnv.Sandbox, workerEnv)).toBe( + "whsec_encrypted", + ); + }); + + test("uses plaintext whsec legacy Stripe webhook secrets without decrypting", () => { + const org = createOrg({ + test_webhook_secret: "whsec_plaintext", + }); + + expect(getStripeWebhookSecret(org, AppEnv.Sandbox, workerEnv)).toBe( + "whsec_plaintext", + ); + }); +}); diff --git a/server/worker-configuration.d.ts b/server/worker-configuration.d.ts index 7151bd908..b86b61b32 100644 --- a/server/worker-configuration.d.ts +++ b/server/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --strict-vars=false` (hash: abde6c710d9140eb6b570f8c24efd178) +// Generated by Wrangler by running `wrangler types --strict-vars=false` (hash: e70161a0c0764a8d9ed68acfaeb567f9) // Runtime types generated with workerd@1.20260611.1 2026-06-17 nodejs_compat interface __BaseEnv_Env { CACHE_KV?: KVNamespace; @@ -28,6 +28,9 @@ interface __BaseEnv_Env { RATE_LIMIT_DO: DurableObjectNamespace; SCHEDULED_WORKFLOW_DO: DurableObjectNamespace; EVENTS_ARCHIVE_PIPELINE: import("cloudflare:pipelines").Pipeline | import("cloudflare:pipelines").Pipeline; + STRIPE_WEBHOOK_TRACE_KV?: KVNamespace; + STRIPE_WEBHOOK_URL?: string; + SERVER_URL?: string; ANTHROPIC_API_KEY?: string; ATMN_OAUTH_CLIENT_IDS?: string; AUTUMN_SECRET_KEY?: string; @@ -84,7 +87,6 @@ interface __BaseEnv_Env { REVENUECAT_OAUTH_CLIENT_SECRET?: string; SENTRY_DSN?: string; SERVER_PORT?: string; - SERVER_URL?: string; SLACK_BOT_SCOPES?: string; SLACK_CLIENT_ID?: string; SLACK_REDIRECT_URI?: string; @@ -96,7 +98,6 @@ interface __BaseEnv_Env { STRIPE_SANDBOX_SECRET_KEY?: string; STRIPE_SANDBOX_WEBHOOK_SECRET?: string; STRIPE_WEBHOOK_SKIP_VERIFY?: string; - STRIPE_WEBHOOK_URL?: string; SVIX_API_KEY?: string; TEST_FILE_CONCURRENCY?: string; TESTS_ORG_ID?: string; @@ -150,7 +151,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types diff --git a/server/wrangler.jsonc b/server/wrangler.jsonc index 6cd94d7d5..08a5bdff0 100644 --- a/server/wrangler.jsonc +++ b/server/wrangler.jsonc @@ -10,6 +10,12 @@ "enabled": true, "head_sampling_rate": 1 }, + "routes": [ + { + "pattern": "autumn-api.bowong.cc", + "custom_domain": true + } + ], "triggers": { "crons": [ "* * * * *" @@ -23,6 +29,12 @@ "id": "9f952eb0603044d4beb78e634fe503bc" } ], + "kv_namespaces": [ + { + "binding": "STRIPE_WEBHOOK_TRACE_KV", + "id": "bee161b0d50c4fa68bb66afe51a780b6" + } + ], "r2_buckets": [ { "binding": "ADMIN_R2_BUCKET", @@ -145,11 +157,11 @@ "CLOUDFLARE_ACCOUNT_ID": "67720b647ff2b55cf37ba3ef9e677083", "CLOUDFLARE_API_TOKEN": "dlGquMNiAX-S7SV9pXne7YGfdH_fEgq3TfIGgNcQ", "CRITICAL_DB_POOL_MAX": "5", - "CRITICAL_DATABASE_URL": "postgres://autumn:autumn@127.0.0.1:5432/autumn", + "CRITICAL_DATABASE_URL": "postgresql://postgres:dJh0q4aaUb^PH3@34.21.187.198:5432/postgres", "CRON": "false", - "DATABASE_CRITICAL_URL": "postgres://autumn:autumn@127.0.0.1:5432/autumn", - "DATABASE_REPLICA_URL": "postgres://autumn:autumn@127.0.0.1:5432/autumn", - "DATABASE_URL": "postgres://autumn:autumn@127.0.0.1:5432/autumn", + "DATABASE_CRITICAL_URL": "postgresql://postgres:dJh0q4aaUb^PH3@34.21.187.198:5432/postgres", + "DATABASE_REPLICA_URL": "postgresql://postgres:dJh0q4aaUb^PH3@34.21.187.198:5432/postgres", + "DATABASE_URL": "postgresql://postgres:dJh0q4aaUb^PH3@34.21.187.198:5432/postgres", "DISABLE_CORS_CHECK": "true", "DISABLE_CRON": "false", "DISCORD_FEEDBACK_WEBHOOK": "http://localhost:8080/webhooks/discord-feedback", @@ -194,7 +206,7 @@ "STRIPE_LIVE_WEBHOOK_SECRET": "whsec_lV2YzyjMm1WcxpJIxRstRFiIB4c3EZnV", "STRIPE_SANDBOX_CLIENT_ID": "ca_test_9546ff0bd6ee3d3a551b9f277ea3e1815b2db37d", "STRIPE_SANDBOX_SECRET_KEY": "sk_test_ZkqfCzHwunOTVUiLluRkAxcU3c5x9vzg", - "STRIPE_SANDBOX_WEBHOOK_SECRET": "whsec_Gf0SU4UAK5PPJdp4no6e63IppLvcnAgy", + "STRIPE_SANDBOX_WEBHOOK_SECRET": "whsec_Bp2p2BuYWZBmRfFxhmJGECB382p5TWFt", "STRIPE_WEBHOOK_SKIP_VERIFY": "false", "SVIX_API_KEY": "sk_svix_IMsy8zrwtFxEuJ5ukq6lnRrODlifyuBb", "TEST_FILE_CONCURRENCY": "1", @@ -336,4 +348,4 @@ } } } -} \ No newline at end of file +} diff --git a/shared/models/otherModels/metadataTable.ts b/shared/models/otherModels/metadataTable.ts index 45103c1d8..d600c48e4 100644 --- a/shared/models/otherModels/metadataTable.ts +++ b/shared/models/otherModels/metadataTable.ts @@ -12,6 +12,7 @@ export enum MetadataType { CheckoutSessionV2Processing = "checkout_session_v2_processing", CheckoutSessionEnabledImmediately = "checkout_session_enabled_immediately", SetupPaymentV2 = "setup_payment_v2", + StripeWebhookTrace = "stripe_webhook_trace", } export const metadata = pgTable("metadata", {