@open-mercato/gateway-stripe 0.9.1-develop.7519.1.38f51f8dbb → 0.9.1-develop.7532.1.8eac7e198f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { asValue } from "awilix";
2
+ import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES, resolveWebhookBodyLimitBytes } from "@open-mercato/shared/lib/webhooks";
2
3
  import {
3
4
  registerGatewayAdapter,
4
5
  registerPaymentGatewayDescriptor,
@@ -15,7 +16,8 @@ function register(container) {
15
16
  registerGatewayAdapter(stripeAdapterV20231016, { version: "2023-10-16" });
16
17
  registerWebhookHandler("stripe", verifyStripeWebhook, {
17
18
  queue: "stripe-webhook",
18
- readSessionIdHint: readStripeSessionIdHint
19
+ readSessionIdHint: readStripeSessionIdHint,
20
+ maxBodyBytes: Math.min(resolveWebhookBodyLimitBytes(), DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
19
21
  });
20
22
  registerPaymentGatewayDescriptor({
21
23
  providerKey: "stripe",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/gateway_stripe/di.ts"],
4
- "sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n registerGatewayAdapter,\n registerPaymentGatewayDescriptor,\n registerWebhookHandler,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport { stripeHealthCheck } from './lib/health'\nimport { stripeAdapterV20250224Acacia } from './lib/adapters/v2025-02-24.acacia'\nimport { stripeAdapterV20241218 } from './lib/adapters/v2024-12-18'\nimport { stripeAdapterV20231016 } from './lib/adapters/v2023-10-16'\nimport { readStripeSessionIdHint, verifyStripeWebhook } from './lib/webhook-handler'\n\nexport function register(container: AppContainer) {\n registerGatewayAdapter(stripeAdapterV20250224Acacia, { version: '2025-02-24.acacia' })\n registerGatewayAdapter(stripeAdapterV20241218, { version: '2024-12-18' })\n registerGatewayAdapter(stripeAdapterV20231016, { version: '2023-10-16' })\n registerWebhookHandler('stripe', verifyStripeWebhook, {\n queue: 'stripe-webhook',\n readSessionIdHint: readStripeSessionIdHint,\n })\n registerPaymentGatewayDescriptor({\n providerKey: 'stripe',\n label: 'Stripe',\n sessionConfig: {\n fields: [\n {\n key: 'captureMethod',\n label: 'Capture method',\n type: 'select',\n required: false,\n options: [\n { value: 'automatic', label: 'Automatic capture' },\n { value: 'manual', label: 'Manual capture' },\n ],\n },\n {\n key: 'paymentTypes',\n label: 'Payment methods',\n type: 'multiselect',\n required: false,\n options: [\n { value: 'card', label: 'Credit card' },\n { value: 'apple_pay', label: 'Apple Pay' },\n { value: 'google_pay', label: 'Google Pay' },\n { value: 'link', label: 'Link' },\n ],\n },\n ],\n supportedCurrencies: '*',\n supportedPaymentTypes: [\n { value: 'card', label: 'Card' },\n { value: 'apple_pay', label: 'Apple Pay' },\n { value: 'google_pay', label: 'Google Pay' },\n { value: 'link', label: 'Link' },\n ],\n defaultRendererKey: 'stripe.payment_element',\n renderers: [\n {\n key: 'stripe.payment_element',\n label: 'Stripe Payment Element',\n type: 'embedded',\n description: 'Embeds Stripe Elements on the merchant page while Stripe handles payment method UX and required redirects.',\n supportedPaymentTypes: ['card', 'apple_pay', 'google_pay', 'link'],\n settingsFields: [\n {\n key: 'layout',\n label: 'Layout',\n type: 'select',\n required: false,\n options: [\n { value: 'tabs', label: 'Tabs' },\n { value: 'accordion', label: 'Accordion' },\n ],\n },\n {\n key: 'paymentMethodOrder',\n label: 'Payment method order',\n type: 'multiselect',\n required: false,\n options: [\n { value: 'card', label: 'Card' },\n { value: 'apple_pay', label: 'Apple Pay' },\n { value: 'google_pay', label: 'Google Pay' },\n { value: 'link', label: 'Link' },\n ],\n },\n {\n key: 'billingDetails',\n label: 'Billing details collection',\n type: 'select',\n required: false,\n options: [\n { value: 'auto', label: 'Auto' },\n { value: 'if_required', label: 'If required' },\n { value: 'never', label: 'Never' },\n ],\n },\n ],\n },\n ],\n presentation: 'either',\n embeddedRenderers: ['stripe.payment_element'],\n },\n })\n\n container.register({\n stripeHealthCheck: asValue(stripeHealthCheck),\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES, resolveWebhookBodyLimitBytes } from '@open-mercato/shared/lib/webhooks'\nimport {\n registerGatewayAdapter,\n registerPaymentGatewayDescriptor,\n registerWebhookHandler,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport { stripeHealthCheck } from './lib/health'\nimport { stripeAdapterV20250224Acacia } from './lib/adapters/v2025-02-24.acacia'\nimport { stripeAdapterV20241218 } from './lib/adapters/v2024-12-18'\nimport { stripeAdapterV20231016 } from './lib/adapters/v2023-10-16'\nimport { readStripeSessionIdHint, verifyStripeWebhook } from './lib/webhook-handler'\n\nexport function register(container: AppContainer) {\n registerGatewayAdapter(stripeAdapterV20250224Acacia, { version: '2025-02-24.acacia' })\n registerGatewayAdapter(stripeAdapterV20241218, { version: '2024-12-18' })\n registerGatewayAdapter(stripeAdapterV20231016, { version: '2023-10-16' })\n registerWebhookHandler('stripe', verifyStripeWebhook, {\n queue: 'stripe-webhook',\n readSessionIdHint: readStripeSessionIdHint,\n maxBodyBytes: Math.min(resolveWebhookBodyLimitBytes(), DEFAULT_WEBHOOK_BODY_LIMIT_BYTES),\n })\n registerPaymentGatewayDescriptor({\n providerKey: 'stripe',\n label: 'Stripe',\n sessionConfig: {\n fields: [\n {\n key: 'captureMethod',\n label: 'Capture method',\n type: 'select',\n required: false,\n options: [\n { value: 'automatic', label: 'Automatic capture' },\n { value: 'manual', label: 'Manual capture' },\n ],\n },\n {\n key: 'paymentTypes',\n label: 'Payment methods',\n type: 'multiselect',\n required: false,\n options: [\n { value: 'card', label: 'Credit card' },\n { value: 'apple_pay', label: 'Apple Pay' },\n { value: 'google_pay', label: 'Google Pay' },\n { value: 'link', label: 'Link' },\n ],\n },\n ],\n supportedCurrencies: '*',\n supportedPaymentTypes: [\n { value: 'card', label: 'Card' },\n { value: 'apple_pay', label: 'Apple Pay' },\n { value: 'google_pay', label: 'Google Pay' },\n { value: 'link', label: 'Link' },\n ],\n defaultRendererKey: 'stripe.payment_element',\n renderers: [\n {\n key: 'stripe.payment_element',\n label: 'Stripe Payment Element',\n type: 'embedded',\n description: 'Embeds Stripe Elements on the merchant page while Stripe handles payment method UX and required redirects.',\n supportedPaymentTypes: ['card', 'apple_pay', 'google_pay', 'link'],\n settingsFields: [\n {\n key: 'layout',\n label: 'Layout',\n type: 'select',\n required: false,\n options: [\n { value: 'tabs', label: 'Tabs' },\n { value: 'accordion', label: 'Accordion' },\n ],\n },\n {\n key: 'paymentMethodOrder',\n label: 'Payment method order',\n type: 'multiselect',\n required: false,\n options: [\n { value: 'card', label: 'Card' },\n { value: 'apple_pay', label: 'Apple Pay' },\n { value: 'google_pay', label: 'Google Pay' },\n { value: 'link', label: 'Link' },\n ],\n },\n {\n key: 'billingDetails',\n label: 'Billing details collection',\n type: 'select',\n required: false,\n options: [\n { value: 'auto', label: 'Auto' },\n { value: 'if_required', label: 'If required' },\n { value: 'never', label: 'Never' },\n ],\n },\n ],\n },\n ],\n presentation: 'either',\n embeddedRenderers: ['stripe.payment_element'],\n },\n })\n\n container.register({\n stripeHealthCheck: asValue(stripeHealthCheck),\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/gateway-stripe",
3
- "version": "0.9.1-develop.7519.1.38f51f8dbb",
3
+ "version": "0.9.1-develop.7532.1.8eac7e198f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,21 +62,21 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
- "@open-mercato/core": "0.9.1-develop.7519.1.38f51f8dbb",
66
- "@open-mercato/queue": "0.9.1-develop.7519.1.38f51f8dbb",
67
- "@open-mercato/ui": "0.9.1-develop.7519.1.38f51f8dbb",
65
+ "@open-mercato/core": "0.9.1-develop.7532.1.8eac7e198f",
66
+ "@open-mercato/queue": "0.9.1-develop.7532.1.8eac7e198f",
67
+ "@open-mercato/ui": "0.9.1-develop.7532.1.8eac7e198f",
68
68
  "@stripe/react-stripe-js": "^6.8.2",
69
69
  "@stripe/stripe-js": "^9.14.0",
70
70
  "stripe": "^22.3.1"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@mikro-orm/postgresql": "^7.0.14",
74
- "@open-mercato/shared": "0.9.1-develop.7519.1.38f51f8dbb",
74
+ "@open-mercato/shared": "0.9.1-develop.7532.1.8eac7e198f",
75
75
  "react": "^19.0.0",
76
76
  "react-dom": "^19.0.0"
77
77
  },
78
78
  "devDependencies": {
79
- "@open-mercato/shared": "0.9.1-develop.7519.1.38f51f8dbb",
79
+ "@open-mercato/shared": "0.9.1-develop.7532.1.8eac7e198f",
80
80
  "@types/jest": "^30.0.0",
81
81
  "@types/react": "^19.2.18",
82
82
  "@types/react-dom": "^19.2.5",
@@ -0,0 +1,224 @@
1
+ /** @jest-environment node */
2
+ import Stripe from 'stripe'
3
+ import { POST } from '@open-mercato/core/modules/payment_gateways/api/webhook/[provider]/route'
4
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { getWebhookHandler } from '@open-mercato/shared/modules/payment_gateways/types'
6
+ import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from '@open-mercato/shared/lib/webhooks'
7
+ import { register } from '../di'
8
+ import { readStripeSessionIdHint, verifyStripeWebhook } from '../lib/webhook-handler'
9
+
10
+ const mockResolve = jest.fn()
11
+ const mockRateLimiterService = { trustProxyDepth: 1, consume: jest.fn() }
12
+ const mockEnqueue = jest.fn(async () => undefined)
13
+ const mockCredentialsResolve = jest.fn(async () => ({}))
14
+
15
+ jest.mock('@open-mercato/shared/lib/di/container', () => ({
16
+ createRequestContainer: jest.fn(async () => ({ resolve: mockResolve })),
17
+ }))
18
+
19
+ jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
20
+ findWithDecryption: jest.fn(async () => []),
21
+ }))
22
+
23
+ jest.mock('@open-mercato/core/modules/payment_gateways/lib/queue', () => ({
24
+ getPaymentGatewayQueue: jest.fn(() => ({ enqueue: mockEnqueue })),
25
+ }))
26
+
27
+ jest.mock('../lib/webhook-handler', () => {
28
+ const actual = jest.requireActual('../lib/webhook-handler')
29
+ return {
30
+ ...actual,
31
+ verifyStripeWebhook: jest.fn(actual.verifyStripeWebhook),
32
+ readStripeSessionIdHint: jest.fn(actual.readStripeSessionIdHint),
33
+ }
34
+ })
35
+
36
+ const WEBHOOK_URL = 'http://localhost/api/payment_gateways/webhook/stripe'
37
+
38
+ function chunkedBody(totalBytes: number, chunkBytes = 64 * 1024): ReadableStream<Uint8Array> {
39
+ let sent = 0
40
+ return new ReadableStream<Uint8Array>({
41
+ pull(controller) {
42
+ if (sent >= totalBytes) {
43
+ controller.close()
44
+ return
45
+ }
46
+ const size = Math.min(chunkBytes, totalBytes - sent)
47
+ controller.enqueue(new Uint8Array(size).fill(0x20))
48
+ sent += size
49
+ },
50
+ })
51
+ }
52
+
53
+ function streamedRequest(totalBytes: number, headers: Record<string, string> = {}): Request {
54
+ return new Request(WEBHOOK_URL, {
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json', 'stripe-signature': 't=1,v1=00', ...headers },
57
+ body: chunkedBody(totalBytes),
58
+ duplex: 'half',
59
+ } as RequestInit & { duplex: 'half' })
60
+ }
61
+
62
+ function paddedEventJson(totalBytes: number): string {
63
+ const prefix = '{"id":"evt_test","type":"payment_intent.succeeded","data":{"object":{"id":"pi_test"}},"pad":"'
64
+ const suffix = '"}'
65
+ return prefix + 'x'.repeat(totalBytes - prefix.length - suffix.length) + suffix
66
+ }
67
+
68
+ describe('Stripe webhook body limit', () => {
69
+ const originalLimitEnv = process.env.OM_WEBHOOK_MAX_BODY_BYTES
70
+
71
+ beforeAll(() => {
72
+ delete process.env.OM_WEBHOOK_MAX_BODY_BYTES
73
+ })
74
+
75
+ afterAll(() => {
76
+ if (originalLimitEnv === undefined) delete process.env.OM_WEBHOOK_MAX_BODY_BYTES
77
+ else process.env.OM_WEBHOOK_MAX_BODY_BYTES = originalLimitEnv
78
+ })
79
+
80
+ beforeEach(() => {
81
+ register({ register: jest.fn() } as never)
82
+ jest.clearAllMocks()
83
+ mockRateLimiterService.consume.mockResolvedValue({
84
+ allowed: true,
85
+ remainingPoints: 59,
86
+ msBeforeNext: 0,
87
+ consumedPoints: 1,
88
+ })
89
+ mockResolve.mockImplementation((token: string) => {
90
+ if (token === 'rateLimiterService') return mockRateLimiterService
91
+ if (token === 'em') return {}
92
+ if (token === 'paymentGatewayService') return {}
93
+ if (token === 'integrationCredentialsService') return { resolve: mockCredentialsResolve }
94
+ if (token === 'integrationLogService') return {}
95
+ throw new Error(`[internal] Unexpected token: ${token}`)
96
+ })
97
+ })
98
+
99
+ test('the Stripe registration caps bodies at the shared webhook limit', () => {
100
+ expect(getWebhookHandler('stripe')?.maxBodyBytes).toBe(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)
101
+ })
102
+
103
+ test.each([
104
+ ['4096', 4096],
105
+ [String(10 * 1024 * 1024), DEFAULT_WEBHOOK_BODY_LIMIT_BYTES],
106
+ ])('honours OM_WEBHOOK_MAX_BODY_BYTES=%s only when it is lower than 1 MiB', (envValue, expected) => {
107
+ const previous = process.env.OM_WEBHOOK_MAX_BODY_BYTES
108
+ process.env.OM_WEBHOOK_MAX_BODY_BYTES = envValue
109
+ try {
110
+ jest.isolateModules(() => {
111
+ const isolatedTypes = require('@open-mercato/shared/modules/payment_gateways/types') as typeof import('@open-mercato/shared/modules/payment_gateways/types')
112
+ const isolatedDi = require('../di') as typeof import('../di')
113
+ isolatedDi.register({ register: jest.fn() } as never)
114
+ expect(isolatedTypes.getWebhookHandler('stripe')?.maxBodyBytes).toBe(expected)
115
+ })
116
+ } finally {
117
+ if (previous === undefined) delete process.env.OM_WEBHOOK_MAX_BODY_BYTES
118
+ else process.env.OM_WEBHOOK_MAX_BODY_BYTES = previous
119
+ register({ register: jest.fn() } as never)
120
+ }
121
+ })
122
+
123
+ test('rejects a body whose Content-Length exceeds the limit without verifying or locating', async () => {
124
+ const response = await POST(
125
+ new Request(WEBHOOK_URL, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'content-type': 'application/json',
129
+ 'content-length': String(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES + 1),
130
+ },
131
+ body: '{}',
132
+ }),
133
+ { params: { provider: 'stripe' } },
134
+ )
135
+
136
+ expect(response.status).toBe(413)
137
+ await expect(response.json()).resolves.toEqual({ error: 'Webhook payload too large' })
138
+ expect(readStripeSessionIdHint).not.toHaveBeenCalled()
139
+ expect(verifyStripeWebhook).not.toHaveBeenCalled()
140
+ expect(findWithDecryption).not.toHaveBeenCalled()
141
+ })
142
+
143
+ test('rejects an oversized chunked body without a Content-Length', async () => {
144
+ const response = await POST(streamedRequest(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES * 4), { params: { provider: 'stripe' } })
145
+
146
+ expect(response.status).toBe(413)
147
+ expect(readStripeSessionIdHint).not.toHaveBeenCalled()
148
+ expect(verifyStripeWebhook).not.toHaveBeenCalled()
149
+ expect(findWithDecryption).not.toHaveBeenCalled()
150
+ })
151
+
152
+ test('rejects an oversized body that declares a small Content-Length', async () => {
153
+ const response = await POST(
154
+ streamedRequest(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES + 1, { 'content-length': '2' }),
155
+ { params: { provider: 'stripe' } },
156
+ )
157
+
158
+ expect(response.status).toBe(413)
159
+ expect(readStripeSessionIdHint).not.toHaveBeenCalled()
160
+ expect(verifyStripeWebhook).not.toHaveBeenCalled()
161
+ expect(findWithDecryption).not.toHaveBeenCalled()
162
+ })
163
+
164
+ test('still reads an event of exactly the limit and looks up its transaction', async () => {
165
+ const response = await POST(
166
+ new Request(WEBHOOK_URL, {
167
+ method: 'POST',
168
+ headers: { 'content-type': 'application/json', 'stripe-signature': 't=1,v1=00' },
169
+ body: paddedEventJson(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES),
170
+ }),
171
+ { params: { provider: 'stripe' } },
172
+ )
173
+
174
+ expect(response.status).toBe(401)
175
+ expect(readStripeSessionIdHint).toHaveBeenCalledTimes(1)
176
+ expect(findWithDecryption).toHaveBeenCalledWith(
177
+ expect.anything(),
178
+ expect.anything(),
179
+ expect.objectContaining({ providerKey: 'stripe', providerSessionId: 'pi_test' }),
180
+ expect.anything(),
181
+ )
182
+ })
183
+
184
+ test('accepts a correctly signed event just under the limit and queues it', async () => {
185
+ const previousStrategy = process.env.QUEUE_STRATEGY
186
+ process.env.QUEUE_STRATEGY = 'async'
187
+ const webhookSecret = 'whsec_body_limit_test'
188
+ const payload = paddedEventJson(DEFAULT_WEBHOOK_BODY_LIMIT_BYTES - 4096)
189
+ const signature = new Stripe('sk_test_body_limit').webhooks.generateTestHeaderString({ payload, secret: webhookSecret })
190
+ ;(findWithDecryption as jest.Mock).mockResolvedValueOnce([
191
+ {
192
+ id: 'transaction-1',
193
+ paymentId: 'payment-1',
194
+ providerSessionId: 'pi_test',
195
+ amount: '10.00',
196
+ currencyCode: 'USD',
197
+ organizationId: 'org-1',
198
+ tenantId: 'tenant-1',
199
+ },
200
+ ])
201
+ mockCredentialsResolve.mockResolvedValueOnce({ secretKey: 'sk_test_body_limit', webhookSecret })
202
+ try {
203
+ const response = await POST(
204
+ new Request(WEBHOOK_URL, {
205
+ method: 'POST',
206
+ headers: { 'content-type': 'application/json', 'stripe-signature': signature },
207
+ body: payload,
208
+ }),
209
+ { params: { provider: 'stripe' } },
210
+ )
211
+
212
+ expect(response.status).toBe(202)
213
+ expect(verifyStripeWebhook).toHaveBeenCalledTimes(1)
214
+ expect(mockEnqueue).toHaveBeenCalledWith(expect.objectContaining({
215
+ providerKey: 'stripe',
216
+ transactionId: 'transaction-1',
217
+ event: expect.objectContaining({ eventId: 'evt_test', eventType: 'payment_intent.succeeded' }),
218
+ }))
219
+ } finally {
220
+ if (previousStrategy === undefined) delete process.env.QUEUE_STRATEGY
221
+ else process.env.QUEUE_STRATEGY = previousStrategy
222
+ }
223
+ })
224
+ })
@@ -1,5 +1,6 @@
1
1
  import { asValue } from 'awilix'
2
2
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES, resolveWebhookBodyLimitBytes } from '@open-mercato/shared/lib/webhooks'
3
4
  import {
4
5
  registerGatewayAdapter,
5
6
  registerPaymentGatewayDescriptor,
@@ -18,6 +19,7 @@ export function register(container: AppContainer) {
18
19
  registerWebhookHandler('stripe', verifyStripeWebhook, {
19
20
  queue: 'stripe-webhook',
20
21
  readSessionIdHint: readStripeSessionIdHint,
22
+ maxBodyBytes: Math.min(resolveWebhookBodyLimitBytes(), DEFAULT_WEBHOOK_BODY_LIMIT_BYTES),
21
23
  })
22
24
  registerPaymentGatewayDescriptor({
23
25
  providerKey: 'stripe',