@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.
@@ -1,5 +1,5 @@
1
- import { a as PaymentVerificationError, r as PaymentProviderError, t as PaymentConfigurationError } from "../chunks/errors-feZrwnTy.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-DVl6UzD9.js";
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
- if (options.webhookSecret !== void 0) {
29
- if (typeof options.webhookSecret !== "string") throw new PaymentConfigurationError("StripeAdapter webhookSecret must be a string when configured.");
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 sessionId = this.optionsByQuote.get(normalizedQuoteId)?.providerPaymentId ?? contextProviderPaymentId ?? extractStripeSessionId(normalizedPayTo);
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 session = await this.stripeRequest(`/checkout/sessions/${encodeURIComponent(sessionId)}`);
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
- if (!this.webhookSecret) throw new PaymentConfigurationError("StripeAdapter parseWebhookEvent requires webhookSecret.");
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, this.webhookSecret);
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 duplicate = this.seenWebhookEventIds.has(id);
371
- if (!duplicate) {
372
- this.seenWebhookEventIds.add(id);
373
- while (this.seenWebhookEventIds.size > this.maxStoredWebhookEventIds) {
374
- const oldest = this.seenWebhookEventIds.values().next().value;
375
- if (oldest === void 0) break;
376
- this.seenWebhookEventIds.delete(oldest);
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)}`), "payment_intent"));
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 };