@happyvertical/payments 0.105.4 → 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 +108 -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 +157 -0
- package/dist/adapters/stripe.d.ts.map +1 -1
- package/dist/adapters/stripe.js +224 -18
- 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,7 @@ var StripeAdapter = class {
|
|
|
18
18
|
supportedCurrencies;
|
|
19
19
|
checkoutMode;
|
|
20
20
|
webhookSecret;
|
|
21
|
+
connectWebhookSecret;
|
|
21
22
|
stripeAccount;
|
|
22
23
|
maxStoredPaymentOptions;
|
|
23
24
|
maxStoredWebhookEventIds;
|
|
@@ -26,10 +27,8 @@ var StripeAdapter = class {
|
|
|
26
27
|
constructor(options) {
|
|
27
28
|
this.options = options;
|
|
28
29
|
if (typeof options.secretKey !== "string" || !options.secretKey.trim()) throw new PaymentConfigurationError("StripeAdapter requires secretKey.");
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!options.webhookSecret.trim()) throw new PaymentConfigurationError("StripeAdapter webhookSecret must not be empty when configured.");
|
|
32
|
-
}
|
|
30
|
+
assertOptionalSecret(options.webhookSecret, "webhookSecret");
|
|
31
|
+
assertOptionalSecret(options.connectWebhookSecret, "connectWebhookSecret");
|
|
33
32
|
if (options.apiBaseUrl !== void 0) {
|
|
34
33
|
if (typeof options.apiBaseUrl !== "string") throw new PaymentConfigurationError("StripeAdapter apiBaseUrl must be a string when configured.");
|
|
35
34
|
if (!options.apiBaseUrl.trim()) throw new PaymentConfigurationError("StripeAdapter apiBaseUrl must not be empty when configured.");
|
|
@@ -45,6 +44,7 @@ var StripeAdapter = class {
|
|
|
45
44
|
this.checkoutMode = options.checkoutMode ?? "payment";
|
|
46
45
|
if (this.checkoutMode !== "payment") throw new PaymentConfigurationError("StripeAdapter only supports payment Checkout mode.");
|
|
47
46
|
this.webhookSecret = options.webhookSecret?.trim();
|
|
47
|
+
this.connectWebhookSecret = options.connectWebhookSecret?.trim();
|
|
48
48
|
this.stripeAccount = normalizeOptionalStripeAccount(options.stripeAccount, "StripeAdapter stripeAccount");
|
|
49
49
|
this.capabilities = {
|
|
50
50
|
id: STRIPE_BACKEND_ID,
|
|
@@ -382,33 +382,165 @@ var StripeAdapter = class {
|
|
|
382
382
|
raw: session
|
|
383
383
|
};
|
|
384
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
|
+
*/
|
|
385
390
|
parseWebhookEvent(payload, signature) {
|
|
386
|
-
|
|
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.");
|
|
387
501
|
if (!signature) throw new PaymentProviderError("Missing Stripe webhook signature.");
|
|
388
|
-
verifyStripeWebhookSignature(payload, signature,
|
|
502
|
+
verifyStripeWebhookSignature(payload, signature, secret);
|
|
389
503
|
const event = parseStripeWebhookPayload(payload);
|
|
390
504
|
const id = readRequiredWebhookString(event, "id", "Stripe webhook event id");
|
|
391
505
|
const type = readRequiredWebhookString(event, "type", "Stripe webhook event type");
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
}
|
|
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.");
|
|
401
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.");
|
|
402
516
|
return {
|
|
403
517
|
id,
|
|
404
518
|
type,
|
|
405
519
|
status: mapStripeWebhookStatus(type, data),
|
|
406
520
|
quoteId: normalizeOptionalWebhookString((type.startsWith("checkout.session.") ? readString(data, "client_reference_id") : void 0) ?? readString(data?.metadata ?? {}, "quoteId")),
|
|
407
521
|
providerPaymentId: normalizeOptionalWebhookString(readString(data, "id")),
|
|
408
|
-
duplicate,
|
|
522
|
+
duplicate: this.seenWebhookEventIds.has(id),
|
|
523
|
+
...account === void 0 ? {} : { account },
|
|
524
|
+
...connect === void 0 ? {} : { connect },
|
|
409
525
|
raw: event
|
|
410
526
|
};
|
|
411
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
|
+
}
|
|
412
544
|
async stripeRequest(path, init = {}) {
|
|
413
545
|
const headers = new Headers(init.headers);
|
|
414
546
|
headers.set("Authorization", `Bearer ${this.secretKey}`);
|
|
@@ -442,6 +574,80 @@ var StripeAdapter = class {
|
|
|
442
574
|
for (const option of this.optionsByQuote.values()) if (option.providerPaymentId === sessionId) return option;
|
|
443
575
|
}
|
|
444
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
|
+
}
|
|
445
651
|
function readStripeOptionAccount(option) {
|
|
446
652
|
const value = option?.metadata?.stripeAccount;
|
|
447
653
|
return typeof value === "string" ? value : void 0;
|