fix: trace stripe webhooks on cloudflare

This commit is contained in:
2026-07-02 03:36:17 -07:00
parent fd95b2da5c
commit e2366bdde7
21 changed files with 934 additions and 23 deletions

View File

@@ -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<string, string> })
.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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<void> => {
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;

View File

@@ -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 {

View File

@@ -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<string, string> }
).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);
}

View File

@@ -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<string, string> })
.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<string, string>;
}
).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<string, string> })
.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<string, string> })
.metadata?.autumn_metadata_id,
});
await next();
};

View File

@@ -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<string, unknown>;
};
export const getStripeWebhookTraceKey = (eventId: string) =>
`stripe_webhook_trace:${eventId}`;
export const recordStripeWebhookTrace = async ({
ctx,
stage,
data = {},
}: {
ctx: StripeWebhookContext;
stage: string;
data?: Record<string, unknown>;
}) => {
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<string, string> }).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<StripeWebhookTraceEntry[]>(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),
});
}
};

View File

@@ -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<HonoEnv>, 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,

View File

@@ -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<string, string>;
})
: undefined;
};
const appendStripeWebhookIngressTrace = async ({
kv,
key,
entry,
}: {
kv: KVNamespace;
key: string;
entry: StripeWebhookTraceEntry;
}) => {
const existing = (await kv.get<StripeWebhookTraceEntry[]>(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<Response | null> => {
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<StripeWebhookTraceEntry[]>(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<Response | null> => {
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<StripeWebhookTraceEntry[]>(
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);
},

View File

@@ -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<HonoEnv>();
configsRouter.post("/push", ...handlePushOrganisationConfiguration);
configsRouter.delete("/nuke", ...handleNukeOrganisationConfiguration);
configsRouter.post("/debug/stripe-checkout", ...handleDebugStripeCheckout);

View File

@@ -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<StripeWebhookTraceEntry[]>(
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,
});
},
});

View File

@@ -184,6 +184,10 @@ export const getStripeWebhookSecret = (
});
}
if (webhookSecret.startsWith("whsec_")) {
return webhookSecret;
}
return decryptData(webhookSecret, workerEnv);
};

View File

@@ -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");
});
});

View File

@@ -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/");
});
});

View File

@@ -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<StripeWebhookTraceEntry[]>");
});
});

View File

@@ -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");
});
});

View File

@@ -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<string, unknown>) =>
({
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",
);
});
});

View File

