From 8f943c4c47ddcc662c0afcbd6a0645bcb59b782e Mon Sep 17 00:00:00 2001 From: Matthew Mueller Date: Tue, 12 May 2026 16:30:17 -0500 Subject: [PATCH] fix(source-stripe): accept string `request` field in stripeEventSchema Older Stripe API versions send `event.request` as a plain string (the request ID), not the `{ id, idempotency_key }` object used by modern versions. When sdb-webhook-srv forwards these raw events as source_input messages, Zod rejects the string at parse time, silently dropping the record from the sync pipeline. Widen the schema to accept both formats via z.union. Co-Authored-By: Claude Opus 4.6 (1M context) Committed-By-Agent: claude --- packages/source-stripe/src/spec.test.ts | 30 ++++++++++++++++++++++++- packages/source-stripe/src/spec.ts | 11 +++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/source-stripe/src/spec.test.ts b/packages/source-stripe/src/spec.test.ts index d2b1e9ec9..c7222734e 100644 --- a/packages/source-stripe/src/spec.test.ts +++ b/packages/source-stripe/src/spec.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { z } from 'zod' -import spec, { configSchema, streamStateSpec } from './spec.js' +import spec, { configSchema, stripeEventSchema, streamStateSpec } from './spec.js' import { BUNDLED_API_VERSION, SUPPORTED_API_VERSIONS } from '@stripe/sync-openapi' describe('configSchema api_version field', () => { @@ -34,6 +34,34 @@ describe('configSchema api_version field', () => { }) }) +describe('stripeEventSchema', () => { + const baseEvent = { + id: 'evt_123', + object: 'event' as const, + api_version: '2024-06-20', + created: 1715500000, + type: 'invoice.created', + livemode: true, + pending_webhooks: 0, + data: { object: { id: 'in_123', object: 'invoice' } }, + } + + it('accepts request as a plain string (older API versions)', () => { + const event = { ...baseEvent, request: 'req_xxxxx' } + expect(stripeEventSchema.safeParse(event).success).toBe(true) + }) + + it('accepts request as an object (modern API versions)', () => { + const event = { ...baseEvent, request: { id: 'req_xxxxx', idempotency_key: null } } + expect(stripeEventSchema.safeParse(event).success).toBe(true) + }) + + it('accepts request as null', () => { + const event = { ...baseEvent, request: null } + expect(stripeEventSchema.safeParse(event).success).toBe(true) + }) +}) + describe('streamStateSpec JSON Schema round-trip', () => { it('accounted_range survives toJSONSchema → fromJSONSchema round-trip', () => { // The engine converts streamStateSpec to JSON Schema (spec export) then back diff --git a/packages/source-stripe/src/spec.ts b/packages/source-stripe/src/spec.ts index 769fe1898..b1e442643 100644 --- a/packages/source-stripe/src/spec.ts +++ b/packages/source-stripe/src/spec.ts @@ -113,10 +113,13 @@ export const stripeEventSchema = z.object({ "Number of webhooks that haven't been successfully delivered (for example, to return a 20x response) to the URLs you specify." ), request: z - .object({ - id: z.string().nullable(), - idempotency_key: z.string().nullable(), - }) + .union([ + z.string(), + z.object({ + id: z.string().nullable(), + idempotency_key: z.string().nullable(), + }), + ]) .nullable() .describe('Information on the API request that triggers the event.'), type: z