@happyvertical/payments 0.74.8
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.
- package/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +97 -0
- package/dist/adapters/base-usdc.d.ts +85 -0
- package/dist/adapters/base-usdc.d.ts.map +1 -0
- package/dist/adapters/base-usdc.js +1182 -0
- package/dist/adapters/base-usdc.js.map +1 -0
- package/dist/adapters/btc.d.ts +63 -0
- package/dist/adapters/btc.d.ts.map +1 -0
- package/dist/adapters/btc.js +843 -0
- package/dist/adapters/btc.js.map +1 -0
- package/dist/adapters/stripe.d.ts +54 -0
- package/dist/adapters/stripe.d.ts.map +1 -0
- package/dist/adapters/stripe.js +696 -0
- package/dist/adapters/stripe.js.map +1 -0
- package/dist/chunks/errors-BgFC46qQ.js +45 -0
- package/dist/chunks/errors-BgFC46qQ.js.map +1 -0
- package/dist/chunks/shared-DGHSqDQT.js +392 -0
- package/dist/chunks/shared-DGHSqDQT.js.map +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/factory.d.ts +13 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/shared.d.ts +32 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/testing/conformance.d.ts +33 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +114 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/types.d.ts +174 -0
- package/dist/types.d.ts.map +1 -0
- package/metadata.json +30 -0
- package/package.json +88 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { P as PaymentConfigurationError, b as PaymentProviderError, d as PaymentVerificationError } from "../chunks/errors-BgFC46qQ.js";
|
|
3
|
+
import { g as getFetch, a as normalizeMaxStoredPaymentOptions, b as normalizeUrlString, n as normalizeNonEmptyString, o as normalizePositivePaymentAmount, h as normalizeDate, t as formEncode, r as rememberPaymentOption, p as pollPaymentStatus, i as normalizeMinorUnitAmount, j as readJsonResponse, s as applyExpiryToPendingStatus } from "../chunks/shared-DGHSqDQT.js";
|
|
4
|
+
const STRIPE_BACKEND_ID = "stripe";
|
|
5
|
+
const STRIPE_CHECKOUT_MIN_EXPIRY_MS = 30 * 60 * 1e3;
|
|
6
|
+
const STRIPE_CHECKOUT_MAX_EXPIRY_MS = 24 * 60 * 60 * 1e3;
|
|
7
|
+
const DEFAULT_STRIPE_API_VERSION = "2024-06-20";
|
|
8
|
+
const DEFAULT_MAX_STORED_WEBHOOK_EVENT_IDS = 5e4;
|
|
9
|
+
class StripeAdapter {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
if (typeof options.secretKey !== "string" || !options.secretKey.trim()) {
|
|
13
|
+
throw new PaymentConfigurationError("StripeAdapter requires secretKey.");
|
|
14
|
+
}
|
|
15
|
+
if (options.webhookSecret !== void 0) {
|
|
16
|
+
if (typeof options.webhookSecret !== "string") {
|
|
17
|
+
throw new PaymentConfigurationError(
|
|
18
|
+
"StripeAdapter webhookSecret must be a string when configured."
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (!options.webhookSecret.trim()) {
|
|
22
|
+
throw new PaymentConfigurationError(
|
|
23
|
+
"StripeAdapter webhookSecret must not be empty when configured."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (options.apiBaseUrl !== void 0) {
|
|
28
|
+
if (typeof options.apiBaseUrl !== "string") {
|
|
29
|
+
throw new PaymentConfigurationError(
|
|
30
|
+
"StripeAdapter apiBaseUrl must be a string when configured."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (!options.apiBaseUrl.trim()) {
|
|
34
|
+
throw new PaymentConfigurationError(
|
|
35
|
+
"StripeAdapter apiBaseUrl must not be empty when configured."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.fetch = getFetch(options.fetch);
|
|
40
|
+
this.maxStoredPaymentOptions = normalizeMaxStoredPaymentOptions(
|
|
41
|
+
options.maxStoredPaymentOptions,
|
|
42
|
+
"StripeAdapter maxStoredPaymentOptions"
|
|
43
|
+
);
|
|
44
|
+
this.maxStoredWebhookEventIds = normalizeMaxStoredPaymentOptions(
|
|
45
|
+
options.maxStoredWebhookEventIds ?? DEFAULT_MAX_STORED_WEBHOOK_EVENT_IDS,
|
|
46
|
+
"StripeAdapter maxStoredWebhookEventIds"
|
|
47
|
+
);
|
|
48
|
+
this.secretKey = options.secretKey.trim();
|
|
49
|
+
this.apiBaseUrl = normalizeUrlString(
|
|
50
|
+
options.apiBaseUrl ?? "https://api.stripe.com/v1",
|
|
51
|
+
"StripeAdapter apiBaseUrl"
|
|
52
|
+
).replace(/\/$/, "");
|
|
53
|
+
this.apiVersion = normalizeNonEmptyString(
|
|
54
|
+
options.apiVersion ?? DEFAULT_STRIPE_API_VERSION,
|
|
55
|
+
"StripeAdapter apiVersion"
|
|
56
|
+
);
|
|
57
|
+
this.defaultCurrency = normalizeStripeCurrency(
|
|
58
|
+
options.defaultCurrency ?? "usd"
|
|
59
|
+
);
|
|
60
|
+
this.supportedCurrencies = normalizeSupportedStripeCurrencies(
|
|
61
|
+
options.supportedCurrencies,
|
|
62
|
+
this.defaultCurrency
|
|
63
|
+
);
|
|
64
|
+
this.checkoutMode = options.checkoutMode ?? "payment";
|
|
65
|
+
if (this.checkoutMode !== "payment") {
|
|
66
|
+
throw new PaymentConfigurationError(
|
|
67
|
+
"StripeAdapter only supports payment Checkout mode."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
this.webhookSecret = options.webhookSecret?.trim();
|
|
71
|
+
this.capabilities = {
|
|
72
|
+
id: STRIPE_BACKEND_ID,
|
|
73
|
+
displayName: "Stripe Checkout",
|
|
74
|
+
settlementCurrency: this.defaultCurrency.toUpperCase(),
|
|
75
|
+
supportedSettlementCurrencies: [...this.supportedCurrencies].map(
|
|
76
|
+
(currency) => currency.toUpperCase()
|
|
77
|
+
),
|
|
78
|
+
chain: "stripe",
|
|
79
|
+
settlementShape: "url",
|
|
80
|
+
x402Capable: false,
|
|
81
|
+
confirmationLatency: {
|
|
82
|
+
expectedSeconds: 5,
|
|
83
|
+
maxExpectedSeconds: 600,
|
|
84
|
+
description: "Card payments usually confirm in seconds; bank debits can remain processing while Stripe settles them."
|
|
85
|
+
},
|
|
86
|
+
supportsRefunds: true,
|
|
87
|
+
supportsPayouts: true,
|
|
88
|
+
supportsWebhooks: true,
|
|
89
|
+
metadata: {
|
|
90
|
+
provider: "stripe"
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
capabilities;
|
|
95
|
+
fetch;
|
|
96
|
+
secretKey;
|
|
97
|
+
apiBaseUrl;
|
|
98
|
+
apiVersion;
|
|
99
|
+
defaultCurrency;
|
|
100
|
+
supportedCurrencies;
|
|
101
|
+
checkoutMode;
|
|
102
|
+
webhookSecret;
|
|
103
|
+
maxStoredPaymentOptions;
|
|
104
|
+
maxStoredWebhookEventIds;
|
|
105
|
+
optionsByQuote = /* @__PURE__ */ new Map();
|
|
106
|
+
seenWebhookEventIds = /* @__PURE__ */ new Set();
|
|
107
|
+
async createPaymentOption(input) {
|
|
108
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "Stripe quoteId");
|
|
109
|
+
const rawSuccessUrl = input.successUrl ?? this.options.successUrl;
|
|
110
|
+
const rawCancelUrl = input.cancelUrl ?? this.options.cancelUrl;
|
|
111
|
+
if (rawSuccessUrl === void 0 || rawCancelUrl === void 0) {
|
|
112
|
+
throw new PaymentConfigurationError(
|
|
113
|
+
"StripeAdapter createPaymentOption requires successUrl and cancelUrl."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const successUrl = normalizeUrlString(rawSuccessUrl, "Stripe successUrl");
|
|
117
|
+
const cancelUrl = normalizeUrlString(rawCancelUrl, "Stripe cancelUrl");
|
|
118
|
+
const currency = normalizeStripeCurrency(input.currency);
|
|
119
|
+
this.assertSupportedCurrency(currency);
|
|
120
|
+
const amount = normalizePositivePaymentAmount(
|
|
121
|
+
input.amount,
|
|
122
|
+
currency,
|
|
123
|
+
"Stripe Checkout amount"
|
|
124
|
+
);
|
|
125
|
+
const expiresAt = normalizeDate(input.expiresAt);
|
|
126
|
+
const stripeExpiresAt = normalizeStripeCheckoutExpiresAt(expiresAt);
|
|
127
|
+
const params = {
|
|
128
|
+
mode: this.checkoutMode,
|
|
129
|
+
...Object.fromEntries([
|
|
130
|
+
["success_url", successUrl],
|
|
131
|
+
["cancel_url", cancelUrl],
|
|
132
|
+
["client_reference_id", quoteId],
|
|
133
|
+
["customer_email", input.buyerEmail],
|
|
134
|
+
["expires_at", Math.floor(stripeExpiresAt.getTime() / 1e3)]
|
|
135
|
+
]),
|
|
136
|
+
"line_items[0][quantity]": 1,
|
|
137
|
+
"line_items[0][price_data][currency]": currency,
|
|
138
|
+
"line_items[0][price_data][unit_amount]": amount,
|
|
139
|
+
"line_items[0][price_data][product_data][name]": input.description ?? `Quote ${quoteId}`,
|
|
140
|
+
...flattenStripeMetadata(input.metadata),
|
|
141
|
+
"metadata[quoteId]": quoteId
|
|
142
|
+
};
|
|
143
|
+
const session = await this.stripeRequest(
|
|
144
|
+
"/checkout/sessions",
|
|
145
|
+
{
|
|
146
|
+
method: "POST",
|
|
147
|
+
idempotencyKey: input.idempotencyKey,
|
|
148
|
+
body: formEncode(params)
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
const sessionId = normalizeOptionalProviderString(
|
|
152
|
+
readProviderString(session, "id")
|
|
153
|
+
);
|
|
154
|
+
const checkoutUrl = normalizeOptionalProviderString(
|
|
155
|
+
readProviderString(session, "url")
|
|
156
|
+
);
|
|
157
|
+
if (!sessionId || !checkoutUrl) {
|
|
158
|
+
throw new PaymentProviderError(
|
|
159
|
+
"Stripe Checkout Session response did not include id and url."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const option = {
|
|
163
|
+
backendId: this.capabilities.id,
|
|
164
|
+
quoteId,
|
|
165
|
+
payTo: checkoutUrl,
|
|
166
|
+
settlementShape: "url",
|
|
167
|
+
settlementCurrency: currency.toUpperCase(),
|
|
168
|
+
settlementAmount: amount,
|
|
169
|
+
amount,
|
|
170
|
+
currency: currency.toUpperCase(),
|
|
171
|
+
expiresAt: stripeExpiresAt,
|
|
172
|
+
providerPaymentId: sessionId,
|
|
173
|
+
paymentUri: checkoutUrl,
|
|
174
|
+
metadata: {
|
|
175
|
+
sessionId,
|
|
176
|
+
paymentIntent: normalizeOptionalProviderString(
|
|
177
|
+
readProviderString(session, "payment_intent")
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
rememberPaymentOption(
|
|
182
|
+
this.optionsByQuote,
|
|
183
|
+
quoteId,
|
|
184
|
+
option,
|
|
185
|
+
this.maxStoredPaymentOptions
|
|
186
|
+
);
|
|
187
|
+
return option;
|
|
188
|
+
}
|
|
189
|
+
watchPayment(input) {
|
|
190
|
+
return pollPaymentStatus(
|
|
191
|
+
{
|
|
192
|
+
...input,
|
|
193
|
+
pollIntervalMs: input.pollIntervalMs ?? this.options.pollIntervalMs ?? 5e3
|
|
194
|
+
},
|
|
195
|
+
() => this.getStatus(input.quoteId, input.payTo, input.statusContext)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
async getStatus(quoteId, payTo, context = {}) {
|
|
199
|
+
const normalizedQuoteId = normalizeNonEmptyString(
|
|
200
|
+
quoteId,
|
|
201
|
+
"Stripe quoteId"
|
|
202
|
+
);
|
|
203
|
+
const normalizedPayTo = normalizeNonEmptyString(payTo, "Stripe payTo");
|
|
204
|
+
const contextProviderPaymentId = context.providerPaymentId === void 0 ? void 0 : normalizeNonEmptyString(
|
|
205
|
+
context.providerPaymentId,
|
|
206
|
+
"Stripe providerPaymentId"
|
|
207
|
+
);
|
|
208
|
+
const sessionId = this.optionsByQuote.get(normalizedQuoteId)?.providerPaymentId ?? contextProviderPaymentId ?? extractStripeSessionId(normalizedPayTo);
|
|
209
|
+
if (!sessionId) {
|
|
210
|
+
throw new PaymentConfigurationError(
|
|
211
|
+
"Stripe getStatus requires a Checkout Session id or URL."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const session = await this.stripeRequest(
|
|
215
|
+
`/checkout/sessions/${encodeURIComponent(sessionId)}`
|
|
216
|
+
);
|
|
217
|
+
const sessionQuoteId = readStripeSessionQuoteId(session);
|
|
218
|
+
if (sessionQuoteId !== void 0 && sessionQuoteId !== normalizedQuoteId) {
|
|
219
|
+
throw new PaymentVerificationError(
|
|
220
|
+
`Stripe Checkout Session quoteId ${sessionQuoteId} did not match ${normalizedQuoteId}.`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const paymentStatus = readString(session, "payment_status");
|
|
224
|
+
const status = mapStripeStatus(
|
|
225
|
+
readString(session, "status"),
|
|
226
|
+
paymentStatus
|
|
227
|
+
);
|
|
228
|
+
const option = this.optionsByQuote.get(normalizedQuoteId);
|
|
229
|
+
const contextAmount = context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "Stripe status");
|
|
230
|
+
const contextCurrency = context.currency === void 0 ? void 0 : normalizeStripeCurrency(context.currency).toUpperCase();
|
|
231
|
+
const sessionCurrency = readString(session, "currency") === void 0 ? void 0 : normalizeStripeCurrency(
|
|
232
|
+
readString(session, "currency") ?? ""
|
|
233
|
+
).toUpperCase();
|
|
234
|
+
const sessionAmount = readSafeInteger(session, "amount_total");
|
|
235
|
+
const expiresAt = option?.expiresAt ?? (context.expiresAt === void 0 ? void 0 : normalizeDate(context.expiresAt));
|
|
236
|
+
const effectiveStatus = applyExpiryToPendingStatus(status, expiresAt);
|
|
237
|
+
return {
|
|
238
|
+
backendId: this.capabilities.id,
|
|
239
|
+
quoteId: normalizedQuoteId,
|
|
240
|
+
payTo: normalizedPayTo,
|
|
241
|
+
status: effectiveStatus,
|
|
242
|
+
settlementCurrency: sessionCurrency ?? option?.settlementCurrency ?? contextCurrency ?? this.defaultCurrency.toUpperCase(),
|
|
243
|
+
settlementAmount: sessionAmount ?? option?.settlementAmount ?? contextAmount,
|
|
244
|
+
receivedAmount: paymentStatus === "paid" || paymentStatus === "no_payment_required" ? sessionAmount ?? option?.settlementAmount ?? contextAmount : void 0,
|
|
245
|
+
amount: option?.amount ?? contextAmount,
|
|
246
|
+
currency: option?.currency ?? contextCurrency,
|
|
247
|
+
providerPaymentId: sessionId,
|
|
248
|
+
transactionId: normalizeOptionalProviderString(
|
|
249
|
+
readProviderString(session, "payment_intent")
|
|
250
|
+
),
|
|
251
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
252
|
+
raw: session
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async sendPayout(input) {
|
|
256
|
+
const currency = normalizeStripeCurrency(
|
|
257
|
+
input.currency ?? this.defaultCurrency
|
|
258
|
+
);
|
|
259
|
+
this.assertSupportedCurrency(currency);
|
|
260
|
+
const destination = normalizeNonEmptyString(
|
|
261
|
+
input.destination,
|
|
262
|
+
"Stripe payout destination"
|
|
263
|
+
);
|
|
264
|
+
const quoteId = input.quoteId === void 0 ? void 0 : normalizeNonEmptyString(input.quoteId, "Stripe payout quoteId");
|
|
265
|
+
const amount = normalizePositivePaymentAmount(
|
|
266
|
+
input.amount,
|
|
267
|
+
currency,
|
|
268
|
+
"Stripe payout amount"
|
|
269
|
+
);
|
|
270
|
+
const transfer = await this.stripeRequest(
|
|
271
|
+
"/transfers",
|
|
272
|
+
{
|
|
273
|
+
method: "POST",
|
|
274
|
+
idempotencyKey: input.idempotencyKey,
|
|
275
|
+
body: formEncode({
|
|
276
|
+
amount,
|
|
277
|
+
currency,
|
|
278
|
+
destination,
|
|
279
|
+
description: input.memo,
|
|
280
|
+
...flattenStripeMetadata(input.metadata),
|
|
281
|
+
...quoteId === void 0 ? {} : { "metadata[quoteId]": quoteId }
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
return {
|
|
286
|
+
backendId: this.capabilities.id,
|
|
287
|
+
status: "submitted",
|
|
288
|
+
payoutId: normalizeOptionalProviderString(
|
|
289
|
+
readProviderString(transfer, "id")
|
|
290
|
+
),
|
|
291
|
+
destination,
|
|
292
|
+
amount,
|
|
293
|
+
currency: currency.toUpperCase(),
|
|
294
|
+
raw: transfer
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async refundPayment(input) {
|
|
298
|
+
const rawPaymentReference = input.paymentId ?? input.transactionId;
|
|
299
|
+
if (!rawPaymentReference) {
|
|
300
|
+
throw new PaymentConfigurationError(
|
|
301
|
+
"Stripe refunds require paymentId or transactionId."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const paymentReference = normalizeNonEmptyString(
|
|
305
|
+
rawPaymentReference,
|
|
306
|
+
"Stripe refund payment reference"
|
|
307
|
+
);
|
|
308
|
+
const currency = normalizeStripeCurrency(
|
|
309
|
+
input.currency ?? this.defaultCurrency
|
|
310
|
+
);
|
|
311
|
+
this.assertSupportedCurrency(currency);
|
|
312
|
+
const amount = input.amount === void 0 ? void 0 : normalizePositivePaymentAmount(
|
|
313
|
+
input.amount,
|
|
314
|
+
currency,
|
|
315
|
+
"Stripe refund amount"
|
|
316
|
+
);
|
|
317
|
+
const refundReferenceParam = await this.resolveStripeRefundReferenceParam(paymentReference);
|
|
318
|
+
const refundParams = {
|
|
319
|
+
...refundReferenceParam,
|
|
320
|
+
amount: amount === void 0 ? void 0 : amount,
|
|
321
|
+
reason: input.reason,
|
|
322
|
+
...flattenStripeMetadata(input.metadata)
|
|
323
|
+
};
|
|
324
|
+
const refund = await this.stripeRequest(
|
|
325
|
+
"/refunds",
|
|
326
|
+
{
|
|
327
|
+
method: "POST",
|
|
328
|
+
idempotencyKey: input.idempotencyKey,
|
|
329
|
+
body: formEncode(refundParams)
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
return {
|
|
333
|
+
backendId: this.capabilities.id,
|
|
334
|
+
status: mapStripeRefundStatus(readString(refund, "status")),
|
|
335
|
+
refundId: normalizeOptionalProviderString(
|
|
336
|
+
readProviderString(refund, "id")
|
|
337
|
+
),
|
|
338
|
+
transactionId: paymentReference,
|
|
339
|
+
amount,
|
|
340
|
+
currency: currency.toUpperCase(),
|
|
341
|
+
raw: refund
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
parseWebhookEvent(payload, signature) {
|
|
345
|
+
if (!this.webhookSecret) {
|
|
346
|
+
throw new PaymentConfigurationError(
|
|
347
|
+
"StripeAdapter parseWebhookEvent requires webhookSecret."
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (!signature) {
|
|
351
|
+
throw new PaymentProviderError("Missing Stripe webhook signature.");
|
|
352
|
+
}
|
|
353
|
+
verifyStripeWebhookSignature(payload, signature, this.webhookSecret);
|
|
354
|
+
const event = parseStripeWebhookPayload(payload);
|
|
355
|
+
const id = readRequiredWebhookString(
|
|
356
|
+
event,
|
|
357
|
+
"id",
|
|
358
|
+
"Stripe webhook event id"
|
|
359
|
+
);
|
|
360
|
+
const type = readRequiredWebhookString(
|
|
361
|
+
event,
|
|
362
|
+
"type",
|
|
363
|
+
"Stripe webhook event type"
|
|
364
|
+
);
|
|
365
|
+
const duplicate = this.seenWebhookEventIds.has(id);
|
|
366
|
+
if (!duplicate) {
|
|
367
|
+
this.seenWebhookEventIds.add(id);
|
|
368
|
+
while (this.seenWebhookEventIds.size > this.maxStoredWebhookEventIds) {
|
|
369
|
+
const oldest = this.seenWebhookEventIds.values().next().value;
|
|
370
|
+
if (oldest === void 0) {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
this.seenWebhookEventIds.delete(oldest);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const data = event.data?.object;
|
|
377
|
+
return {
|
|
378
|
+
id,
|
|
379
|
+
type,
|
|
380
|
+
status: mapStripeWebhookStatus(type, data),
|
|
381
|
+
quoteId: normalizeOptionalWebhookString(
|
|
382
|
+
readString(data, "client_reference_id") ?? readString(
|
|
383
|
+
data?.metadata ?? {},
|
|
384
|
+
"quoteId"
|
|
385
|
+
)
|
|
386
|
+
),
|
|
387
|
+
providerPaymentId: normalizeOptionalWebhookString(readString(data, "id")),
|
|
388
|
+
duplicate,
|
|
389
|
+
raw: event
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async stripeRequest(path, init = {}) {
|
|
393
|
+
const headers = new Headers(init.headers);
|
|
394
|
+
headers.set("Authorization", `Bearer ${this.secretKey}`);
|
|
395
|
+
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
396
|
+
headers.set("Accept", "application/json");
|
|
397
|
+
headers.set("Stripe-Version", this.apiVersion);
|
|
398
|
+
if (init.idempotencyKey !== void 0) {
|
|
399
|
+
headers.set(
|
|
400
|
+
"Idempotency-Key",
|
|
401
|
+
normalizeNonEmptyString(init.idempotencyKey, "Stripe idempotencyKey")
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const { idempotencyKey: _idempotencyKey, ...requestInit } = init;
|
|
405
|
+
const response = await this.fetch(`${this.apiBaseUrl}${path}`, {
|
|
406
|
+
...requestInit,
|
|
407
|
+
method: init.method ?? "GET",
|
|
408
|
+
headers
|
|
409
|
+
});
|
|
410
|
+
return readJsonResponse(response, `Stripe ${path}`);
|
|
411
|
+
}
|
|
412
|
+
assertSupportedCurrency(currency) {
|
|
413
|
+
if (!this.supportedCurrencies.has(currency)) {
|
|
414
|
+
throw new PaymentConfigurationError(
|
|
415
|
+
`StripeAdapter currency ${currency.toUpperCase()} is not configured as supported.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async resolveStripeRefundReferenceParam(paymentReference) {
|
|
420
|
+
if (paymentReference.startsWith("cs_")) {
|
|
421
|
+
const session = await this.stripeRequest(
|
|
422
|
+
`/checkout/sessions/${encodeURIComponent(paymentReference)}`
|
|
423
|
+
);
|
|
424
|
+
const paymentIntent = normalizeOptionalProviderString(
|
|
425
|
+
readProviderString(session, "payment_intent")
|
|
426
|
+
);
|
|
427
|
+
if (!paymentIntent) {
|
|
428
|
+
throw new PaymentProviderError(
|
|
429
|
+
"Stripe Checkout Session did not include payment_intent for refund."
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return Object.fromEntries([["payment_intent", paymentIntent]]);
|
|
433
|
+
}
|
|
434
|
+
return stripeRefundReferenceParam(paymentReference);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function stripeRefundReferenceParam(paymentReference) {
|
|
438
|
+
if (paymentReference.startsWith("pi_")) {
|
|
439
|
+
return Object.fromEntries([["payment_intent", paymentReference]]);
|
|
440
|
+
}
|
|
441
|
+
if (paymentReference.startsWith("ch_")) {
|
|
442
|
+
return { charge: paymentReference };
|
|
443
|
+
}
|
|
444
|
+
throw new PaymentConfigurationError(
|
|
445
|
+
"Stripe refunds require a Checkout Session (cs_), payment_intent (pi_), or charge (ch_) reference."
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
function normalizeStripeCurrency(value) {
|
|
449
|
+
if (typeof value !== "string") {
|
|
450
|
+
throw new PaymentConfigurationError(
|
|
451
|
+
`StripeAdapter currency must be a three-letter ISO currency code, received ${String(value)}.`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const normalized = value.trim().toLowerCase();
|
|
455
|
+
if (!/^[a-z]{3}$/.test(normalized)) {
|
|
456
|
+
throw new PaymentConfigurationError(
|
|
457
|
+
`StripeAdapter currency must be a three-letter ISO currency code, received ${String(value)}.`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return normalized;
|
|
461
|
+
}
|
|
462
|
+
function normalizeSupportedStripeCurrencies(configuredCurrencies, defaultCurrency) {
|
|
463
|
+
const normalized = new Set(
|
|
464
|
+
(configuredCurrencies ?? ["usd", "eur", "gbp", "cad"]).map(
|
|
465
|
+
normalizeStripeCurrency
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
normalized.add(defaultCurrency);
|
|
469
|
+
return normalized;
|
|
470
|
+
}
|
|
471
|
+
function normalizeStripeCheckoutExpiresAt(expiresAt, now = Date.now()) {
|
|
472
|
+
if (expiresAt.getTime() <= now) {
|
|
473
|
+
throw new PaymentProviderError("Stripe Checkout expiry is in the past.");
|
|
474
|
+
}
|
|
475
|
+
const min = now + STRIPE_CHECKOUT_MIN_EXPIRY_MS;
|
|
476
|
+
const max = now + STRIPE_CHECKOUT_MAX_EXPIRY_MS;
|
|
477
|
+
if (expiresAt.getTime() < min || expiresAt.getTime() > max) {
|
|
478
|
+
throw new PaymentConfigurationError(
|
|
479
|
+
"Stripe Checkout expiry must be at least 30 minutes and no more than 24 hours from now."
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return expiresAt;
|
|
483
|
+
}
|
|
484
|
+
function verifyStripeWebhookSignature(payload, signatureHeader, secret, toleranceSeconds = 300) {
|
|
485
|
+
if (typeof payload !== "string") {
|
|
486
|
+
throw new PaymentProviderError("Stripe webhook payload must be a string.");
|
|
487
|
+
}
|
|
488
|
+
const normalizedSignatureHeader = normalizeNonEmptyString(
|
|
489
|
+
signatureHeader,
|
|
490
|
+
"Stripe signature header"
|
|
491
|
+
);
|
|
492
|
+
const normalizedSecret = normalizeNonEmptyString(
|
|
493
|
+
secret,
|
|
494
|
+
"Stripe webhook secret"
|
|
495
|
+
);
|
|
496
|
+
if (!Number.isFinite(toleranceSeconds) || toleranceSeconds < 0) {
|
|
497
|
+
throw new PaymentConfigurationError(
|
|
498
|
+
`Stripe webhook toleranceSeconds must be a non-negative finite number, received ${String(toleranceSeconds)}.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const timestampCandidates = [];
|
|
502
|
+
const signatures = [];
|
|
503
|
+
for (const part of normalizedSignatureHeader.split(",")) {
|
|
504
|
+
const [rawKey, ...rawValue] = part.split("=");
|
|
505
|
+
const key = rawKey?.trim();
|
|
506
|
+
const value = rawValue.join("=").trim();
|
|
507
|
+
if (!key || !value) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (key === "t") {
|
|
511
|
+
timestampCandidates.push(value);
|
|
512
|
+
}
|
|
513
|
+
if (key === "v1") {
|
|
514
|
+
signatures.push(value);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (timestampCandidates.length !== 1 || signatures.length === 0) {
|
|
518
|
+
throw new PaymentProviderError("Invalid Stripe signature header.");
|
|
519
|
+
}
|
|
520
|
+
const timestamp = timestampCandidates[0] ?? "";
|
|
521
|
+
if (!/^\d+$/.test(timestamp)) {
|
|
522
|
+
throw new PaymentProviderError("Invalid Stripe signature header.");
|
|
523
|
+
}
|
|
524
|
+
const timestampSeconds = Number(timestamp);
|
|
525
|
+
if (!Number.isSafeInteger(timestampSeconds)) {
|
|
526
|
+
throw new PaymentProviderError("Invalid Stripe signature header.");
|
|
527
|
+
}
|
|
528
|
+
const nowSeconds = Date.now() / 1e3;
|
|
529
|
+
const ageSeconds = nowSeconds - timestampSeconds;
|
|
530
|
+
if (ageSeconds < -5) {
|
|
531
|
+
throw new PaymentProviderError(
|
|
532
|
+
"Stripe webhook signature timestamp is in the future."
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (ageSeconds > toleranceSeconds) {
|
|
536
|
+
throw new PaymentProviderError("Stripe webhook signature is too old.");
|
|
537
|
+
}
|
|
538
|
+
const expected = createHmac("sha256", normalizedSecret).update(`${timestamp}.${payload}`).digest("hex");
|
|
539
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
540
|
+
for (const signature of signatures) {
|
|
541
|
+
if (!/^[0-9a-f]+$/i.test(signature)) {
|
|
542
|
+
throw new PaymentProviderError("Invalid Stripe signature header.");
|
|
543
|
+
}
|
|
544
|
+
const actualBuffer = Buffer.from(signature, "hex");
|
|
545
|
+
if (expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
throw new PaymentProviderError("Invalid Stripe webhook signature.");
|
|
550
|
+
}
|
|
551
|
+
function parseStripeWebhookPayload(payload) {
|
|
552
|
+
try {
|
|
553
|
+
const event = JSON.parse(payload);
|
|
554
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) {
|
|
555
|
+
throw new PaymentProviderError("Invalid Stripe webhook JSON payload.");
|
|
556
|
+
}
|
|
557
|
+
return event;
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof PaymentProviderError) {
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
throw new PaymentProviderError("Invalid Stripe webhook JSON payload.", {
|
|
563
|
+
cause: error
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function normalizeOptionalWebhookString(value) {
|
|
568
|
+
const normalized = value?.trim();
|
|
569
|
+
return normalized ? normalized : void 0;
|
|
570
|
+
}
|
|
571
|
+
function normalizeOptionalProviderString(value) {
|
|
572
|
+
const normalized = value?.trim();
|
|
573
|
+
return normalized ? normalized : void 0;
|
|
574
|
+
}
|
|
575
|
+
function readProviderString(value, key) {
|
|
576
|
+
if (!value) {
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
const item = value[key];
|
|
580
|
+
return typeof item === "string" ? item : void 0;
|
|
581
|
+
}
|
|
582
|
+
function readRequiredWebhookString(value, key, context) {
|
|
583
|
+
const item = value[key];
|
|
584
|
+
if (typeof item !== "string") {
|
|
585
|
+
throw new PaymentProviderError(`${context} must be a string.`);
|
|
586
|
+
}
|
|
587
|
+
return normalizeNonEmptyString(item, context);
|
|
588
|
+
}
|
|
589
|
+
function readStripeSessionQuoteId(session) {
|
|
590
|
+
return normalizeOptionalWebhookString(
|
|
591
|
+
readString(session, "client_reference_id") ?? readString(
|
|
592
|
+
session.metadata ?? {},
|
|
593
|
+
"quoteId"
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
function mapStripeStatus(checkoutStatus, paymentStatus) {
|
|
598
|
+
if (paymentStatus === "paid" || paymentStatus === "no_payment_required") {
|
|
599
|
+
return "confirmed";
|
|
600
|
+
}
|
|
601
|
+
if (checkoutStatus === "expired") {
|
|
602
|
+
return "expired";
|
|
603
|
+
}
|
|
604
|
+
if (paymentStatus === "unpaid" || checkoutStatus === "open") {
|
|
605
|
+
return "pending";
|
|
606
|
+
}
|
|
607
|
+
if (checkoutStatus === "complete") {
|
|
608
|
+
return "processing";
|
|
609
|
+
}
|
|
610
|
+
return "pending";
|
|
611
|
+
}
|
|
612
|
+
function mapStripeWebhookStatus(type, object) {
|
|
613
|
+
if (type === "checkout.session.completed") {
|
|
614
|
+
return mapStripeStatus(
|
|
615
|
+
readString(object, "status"),
|
|
616
|
+
readString(object, "payment_status")
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
if (type === "checkout.session.async_payment_succeeded") {
|
|
620
|
+
return "confirmed";
|
|
621
|
+
}
|
|
622
|
+
if (type === "checkout.session.async_payment_failed") {
|
|
623
|
+
return "failed";
|
|
624
|
+
}
|
|
625
|
+
if (type === "checkout.session.expired") {
|
|
626
|
+
return "expired";
|
|
627
|
+
}
|
|
628
|
+
if (type?.includes("failed")) {
|
|
629
|
+
return "failed";
|
|
630
|
+
}
|
|
631
|
+
if (type?.includes("refunded")) {
|
|
632
|
+
return "refunded";
|
|
633
|
+
}
|
|
634
|
+
return "processing";
|
|
635
|
+
}
|
|
636
|
+
function mapStripeRefundStatus(status) {
|
|
637
|
+
switch (status) {
|
|
638
|
+
case "succeeded":
|
|
639
|
+
return "succeeded";
|
|
640
|
+
case "failed":
|
|
641
|
+
case "canceled":
|
|
642
|
+
return "failed";
|
|
643
|
+
case "requires_action":
|
|
644
|
+
return "requires_action";
|
|
645
|
+
default:
|
|
646
|
+
return "submitted";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function extractStripeSessionId(payTo) {
|
|
650
|
+
if (/^cs_(test_|live_)?[A-Za-z0-9_]+$/.test(payTo)) {
|
|
651
|
+
return payTo;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const url = new URL(payTo);
|
|
655
|
+
const match = url.pathname.match(/\/pay\/([^/?#]+)/);
|
|
656
|
+
const sessionId = match?.[1];
|
|
657
|
+
return sessionId && /^cs_(test_|live_)?[A-Za-z0-9_]+$/.test(sessionId) ? sessionId : void 0;
|
|
658
|
+
} catch {
|
|
659
|
+
return void 0;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function flattenStripeMetadata(metadata) {
|
|
663
|
+
const result = {};
|
|
664
|
+
for (const [key, value] of Object.entries(metadata ?? {})) {
|
|
665
|
+
if (value !== void 0 && value !== null) {
|
|
666
|
+
result[`metadata[${key}]`] = value;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
function readString(value, key) {
|
|
672
|
+
if (!value) {
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
const item = value[key];
|
|
676
|
+
if (typeof item === "string") {
|
|
677
|
+
return item;
|
|
678
|
+
}
|
|
679
|
+
if (typeof item === "number") {
|
|
680
|
+
return String(item);
|
|
681
|
+
}
|
|
682
|
+
return void 0;
|
|
683
|
+
}
|
|
684
|
+
function readSafeInteger(value, key) {
|
|
685
|
+
if (!value) {
|
|
686
|
+
return void 0;
|
|
687
|
+
}
|
|
688
|
+
const item = value[key];
|
|
689
|
+
return typeof item === "number" && Number.isSafeInteger(item) && item >= 0 ? item : void 0;
|
|
690
|
+
}
|
|
691
|
+
export {
|
|
692
|
+
STRIPE_BACKEND_ID,
|
|
693
|
+
StripeAdapter,
|
|
694
|
+
verifyStripeWebhookSignature
|
|
695
|
+
};
|
|
696
|
+
//# sourceMappingURL=stripe.js.map
|