import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { isRecordLike } from '@sim/utils/object' import { NextResponse } from 'next/server' import Stripe from 'stripe' import type { AuthContext, EventFilterContext, FormatInputContext, FormatInputResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' import { skipByEventTypes } from '@/lib/webhooks/providers/utils' const logger = createLogger('WebhookProvider:Stripe') export const stripeHandler: WebhookProviderHandler = { verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) { const secret = providerConfig.webhookSecret as string | undefined if (!secret) { logger.warn( `[${requestId}] Stripe webhook missing webhookSecret in providerConfig — rejecting request` ) return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 }) } const signature = request.headers.get('stripe-signature') if (!signature) { logger.warn(`[${requestId}] Stripe webhook missing Stripe-Signature header`) return new NextResponse('Unauthorized - Missing Stripe signature', { status: 401 }) } try { Stripe.webhooks.constructEvent(rawBody, signature, secret) } catch (error) { logger.warn(`[${requestId}] Stripe signature verification failed`, { error: getErrorMessage(error), }) return new NextResponse('Unauthorized - Invalid Stripe signature', { status: 401 }) } return null }, async formatInput({ body }: FormatInputContext): Promise { return { input: body } }, shouldSkipEvent(ctx: EventFilterContext) { return skipByEventTypes(ctx, 'Stripe', logger) }, /** * Stripe event ids (evt_...) are globally unique and stable across retries — * Stripe resends the same event id on delivery retries, so keying on it * directly is sufficient without a content-derived fallback. */ extractIdempotencyId(body: unknown): string | null { if (!isRecordLike(body)) return null if (body.id && body.object === 'event') { return String(body.id) } return null }, }