@happyvertical/payments 0.105.3 → 0.105.5
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/README.md +145 -1
- package/dist/adapters/base-usdc.js +2 -2
- package/dist/adapters/btc.js +2 -2
- package/dist/adapters/btcpay.js +3 -3
- package/dist/adapters/stripe.d.ts +202 -9
- package/dist/adapters/stripe.d.ts.map +1 -1
- package/dist/adapters/stripe.js +278 -27
- package/dist/adapters/stripe.js.map +1 -1
- package/dist/chunks/{checkout-gateway-DG-LY0oG.js → checkout-gateway-Bb8UEwK8.js} +2 -2
- package/dist/chunks/{checkout-gateway-DG-LY0oG.js.map → checkout-gateway-Bb8UEwK8.js.map} +1 -1
- package/dist/chunks/{errors-feZrwnTy.js → errors-zSmk_AYY.js} +13 -2
- package/dist/chunks/{errors-feZrwnTy.js.map → errors-zSmk_AYY.js.map} +1 -1
- package/dist/chunks/{shared-DVl6UzD9.js → shared-oBgOhLsa.js} +2 -2
- package/dist/chunks/{shared-DVl6UzD9.js.map → shared-oBgOhLsa.js.map} +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/package.json +1 -1
package/dist/adapters/stripe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as normalizeUrlString, a as formEncode, b as rememberPaymentOption, f as normalizeMaxStoredPaymentOptions, g as normalizePositivePaymentAmount, h as normalizePositiveMinorUnitAmount, m as normalizeNonEmptyString, o as getFetch, p as normalizeMinorUnitAmount, t as applyExpiryToPendingStatus, u as normalizeDate, v as pollPaymentStatus, y as readJsonResponse } from "../chunks/shared-
|
|
1
|
+
import { i as PaymentProviderError, o as PaymentVerificationError, r as PaymentNotOwnedError, t as PaymentConfigurationError } from "../chunks/errors-zSmk_AYY.js";
|
|
2
|
+
import { _ as normalizeUrlString, a as formEncode, b as rememberPaymentOption, f as normalizeMaxStoredPaymentOptions, g as normalizePositivePaymentAmount, h as normalizePositiveMinorUnitAmount, m as normalizeNonEmptyString, o as getFetch, p as normalizeMinorUnitAmount, t as applyExpiryToPendingStatus, u as normalizeDate, v as pollPaymentStatus, y as readJsonResponse } from "../chunks/shared-oBgOhLsa.js";
|
|
3
3
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
4
|
//#region src/adapters/stripe.ts
|
|
5
5
|
var STRIPE_BACKEND_ID = "stripe";
|
|
@@ -18,6 +18,8 @@ var StripeAdapter = class {
|
|
|
18
18
|
supportedCurrencies;
|
|
19
19
|
checkoutMode;
|
|
20
20
|
webhookSecret;
|
|
21
|
+
connectWebhookSecret;
|
|
22
|
+
stripeAccount;
|
|
21
23
|
maxStoredPaymentOptions;
|
|
22
24
|
maxStoredWebhookEventIds;
|
|
23
25
|
optionsByQuote = /* @__PURE__ */ new Map();
|
|
@@ -25,10 +27,8 @@ var StripeAdapter = class {
|
|
|
25
27
|
constructor(options) {
|
|
26
28
|
this.options = options;
|
|
27
29
|
if (typeof options.secretKey !== "string" || !options.secretKey.trim()) throw new PaymentConfigurationError("StripeAdapter requires secretKey.");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!options.webhookSecret.trim()) throw new PaymentConfigurationError("StripeAdapter webhookSecret must not be empty when configured.");
|
|
31
|
-
}
|
|
30
|
+
assertOptionalSecret(options.webhookSecret, "webhookSecret");
|
|
31
|
+
assertOptionalSecret(options.connectWebhookSecret, "connectWebhookSecret");
|
|
32
32
|
if (options.apiBaseUrl !== void 0) {
|
|
33
33
|
if (typeof options.apiBaseUrl !== "string") throw new PaymentConfigurationError("StripeAdapter apiBaseUrl must be a string when configured.");
|
|
34
34
|
if (!options.apiBaseUrl.trim()) throw new PaymentConfigurationError("StripeAdapter apiBaseUrl must not be empty when configured.");
|
|
@@ -44,6 +44,8 @@ var StripeAdapter = class {
|
|
|
44
44
|
this.checkoutMode = options.checkoutMode ?? "payment";
|
|
45
45
|
if (this.checkoutMode !== "payment") throw new PaymentConfigurationError("StripeAdapter only supports payment Checkout mode.");
|
|
46
46
|
this.webhookSecret = options.webhookSecret?.trim();
|
|
47
|
+
this.connectWebhookSecret = options.connectWebhookSecret?.trim();
|
|
48
|
+
this.stripeAccount = normalizeOptionalStripeAccount(options.stripeAccount, "StripeAdapter stripeAccount");
|
|
47
49
|
this.capabilities = {
|
|
48
50
|
id: STRIPE_BACKEND_ID,
|
|
49
51
|
displayName: "Stripe Checkout",
|
|
@@ -75,6 +77,7 @@ var StripeAdapter = class {
|
|
|
75
77
|
const currency = normalizeStripeCurrency(input.currency);
|
|
76
78
|
this.assertSupportedCurrency(currency);
|
|
77
79
|
const amount = normalizePositivePaymentAmount(input.amount, currency, "Stripe Checkout amount");
|
|
80
|
+
const applicationFeeAmount = normalizeApplicationFeeAmount(input.applicationFeeAmount, amount, "Stripe Checkout applicationFeeAmount");
|
|
78
81
|
const stripeExpiresAt = normalizeStripeCheckoutExpiresAt(normalizeDate(input.expiresAt));
|
|
79
82
|
const params = {
|
|
80
83
|
mode: this.checkoutMode,
|
|
@@ -89,12 +92,15 @@ var StripeAdapter = class {
|
|
|
89
92
|
"line_items[0][price_data][currency]": currency,
|
|
90
93
|
"line_items[0][price_data][unit_amount]": amount,
|
|
91
94
|
"line_items[0][price_data][product_data][name]": input.description ?? `Quote ${quoteId}`,
|
|
95
|
+
...applicationFeeAmount === void 0 ? {} : { "payment_intent_data[application_fee_amount]": applicationFeeAmount },
|
|
92
96
|
...flattenStripeMetadata(input.metadata),
|
|
93
97
|
"metadata[quoteId]": quoteId
|
|
94
98
|
};
|
|
99
|
+
const stripeAccount = this.resolveStripeAccount(input.stripeAccount);
|
|
95
100
|
const session = await this.stripeRequest("/checkout/sessions", {
|
|
96
101
|
method: "POST",
|
|
97
102
|
idempotencyKey: input.idempotencyKey,
|
|
103
|
+
stripeAccount,
|
|
98
104
|
body: formEncode(params)
|
|
99
105
|
});
|
|
100
106
|
const sessionId = normalizeOptionalProviderString(readProviderString(session, "id"));
|
|
@@ -114,10 +120,11 @@ var StripeAdapter = class {
|
|
|
114
120
|
paymentUri: checkoutUrl,
|
|
115
121
|
metadata: {
|
|
116
122
|
sessionId,
|
|
123
|
+
stripeAccount,
|
|
117
124
|
paymentIntent: normalizeOptionalProviderString(readProviderString(session, "payment_intent"))
|
|
118
125
|
}
|
|
119
126
|
};
|
|
120
|
-
rememberPaymentOption(this.optionsByQuote, quoteId, option, this.maxStoredPaymentOptions);
|
|
127
|
+
rememberPaymentOption(this.optionsByQuote, stripePaymentOptionKey(quoteId, stripeAccount), option, this.maxStoredPaymentOptions);
|
|
121
128
|
return option;
|
|
122
129
|
}
|
|
123
130
|
watchPayment(input) {
|
|
@@ -130,14 +137,19 @@ var StripeAdapter = class {
|
|
|
130
137
|
const normalizedQuoteId = normalizeNonEmptyString(quoteId, "Stripe quoteId");
|
|
131
138
|
const normalizedPayTo = normalizeNonEmptyString(payTo, "Stripe payTo");
|
|
132
139
|
const contextProviderPaymentId = context.providerPaymentId === void 0 ? void 0 : normalizeNonEmptyString(context.providerPaymentId, "Stripe providerPaymentId");
|
|
133
|
-
const
|
|
140
|
+
const contextStripeAccount = normalizeOptionalStripeAccount(context.stripeAccount, "Stripe stripeAccount");
|
|
141
|
+
const scopedOption = this.optionsByQuote.get(stripePaymentOptionKey(normalizedQuoteId, contextStripeAccount ?? this.stripeAccount));
|
|
142
|
+
const sessionId = contextProviderPaymentId ?? extractStripeSessionId(normalizedPayTo) ?? scopedOption?.providerPaymentId;
|
|
134
143
|
if (!sessionId) throw new PaymentConfigurationError("Stripe getStatus requires a Checkout Session id or URL.");
|
|
135
|
-
const
|
|
144
|
+
const storedOption = this.findStoredOptionBySessionId(sessionId);
|
|
145
|
+
const stripeAccount = contextStripeAccount ?? readStripeOptionAccount(storedOption) ?? this.stripeAccount;
|
|
146
|
+
const cachedOption = this.optionsByQuote.get(stripePaymentOptionKey(normalizedQuoteId, stripeAccount));
|
|
147
|
+
const option = cachedOption?.providerPaymentId === sessionId ? cachedOption : void 0;
|
|
148
|
+
const session = await this.stripeRequest(`/checkout/sessions/${encodeURIComponent(sessionId)}`, { stripeAccount });
|
|
136
149
|
const sessionQuoteId = readStripeSessionQuoteId(session);
|
|
137
150
|
if (sessionQuoteId !== void 0 && sessionQuoteId !== normalizedQuoteId) throw new PaymentVerificationError(`Stripe Checkout Session quoteId ${sessionQuoteId} did not match ${normalizedQuoteId}.`);
|
|
138
151
|
const paymentStatus = readString(session, "payment_status");
|
|
139
152
|
const status = mapStripeStatus(readString(session, "status"), paymentStatus);
|
|
140
|
-
const option = this.optionsByQuote.get(normalizedQuoteId);
|
|
141
153
|
const contextAmount = context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "Stripe status");
|
|
142
154
|
const contextCurrency = context.currency === void 0 ? void 0 : normalizeStripeCurrency(context.currency).toUpperCase();
|
|
143
155
|
const sessionCurrency = readString(session, "currency") === void 0 ? void 0 : normalizeStripeCurrency(readString(session, "currency") ?? "").toUpperCase();
|
|
@@ -194,15 +206,19 @@ var StripeAdapter = class {
|
|
|
194
206
|
const currency = normalizeStripeCurrency(input.currency ?? this.defaultCurrency);
|
|
195
207
|
this.assertSupportedCurrency(currency);
|
|
196
208
|
const amount = input.amount === void 0 ? void 0 : normalizePositivePaymentAmount(input.amount, currency, "Stripe refund amount");
|
|
209
|
+
const stripeAccount = this.resolveStripeAccount(input.stripeAccount);
|
|
197
210
|
const refundParams = {
|
|
198
|
-
...await this.resolveStripeRefundReferenceParam(paymentReference),
|
|
211
|
+
...await this.resolveStripeRefundReferenceParam(paymentReference, stripeAccount),
|
|
199
212
|
amount: amount === void 0 ? void 0 : amount,
|
|
200
213
|
reason: input.reason,
|
|
214
|
+
refund_application_fee: input.refundApplicationFee,
|
|
215
|
+
reverse_transfer: input.reverseTransfer,
|
|
201
216
|
...flattenStripeMetadata(input.metadata)
|
|
202
217
|
};
|
|
203
218
|
const refund = await this.stripeRequest("/refunds", {
|
|
204
219
|
method: "POST",
|
|
205
220
|
idempotencyKey: input.idempotencyKey,
|
|
221
|
+
stripeAccount,
|
|
206
222
|
body: formEncode(refundParams)
|
|
207
223
|
});
|
|
208
224
|
return {
|
|
@@ -222,15 +238,18 @@ var StripeAdapter = class {
|
|
|
222
238
|
const paymentMethod = normalizeNonEmptyString(input.providerPaymentMethodId, "Stripe authorize providerPaymentMethodId");
|
|
223
239
|
const providerCustomerId = input.providerCustomerId === void 0 ? void 0 : normalizeNonEmptyString(input.providerCustomerId, "Stripe authorize providerCustomerId");
|
|
224
240
|
const quoteId = input.quoteId === void 0 ? void 0 : normalizeNonEmptyString(input.quoteId, "Stripe authorize quoteId");
|
|
241
|
+
const applicationFeeAmount = normalizeApplicationFeeAmount(input.applicationFeeAmount, amount, "Stripe PaymentIntent applicationFeeAmount");
|
|
225
242
|
const intent = await this.stripeRequest("/payment_intents", {
|
|
226
243
|
method: "POST",
|
|
227
244
|
idempotencyKey: input.idempotencyKey,
|
|
245
|
+
stripeAccount: this.resolveStripeAccount(input.stripeAccount),
|
|
228
246
|
body: formEncode({
|
|
229
247
|
amount,
|
|
230
248
|
currency,
|
|
231
249
|
confirm: true,
|
|
232
250
|
"payment_method_types[]": "card",
|
|
233
251
|
description: input.description,
|
|
252
|
+
application_fee_amount: applicationFeeAmount,
|
|
234
253
|
...Object.fromEntries([
|
|
235
254
|
["payment_method", paymentMethod],
|
|
236
255
|
["capture_method", "manual"],
|
|
@@ -262,6 +281,7 @@ var StripeAdapter = class {
|
|
|
262
281
|
const intent = await this.stripeRequest(`/payment_intents/${encodeURIComponent(providerPaymentId)}/capture`, {
|
|
263
282
|
method: "POST",
|
|
264
283
|
idempotencyKey: input.idempotencyKey,
|
|
284
|
+
stripeAccount: this.resolveStripeAccount(input.stripeAccount),
|
|
265
285
|
body: formEncode({
|
|
266
286
|
...Object.fromEntries([["amount_to_capture", amount]]),
|
|
267
287
|
...flattenStripeMetadata(input.metadata)
|
|
@@ -282,6 +302,7 @@ var StripeAdapter = class {
|
|
|
282
302
|
const intent = await this.stripeRequest(`/payment_intents/${encodeURIComponent(providerPaymentId)}/cancel`, {
|
|
283
303
|
method: "POST",
|
|
284
304
|
idempotencyKey: input.idempotencyKey,
|
|
305
|
+
stripeAccount: this.resolveStripeAccount(input.stripeAccount),
|
|
285
306
|
body: formEncode(Object.fromEntries([["cancellation_reason", reason]]))
|
|
286
307
|
});
|
|
287
308
|
return {
|
|
@@ -322,6 +343,7 @@ var StripeAdapter = class {
|
|
|
322
343
|
const session = await this.stripeRequest("/checkout/sessions", {
|
|
323
344
|
method: "POST",
|
|
324
345
|
idempotencyKey: input.idempotencyKey,
|
|
346
|
+
stripeAccount: this.resolveStripeAccount(input.stripeAccount),
|
|
325
347
|
body: formEncode(params)
|
|
326
348
|
});
|
|
327
349
|
const sessionId = normalizeOptionalProviderString(readProviderString(session, "id"));
|
|
@@ -339,7 +361,7 @@ var StripeAdapter = class {
|
|
|
339
361
|
const sessionId = normalizeNonEmptyString(input.sessionId, "Stripe setup sessionId");
|
|
340
362
|
const query = new URLSearchParams();
|
|
341
363
|
query.append("expand[]", "setup_intent.payment_method");
|
|
342
|
-
const session = await this.stripeRequest(`/checkout/sessions/${encodeURIComponent(sessionId)}?${query.toString()}
|
|
364
|
+
const session = await this.stripeRequest(`/checkout/sessions/${encodeURIComponent(sessionId)}?${query.toString()}`, { stripeAccount: this.resolveStripeAccount(input.stripeAccount) });
|
|
343
365
|
const setupIntent = readObject(session, "setup_intent");
|
|
344
366
|
const paymentMethod = readObject(setupIntent, "payment_method");
|
|
345
367
|
const card = readObject(paymentMethod, "card");
|
|
@@ -360,41 +382,174 @@ var StripeAdapter = class {
|
|
|
360
382
|
raw: session
|
|
361
383
|
};
|
|
362
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Verify and parse a delivery to the platform webhook endpoint. Events that
|
|
387
|
+
* carry a connected `account` are rejected: they belong to the Connect
|
|
388
|
+
* endpoint and must never be processed as platform-account events.
|
|
389
|
+
*/
|
|
363
390
|
parseWebhookEvent(payload, signature) {
|
|
364
|
-
|
|
391
|
+
const event = this.verifyWebhookEvent(payload, signature, "platform");
|
|
392
|
+
this.recordWebhookEvent(event);
|
|
393
|
+
return event;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Verify a Stripe Connect endpoint delivery with `connectWebhookSecret`,
|
|
397
|
+
* then parse it. The signature is checked before the payload is parsed, and
|
|
398
|
+
* an event without a connected `account` is rejected. When this adapter is
|
|
399
|
+
* scoped to a `stripeAccount`, events for any other account are rejected.
|
|
400
|
+
* The payload, including `quoteId`, is controlled by the connected account:
|
|
401
|
+
* bind `quoteId` to `account` before acting on it.
|
|
402
|
+
*/
|
|
403
|
+
parseConnectWebhookEvent(payload, signature) {
|
|
404
|
+
const event = this.verifyWebhookEvent(payload, signature, "connect");
|
|
405
|
+
this.recordWebhookEvent(event);
|
|
406
|
+
return event;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Verify a webhook delivery, then map its connected account to an owner
|
|
410
|
+
* (for example a tenant) with the host's resolver. An account the resolver
|
|
411
|
+
* does not recognize is rejected with {@link PaymentNotOwnedError}; there is
|
|
412
|
+
* no fallback to the platform account. The event is recorded for duplicate
|
|
413
|
+
* detection only after it resolves, so a retried delivery that failed to
|
|
414
|
+
* resolve is not reported as a duplicate.
|
|
415
|
+
*
|
|
416
|
+
* Security: a connected account controls the objects it creates, so a
|
|
417
|
+
* Connect event's `quoteId` (and every other payload field) is chosen by
|
|
418
|
+
* that account. Resolving the account proves only who sent the event. The
|
|
419
|
+
* host must check that `event.quoteId` belongs to the resolved owner (for
|
|
420
|
+
* example inside `resolveAccount`, which receives the event, or before
|
|
421
|
+
* settling) and must never settle another owner's quote from it.
|
|
422
|
+
*/
|
|
423
|
+
async resolveConnectWebhookEvent(payload, signature, options) {
|
|
424
|
+
if (typeof options?.resolveAccount !== "function") throw new PaymentConfigurationError("Stripe resolveConnectWebhookEvent requires a resolveAccount function.");
|
|
425
|
+
const endpoint = options.endpoint ?? "connect";
|
|
426
|
+
if (endpoint !== "connect" && endpoint !== "platform") throw new PaymentConfigurationError("Stripe resolveConnectWebhookEvent endpoint must be connect or platform.");
|
|
427
|
+
const event = this.verifyWebhookEvent(payload, signature, endpoint);
|
|
428
|
+
const account = endpoint === "connect" ? event.account : event.connect?.type === "application_fee.created" || event.connect?.type === "application_fee.refunded" ? event.connect.applicationFee.account : void 0;
|
|
429
|
+
if (account === void 0) throw new PaymentNotOwnedError(`Stripe webhook event ${event.id} (${event.type}) has no connected account to route by.`);
|
|
430
|
+
this.assertAdapterAccount(account);
|
|
431
|
+
const owner = await options.resolveAccount(account, event);
|
|
432
|
+
if (owner === void 0 || owner === null) throw new PaymentNotOwnedError(`Stripe connected account ${account} is not mapped to an owner.`);
|
|
433
|
+
this.recordWebhookEvent(event);
|
|
434
|
+
return {
|
|
435
|
+
event,
|
|
436
|
+
account,
|
|
437
|
+
owner
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Create a Stripe Connect **Standard** account. The account holder owns the
|
|
442
|
+
* Stripe relationship and dashboard; the platform only connects to it.
|
|
443
|
+
* Always issued on the platform account, never on a connected account.
|
|
444
|
+
*/
|
|
445
|
+
async createStandardAccount(input = {}) {
|
|
446
|
+
const country = input.country === void 0 ? void 0 : normalizeStripeCountry(input.country);
|
|
447
|
+
const email = input.email === void 0 ? void 0 : normalizeNonEmptyString(input.email, "Stripe account email");
|
|
448
|
+
return mapStripeConnectAccount(await this.stripeRequest("/accounts", {
|
|
449
|
+
method: "POST",
|
|
450
|
+
idempotencyKey: input.idempotencyKey,
|
|
451
|
+
body: formEncode({
|
|
452
|
+
type: "standard",
|
|
453
|
+
country,
|
|
454
|
+
email,
|
|
455
|
+
...Object.fromEntries([["business_type", input.businessType]]),
|
|
456
|
+
...flattenStripeMetadata(input.metadata)
|
|
457
|
+
})
|
|
458
|
+
}), "Stripe account response");
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Create a single-use Account Link that sends the account holder through
|
|
462
|
+
* Stripe-hosted onboarding. `refreshUrl` is visited when the link expires or
|
|
463
|
+
* is reused, and should mint a new link; `returnUrl` does not mean
|
|
464
|
+
* onboarding finished, so read {@link getAccountCapabilities} afterwards.
|
|
465
|
+
*/
|
|
466
|
+
async createOnboardingLink(accountId, returnUrl, refreshUrl, options = {}) {
|
|
467
|
+
const account = normalizeStripeConnectedAccountId(accountId);
|
|
468
|
+
const link = await this.stripeRequest("/account_links", {
|
|
469
|
+
method: "POST",
|
|
470
|
+
idempotencyKey: options.idempotencyKey,
|
|
471
|
+
body: formEncode({
|
|
472
|
+
account,
|
|
473
|
+
type: "account_onboarding",
|
|
474
|
+
collect: options.collect,
|
|
475
|
+
...Object.fromEntries([["return_url", normalizeUrlString(returnUrl, "Stripe returnUrl")], ["refresh_url", normalizeUrlString(refreshUrl, "Stripe refreshUrl")]])
|
|
476
|
+
})
|
|
477
|
+
});
|
|
478
|
+
const url = normalizeOptionalProviderString(readProviderString(link, "url"));
|
|
479
|
+
if (!url) throw new PaymentProviderError("Stripe Account Link response did not include a url.");
|
|
480
|
+
const expiresAtSeconds = readSafeInteger(link, "expires_at");
|
|
481
|
+
return {
|
|
482
|
+
url,
|
|
483
|
+
expiresAt: expiresAtSeconds === void 0 ? void 0 : /* @__PURE__ */ new Date(expiresAtSeconds * 1e3),
|
|
484
|
+
raw: link
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Read a connected account's onboarding and capability state
|
|
489
|
+
* (`charges_enabled`, `details_submitted`, `payouts_enabled`, per-capability
|
|
490
|
+
* status, and outstanding requirements).
|
|
491
|
+
*/
|
|
492
|
+
async getAccountCapabilities(accountId) {
|
|
493
|
+
const account = normalizeStripeConnectedAccountId(accountId);
|
|
494
|
+
const status = mapStripeConnectAccount(await this.stripeRequest(`/accounts/${encodeURIComponent(account)}`), "Stripe account response");
|
|
495
|
+
if (status.accountId !== account) throw new PaymentVerificationError(`Stripe account response ${status.accountId} did not match ${account}.`);
|
|
496
|
+
return status;
|
|
497
|
+
}
|
|
498
|
+
verifyWebhookEvent(payload, signature, endpoint) {
|
|
499
|
+
const secret = endpoint === "connect" ? this.connectWebhookSecret : this.webhookSecret;
|
|
500
|
+
if (!secret) throw new PaymentConfigurationError(endpoint === "connect" ? "StripeAdapter parseConnectWebhookEvent requires connectWebhookSecret." : "StripeAdapter parseWebhookEvent requires webhookSecret.");
|
|
365
501
|
if (!signature) throw new PaymentProviderError("Missing Stripe webhook signature.");
|
|
366
|
-
verifyStripeWebhookSignature(payload, signature,
|
|
502
|
+
verifyStripeWebhookSignature(payload, signature, secret);
|
|
367
503
|
const event = parseStripeWebhookPayload(payload);
|
|
368
504
|
const id = readRequiredWebhookString(event, "id", "Stripe webhook event id");
|
|
369
505
|
const type = readRequiredWebhookString(event, "type", "Stripe webhook event type");
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
}
|
|
506
|
+
const rawAccount = event.account;
|
|
507
|
+
let account;
|
|
508
|
+
if (endpoint === "connect") {
|
|
509
|
+
account = typeof rawAccount === "string" ? rawAccount.trim() : void 0;
|
|
510
|
+
if (!account || !STRIPE_CONNECTED_ACCOUNT_ID.test(account)) throw new PaymentVerificationError("Stripe Connect webhook event did not include a valid connected account.");
|
|
511
|
+
this.assertAdapterAccount(account);
|
|
512
|
+
} else if (rawAccount !== void 0 && rawAccount !== null) throw new PaymentVerificationError("Stripe connected-account event received on the platform webhook endpoint; verify it with parseConnectWebhookEvent.");
|
|
379
513
|
const data = event.data?.object;
|
|
514
|
+
const connect = mapStripeConnectEventDetail(type, data);
|
|
515
|
+
if (account !== void 0 && connect?.type === "account.updated" && connect.account.accountId !== account) throw new PaymentVerificationError("Stripe account.updated webhook object did not match the event account.");
|
|
380
516
|
return {
|
|
381
517
|
id,
|
|
382
518
|
type,
|
|
383
519
|
status: mapStripeWebhookStatus(type, data),
|
|
384
520
|
quoteId: normalizeOptionalWebhookString((type.startsWith("checkout.session.") ? readString(data, "client_reference_id") : void 0) ?? readString(data?.metadata ?? {}, "quoteId")),
|
|
385
521
|
providerPaymentId: normalizeOptionalWebhookString(readString(data, "id")),
|
|
386
|
-
duplicate,
|
|
522
|
+
duplicate: this.seenWebhookEventIds.has(id),
|
|
523
|
+
...account === void 0 ? {} : { account },
|
|
524
|
+
...connect === void 0 ? {} : { connect },
|
|
387
525
|
raw: event
|
|
388
526
|
};
|
|
389
527
|
}
|
|
528
|
+
recordWebhookEvent(event) {
|
|
529
|
+
if (this.seenWebhookEventIds.has(event.id)) {
|
|
530
|
+
event.duplicate = true;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
event.duplicate = false;
|
|
534
|
+
this.seenWebhookEventIds.add(event.id);
|
|
535
|
+
while (this.seenWebhookEventIds.size > this.maxStoredWebhookEventIds) {
|
|
536
|
+
const oldest = this.seenWebhookEventIds.values().next().value;
|
|
537
|
+
if (oldest === void 0) break;
|
|
538
|
+
this.seenWebhookEventIds.delete(oldest);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
assertAdapterAccount(account) {
|
|
542
|
+
if (this.stripeAccount !== void 0 && account !== this.stripeAccount) throw new PaymentNotOwnedError(`Stripe connected account ${account} does not match this adapter's account scope.`);
|
|
543
|
+
}
|
|
390
544
|
async stripeRequest(path, init = {}) {
|
|
391
545
|
const headers = new Headers(init.headers);
|
|
392
546
|
headers.set("Authorization", `Bearer ${this.secretKey}`);
|
|
393
547
|
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
|
394
548
|
headers.set("Accept", "application/json");
|
|
395
549
|
headers.set("Stripe-Version", this.apiVersion);
|
|
550
|
+
if (init.stripeAccount !== void 0) headers.set("Stripe-Account", init.stripeAccount);
|
|
396
551
|
if (init.idempotencyKey !== void 0) headers.set("Idempotency-Key", normalizeNonEmptyString(init.idempotencyKey, "Stripe idempotencyKey"));
|
|
397
|
-
const { idempotencyKey: _idempotencyKey, ...requestInit } = init;
|
|
552
|
+
const { idempotencyKey: _idempotencyKey, stripeAccount: _stripeAccount, ...requestInit } = init;
|
|
398
553
|
return readJsonResponse(await this.fetch(`${this.apiBaseUrl}${path}`, {
|
|
399
554
|
...requestInit,
|
|
400
555
|
method: init.method ?? "GET",
|
|
@@ -404,15 +559,111 @@ var StripeAdapter = class {
|
|
|
404
559
|
assertSupportedCurrency(currency) {
|
|
405
560
|
if (!this.supportedCurrencies.has(currency)) throw new PaymentConfigurationError(`StripeAdapter currency ${currency.toUpperCase()} is not configured as supported.`);
|
|
406
561
|
}
|
|
407
|
-
async resolveStripeRefundReferenceParam(paymentReference) {
|
|
562
|
+
async resolveStripeRefundReferenceParam(paymentReference, stripeAccount) {
|
|
408
563
|
if (paymentReference.startsWith("cs_")) {
|
|
409
|
-
const paymentIntent = normalizeOptionalProviderString(readProviderString(await this.stripeRequest(`/checkout/sessions/${encodeURIComponent(paymentReference)}
|
|
564
|
+
const paymentIntent = normalizeOptionalProviderString(readProviderString(await this.stripeRequest(`/checkout/sessions/${encodeURIComponent(paymentReference)}`, { stripeAccount }), "payment_intent"));
|
|
410
565
|
if (!paymentIntent) throw new PaymentProviderError("Stripe Checkout Session did not include payment_intent for refund.");
|
|
411
566
|
return Object.fromEntries([["payment_intent", paymentIntent]]);
|
|
412
567
|
}
|
|
413
568
|
return stripeRefundReferenceParam(paymentReference);
|
|
414
569
|
}
|
|
570
|
+
resolveStripeAccount(override) {
|
|
571
|
+
return normalizeOptionalStripeAccount(override, "Stripe stripeAccount") ?? this.stripeAccount;
|
|
572
|
+
}
|
|
573
|
+
findStoredOptionBySessionId(sessionId) {
|
|
574
|
+
for (const option of this.optionsByQuote.values()) if (option.providerPaymentId === sessionId) return option;
|
|
575
|
+
}
|
|
415
576
|
};
|
|
577
|
+
function assertOptionalSecret(value, name) {
|
|
578
|
+
if (value === void 0) return;
|
|
579
|
+
if (typeof value !== "string") throw new PaymentConfigurationError(`StripeAdapter ${name} must be a string when configured.`);
|
|
580
|
+
if (!value.trim()) throw new PaymentConfigurationError(`StripeAdapter ${name} must not be empty when configured.`);
|
|
581
|
+
}
|
|
582
|
+
var STRIPE_CONNECTED_ACCOUNT_ID = /^acct_[A-Za-z0-9]+$/;
|
|
583
|
+
function normalizeStripeConnectedAccountId(value) {
|
|
584
|
+
const account = typeof value === "string" ? value.trim() : void 0;
|
|
585
|
+
if (!account || !STRIPE_CONNECTED_ACCOUNT_ID.test(account)) throw new PaymentConfigurationError("Stripe connected account id must look like acct_...");
|
|
586
|
+
return account;
|
|
587
|
+
}
|
|
588
|
+
function normalizeStripeCountry(value) {
|
|
589
|
+
const country = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
590
|
+
if (!/^[A-Z]{2}$/.test(country)) throw new PaymentConfigurationError("Stripe account country must be a two-letter ISO country code.");
|
|
591
|
+
return country;
|
|
592
|
+
}
|
|
593
|
+
function readStringArray(value, key) {
|
|
594
|
+
const item = value?.[key];
|
|
595
|
+
return Array.isArray(item) ? item.filter((entry) => typeof entry === "string") : [];
|
|
596
|
+
}
|
|
597
|
+
function mapStripeConnectAccount(account, context) {
|
|
598
|
+
const accountId = readProviderString(account, "id")?.trim();
|
|
599
|
+
if (!account || !accountId || !STRIPE_CONNECTED_ACCOUNT_ID.test(accountId)) throw new PaymentProviderError(`${context} did not include an account id.`);
|
|
600
|
+
const capabilities = {};
|
|
601
|
+
for (const [name, state] of Object.entries(readObject(account, "capabilities") ?? {})) if (typeof state === "string") capabilities[name] = state;
|
|
602
|
+
const requirements = readObject(account, "requirements");
|
|
603
|
+
const disabledReason = normalizeOptionalProviderString(readProviderString(requirements, "disabled_reason"));
|
|
604
|
+
const defaultCurrency = readProviderString(account, "default_currency");
|
|
605
|
+
return {
|
|
606
|
+
accountId,
|
|
607
|
+
type: normalizeOptionalProviderString(readProviderString(account, "type")),
|
|
608
|
+
chargesEnabled: account.charges_enabled === true,
|
|
609
|
+
detailsSubmitted: account.details_submitted === true,
|
|
610
|
+
payoutsEnabled: account.payouts_enabled === true,
|
|
611
|
+
capabilities,
|
|
612
|
+
requirements: {
|
|
613
|
+
currentlyDue: readStringArray(requirements, "currently_due"),
|
|
614
|
+
eventuallyDue: readStringArray(requirements, "eventually_due"),
|
|
615
|
+
pastDue: readStringArray(requirements, "past_due"),
|
|
616
|
+
...disabledReason === void 0 ? {} : { disabledReason }
|
|
617
|
+
},
|
|
618
|
+
country: normalizeOptionalProviderString(readProviderString(account, "country")),
|
|
619
|
+
defaultCurrency: defaultCurrency?.toUpperCase(),
|
|
620
|
+
raw: account
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function mapStripeApplicationFee(fee) {
|
|
624
|
+
const id = normalizeOptionalProviderString(readProviderString(fee, "id"));
|
|
625
|
+
if (!fee || !id) throw new PaymentProviderError("Stripe application_fee webhook object did not include an id.");
|
|
626
|
+
const account = normalizeOptionalProviderString(readProviderString(fee, "account"));
|
|
627
|
+
if (account !== void 0 && !STRIPE_CONNECTED_ACCOUNT_ID.test(account)) throw new PaymentProviderError("Stripe application_fee webhook object has an invalid account id.");
|
|
628
|
+
const livemode = fee.livemode;
|
|
629
|
+
return {
|
|
630
|
+
id,
|
|
631
|
+
account,
|
|
632
|
+
amount: readSafeInteger(fee, "amount"),
|
|
633
|
+
amountRefunded: readSafeInteger(fee, "amount_refunded"),
|
|
634
|
+
currency: readStripeResultCurrency(fee),
|
|
635
|
+
refunded: fee.refunded === true,
|
|
636
|
+
charge: normalizeOptionalProviderString(readProviderString(fee, "charge") ?? readProviderString(readObject(fee, "charge"), "id")),
|
|
637
|
+
livemode: typeof livemode === "boolean" ? livemode : void 0,
|
|
638
|
+
raw: fee
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function mapStripeConnectEventDetail(type, object) {
|
|
642
|
+
if (type === "account.updated") return {
|
|
643
|
+
type,
|
|
644
|
+
account: mapStripeConnectAccount(object, "Stripe account.updated webhook object")
|
|
645
|
+
};
|
|
646
|
+
if (type === "application_fee.created" || type === "application_fee.refunded") return {
|
|
647
|
+
type,
|
|
648
|
+
applicationFee: mapStripeApplicationFee(object)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function readStripeOptionAccount(option) {
|
|
652
|
+
const value = option?.metadata?.stripeAccount;
|
|
653
|
+
return typeof value === "string" ? value : void 0;
|
|
654
|
+
}
|
|
655
|
+
function normalizeOptionalStripeAccount(value, context) {
|
|
656
|
+
return value === void 0 ? void 0 : normalizeNonEmptyString(value, context);
|
|
657
|
+
}
|
|
658
|
+
function normalizeApplicationFeeAmount(value, chargeAmount, context) {
|
|
659
|
+
if (value === void 0) return void 0;
|
|
660
|
+
const fee = normalizePositiveMinorUnitAmount(value, context);
|
|
661
|
+
if (fee > chargeAmount) throw new PaymentConfigurationError(`${context} must not exceed the charge amount.`);
|
|
662
|
+
return fee;
|
|
663
|
+
}
|
|
664
|
+
function stripePaymentOptionKey(quoteId, stripeAccount) {
|
|
665
|
+
return stripeAccount === void 0 ? quoteId : `${stripeAccount}\0${quoteId}`;
|
|
666
|
+
}
|
|
416
667
|
function stripeRefundReferenceParam(paymentReference) {
|
|
417
668
|
if (paymentReference.startsWith("pi_")) return Object.fromEntries([["payment_intent", paymentReference]]);
|
|
418
669
|
if (paymentReference.startsWith("ch_")) return { charge: paymentReference };
|