@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": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAClC,SAAS,oCAAoC;AAC7C,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,yBAAyB,2BAA2B;AAEtD,SAAS,SAAS,WAAyB;AAChD,yBAAuB,8BAA8B,EAAE,SAAS,oBAAoB,CAAC;AACrF,yBAAuB,wBAAwB,EAAE,SAAS,aAAa,CAAC;AACxE,yBAAuB,wBAAwB,EAAE,SAAS,aAAa,CAAC;AACxE,yBAAuB,UAAU,qBAAqB;AAAA,IACpD,OAAO;AAAA,IACP,mBAAmB;AAAA,
|
|
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.
|
|
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.
|
|
66
|
-
"@open-mercato/queue": "0.9.1-develop.
|
|
67
|
-
"@open-mercato/ui": "0.9.1-develop.
|
|
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.
|
|
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.
|
|
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',
|