@@ -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<import("./src/index").RateLimitDurableObject>;
SCHEDULED_WORKFLOW_DO: DurableObjectNamespace<import("./src/index").ScheduledWorkflowDurableObject>;
EVENTS_ARCHIVE_PIPELINE: import("cloudflare:pipelines").Pipeline<Cloudflare.AutumnEventsArchiveStagingRecord> | import("cloudflare:pipelines").Pipeline<Cloudflare.AutumnEventsArchiveRecord>;
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<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLOUDFLARE_ANALYTICS_ARCHIVE_ENABLED" | "CLOUDFLARE_ANALYTICS_DATASET" | "CLOUDFLARE_ANALYTICS_HOT_READ_ENABLED" | "CLOUDFLARE_ANALYTICS_HOT_RETENTION_DAYS" | "CLOUDFLARE_ANALYTICS_WRITE_ENABLED" | "CLOUDFLARE_ACCOUNT_ID" | "CLOUDFLARE_API_TOKEN" | "EMAIL_DOMAIN" | "USE_KV_CACHE" | "USE_DO_IDEMPOTENCY" | "USE_DO_LOCK" | "USE_DO_RATE_LIMIT" | "ANTHROPIC_API_KEY" | "ATMN_OAUTH_CLIENT_IDS" | "AUTUMN_SECRET_KEY" | "AUTUMN_TEST_BASE_URL" | "AUTUMN_WEBHOOK_SECRET" | "AWS_REGION" | "AXIOM_ADMIN_TOKEN" | "AXIOM_METRICS_DATASET" | "AXIOM_ORG_ID" | "AXIOM_SUCCESS_REQUEST_LOG_SAMPLE_RATE" | "AXIOM_TOKEN" | "BETTER_AUTH_SECRET" | "BETTER_AUTH_URL" | "CHAT_SERVER_URL" | "CHAT_STATE_SECRET" | "CLIENT_URL" | "CRITICAL_DB_POOL_MAX" | "CRITICAL_DATABASE_URL" | "CRON" | "DATABASE_CRITICAL_URL" | "DATABASE_REPLICA_URL" | "DATABASE_URL" | "DISABLE_CORS_CHECK" | "DISABLE_CRON" | "DISCORD_FEEDBACK_WEBHOOK" | "ECS_CONTAINER_METADATA_URI_V4" | "EMULATE_GOOGLE_URL" | "ENCRYPTION_IV" | "ENCRYPTION_PASSWORD" | "ENV_FILE" | "FC_GIT_COMMIT_SHA" | "GENERAL_DB_POOL_MAX" | "GOOGLE_CLIENT_ID" | "GOOGLE_CLIENT_SECRET" | "HATCHET_CLIENT_TOKEN" | "IMAGE_TAG" | "INFISICAL_CLIENT_ID" | "INFISICAL_CLIENT_SECRET" | "INFISICAL_ENVIRONMENT" | "INFISICAL_PROJECT_ID" | "INTERNAL_MCP_OAUTH_CLIENT_ID" | "LOCALTUNNEL_RESERVED_KEY" | "LOOPS_API_KEY" | "MCP_RESOURCE_URLS" | "MCP_SERVER_URL" | "NGROK_URL" | "NODE_ENV" | "OTEL_SERVICE_NAME" | "POSTHOG_API_KEY" | "POSTHOG_HOST" | "READY_CHECK_TOKEN" | "REPLICA_DB_POOL_MAX" | "REVENUECAT_OAUTH_CLIENT_ID" | "REVENUECAT_OAUTH_CLIENT_SECRET" | "SENTRY_DSN" | "SERVER_PORT" | "SERVER_URL" | "SLACK_BOT_SCOPES" | "SLACK_CLIENT_ID" | "SLACK_REDIRECT_URI" | "SLACK_STATE_SECRET" | "STRIPE_LIVE_CLIENT_ID" | "STRIPE_LIVE_SECRET_KEY" | "STRIPE_LIVE_WEBHOOK_SECRET" | "STRIPE_SANDBOX_CLIENT_ID" | "STRIPE_SANDBOX_SECRET_KEY" | "STRIPE_SANDBOX_WEBHOOK_SECRET" | "STRIPE_WEBHOOK_SKIP_VERIFY" | "STRIPE_WEBHOOK_URL" | "SVIX_API_KEY" | "TEST_FILE_CONCURRENCY" | "TESTS_ORG_ID" | "TINYBIRD_API_URL" | "TINYBIRD_TOKEN" | "TINYBIRD_US_EAST_API_URL" | "TRIGGER_SERVER_SECRET_KEY" | "UNIT_TEST_AUTUMN_SECRET_KEY" | "VITE_FRONTEND_URL" | "WORKER">> {}
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLOUDFLARE_ANALYTICS_ARCHIVE_ENABLED" | "CLOUDFLARE_ANALYTICS_DATASET" | "CLOUDFLARE_ANALYTICS_HOT_READ_ENABLED" | "CLOUDFLARE_ANALYTICS_HOT_RETENTION_DAYS" | "CLOUDFLARE_ANALYTICS_WRITE_ENABLED" | "CLOUDFLARE_ACCOUNT_ID" | "CLOUDFLARE_API_TOKEN" | "EMAIL_DOMAIN" | "USE_KV_CACHE" | "USE_DO_IDEMPOTENCY" | "USE_DO_LOCK" | "USE_DO_RATE_LIMIT" | "STRIPE_WEBHOOK_URL" | "SERVER_URL" | "ANTHROPIC_API_KEY" | "ATMN_OAUTH_CLIENT_IDS" | "AUTUMN_SECRET_KEY" | "AUTUMN_TEST_BASE_URL" | "AUTUMN_WEBHOOK_SECRET" | "AWS_REGION" | "AXIOM_ADMIN_TOKEN" | "AXIOM_METRICS_DATASET" | "AXIOM_ORG_ID" | "AXIOM_SUCCESS_REQUEST_LOG_SAMPLE_RATE" | "AXIOM_TOKEN" | "BETTER_AUTH_SECRET" | "BETTER_AUTH_URL" | "CHAT_SERVER_URL" | "CHAT_STATE_SECRET" | "CLIENT_URL" | "CRITICAL_DB_POOL_MAX" | "CRITICAL_DATABASE_URL" | "CRON" | "DATABASE_CRITICAL_URL" | "DATABASE_REPLICA_URL" | "DATABASE_URL" | "DISABLE_CORS_CHECK" | "DISABLE_CRON" | "DISCORD_FEEDBACK_WEBHOOK" | "ECS_CONTAINER_METADATA_URI_V4" | "EMULATE_GOOGLE_URL" | "ENCRYPTION_IV" | "ENCRYPTION_PASSWORD" | "ENV_FILE" | "FC_GIT_COMMIT_SHA" | "GENERAL_DB_POOL_MAX" | "GOOGLE_CLIENT_ID" | "GOOGLE_CLIENT_SECRET" | "HATCHET_CLIENT_TOKEN" | "IMAGE_TAG" | "INFISICAL_CLIENT_ID" | "INFISICAL_CLIENT_SECRET" | "INFISICAL_ENVIRONMENT" | "INFISICAL_PROJECT_ID" | "INTERNAL_MCP_OAUTH_CLIENT_ID" | "LOCALTUNNEL_RESERVED_KEY" | "LOOPS_API_KEY" | "MCP_RESOURCE_URLS" | "MCP_SERVER_URL" | "NGROK_URL" | "NODE_ENV" | "OTEL_SERVICE_NAME" | "POSTHOG_API_KEY" | "POSTHOG_HOST" | "READY_CHECK_TOKEN" | "REPLICA_DB_POOL_MAX" | "REVENUECAT_OAUTH_CLIENT_ID" | "REVENUECAT_OAUTH_CLIENT_SECRET" | "SENTRY_DSN" | "SERVER_PORT" | "SLACK_BOT_SCOPES" | "SLACK_CLIENT_ID" | "SLACK_REDIRECT_URI" | "SLACK_STATE_SECRET" | "STRIPE_LIVE_CLIENT_ID" | "STRIPE_LIVE_SECRET_KEY" | "STRIPE_LIVE_WEBHOOK_SECRET" | "STRIPE_SANDBOX_CLIENT_ID" | "STRIPE_SANDBOX_SECRET_KEY" | "STRIPE_SANDBOX_WEBHOOK_SECRET" | "STRIPE_WEBHOOK_SKIP_VERIFY" | "SVIX_API_KEY" | "TEST_FILE_CONCURRENCY" | "TESTS_ORG_ID" | "TINYBIRD_API_URL" | "TINYBIRD_TOKEN" | "TINYBIRD_US_EAST_API_URL" | "TRIGGER_SERVER_SECRET_KEY" | "UNIT_TEST_AUTUMN_SECRET_KEY" | "VITE_FRONTEND_URL" | "WORKER">> {}
}
// Begin runtime types

View File

@@ -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 @@
}
}
}
}
}

View File

@@ -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", {