@agent-cards/sdk 0.23.10 → 0.23.12

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/dist/client.js CHANGED
@@ -14,6 +14,7 @@ import { creditguardPageProcessUrl, creditguardPpDomainHost, creditguardPreparat
14
14
  import { CONVERGE_CAPABILITY } from './elavon-converge.js';
15
15
  import { SHIJI_STS_CAPABILITY, SHIJI_STS_PSP, isShijiStsServedCompanion, isShijiStsServedSubstitution } from './shiji-sts.js';
16
16
  import { AMADEUS_CHECKOUT_CAPABILITY, AMADEUS_CHECKOUT_PSP } from './amadeus-checkout.generated.js';
17
+ import { PADDLE_VGS_CAPABILITY, PADDLE_VGS_PSP } from './paddle-vgs.generated.js';
17
18
  import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
18
19
  import { VoiceModule } from './voice.js';
19
20
  import { BuyModule } from './buy.js';
@@ -185,8 +186,16 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
185
186
  * the same terms: only to a build that lists it, for a company Agentcard turned
186
187
  * Moneris on for. A gated recognizer never sits in BUILTIN_REGISTRY, so it is paused
187
188
  * only after a sync.
189
+ *
190
+ * `paddle_vgs_cards`: the card request Paddle's checkout frame sends to its VGS card
191
+ * vault (vault.paddle.com/cards) on a checkout that names VGS, only through a prepared
192
+ * checkout: the frame aborts the request 10 seconds after Pay and sends it once more,
193
+ * and this build answers that second request from the same approval. The cardholder's
194
+ * device sends the card itself and the frame gets back the stored card's id alone.
195
+ * Served on the same terms: only to a build that lists it, for a company Agentcard
196
+ * turned Paddle on for.
188
197
  */
189
- export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CYBERSOURCE_FLEX_CAPABILITY, JUSPAY_CAPABILITY, DATATRANS_CAPABILITY, 'authorize_net_dpm_form', PELECARD_CAPABILITY, ACI_CAPABILITY, 'creditguard_pps_process', PRIMER_CAPABILITY, PAYRAILS_CAPABILITY, CONVERGE_CAPABILITY, EPROTECT_CAPABILITY, WSPAY_CAPABILITY, SHIJI_STS_CAPABILITY, AMADEUS_CHECKOUT_CAPABILITY, 'worldline_mycheckout_form', 'verifone_card_capture', 'ekashu_hosted_payment_page', SECURE_ACCEPTANCE_CAPABILITY, 'gravy_session_fields_put', 'moneris_hosted_tokenization'];
198
+ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CYBERSOURCE_FLEX_CAPABILITY, JUSPAY_CAPABILITY, DATATRANS_CAPABILITY, 'authorize_net_dpm_form', PELECARD_CAPABILITY, ACI_CAPABILITY, 'creditguard_pps_process', PRIMER_CAPABILITY, PAYRAILS_CAPABILITY, CONVERGE_CAPABILITY, EPROTECT_CAPABILITY, WSPAY_CAPABILITY, SHIJI_STS_CAPABILITY, AMADEUS_CHECKOUT_CAPABILITY, 'worldline_mycheckout_form', 'verifone_card_capture', 'ekashu_hosted_payment_page', SECURE_ACCEPTANCE_CAPABILITY, 'gravy_session_fields_put', 'moneris_hosted_tokenization', PADDLE_VGS_CAPABILITY];
190
199
  /**
191
200
  * Processors whose paused request names no amount at all, so the pair the
192
201
  * agent passes is the amount the cardholder approves: authorize() refuses one
@@ -207,13 +216,15 @@ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CY
207
216
  * no more than that (the API's card rule). Moneris Hosted Tokenization's
208
217
  * request.php carries the card and no price: the merchant's server charges the
209
218
  * token later, so the agent's pair, above zero (MONERIS_PSP below), is the one
210
- * figure the approval shows.
219
+ * figure the approval shows. Paddle VGS's card request carries none either: Paddle
220
+ * charges the stored card at /pay for what the checkout says, so the agent's pair,
221
+ * which the prepared checkout carries, is the total the cardholder approves.
211
222
  */
212
223
  const MONERIS_PSP = 'moneris';
213
- const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP];
224
+ const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP, PADDLE_VGS_PSP];
214
225
  /** How AmountRequiredError names a processor. */
215
226
  const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO', [PELECARD_PSP]: 'Pelecard', [WSPAY_PSP]: 'WSPay',
216
- [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris' };
227
+ [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle' };
217
228
  /**
218
229
  * Processors whose request and answer name no amount at all, so the total the
219
230
  * payment page shows (`pageAmount`) is the one figure that ties the agent's pair
@@ -423,6 +434,35 @@ const PLACEHOLDER_NUMBERS = Object.freeze({
423
434
  discover: '6011000990139424',
424
435
  jcb: '3566111111111113',
425
436
  });
437
+ /**
438
+ * The placeholder number a processor needs in place of its network's default above, by processor
439
+ * (the recognizer's `psp`) and network. A processor whose page reads the typed number's IIN to
440
+ * pick a card product can read the default as another product than the card that pays:
441
+ * Worldline MyCheckout read 4111111111111111 as Visa Electron (122), not Visa (1), on CLZ's live
442
+ * page on 2026-10-12, and switched its form's product. The SDK refuses that submit
443
+ * (worldline-mycheckout.ts), so until this table names a Visa number Worldline reads as product 1,
444
+ * a Visa card cannot pay on a MyCheckout page that offers Visa Electron. The default table stays
445
+ * as it is: CyberSource Flex's Microform rule, ACI's brand check and EBANX's eligibility guard
446
+ * read its Visa and Mastercard numbers.
447
+ *
448
+ * TODO(Felipe): pick worldline_mycheckout's Visa number from Worldline Connect's own test-card
449
+ * list, then confirm it with one production IIN check on a MyCheckout card form that offers
450
+ * products 1 and 122: type it, wait for the IIN answer, read paymentProductId and the logo, clear
451
+ * the field, never Pay. Add it here as `worldline_mycheckout: { visa: '<number>' }`, with the
452
+ * check's date and merchant, and to the live runner's placeholder allowlist.
453
+ */
454
+ const PROCESSOR_PLACEHOLDER_NUMBERS = Object.freeze({});
455
+ /**
456
+ * The placeholder number for a network (lowercase, as GET /api/v2/vault_cards names it) on a
457
+ * processor's page: the processor's own number for that network when `overrides` names one, else
458
+ * the network's default, else undefined for a network with no placeholder.
459
+ */
460
+ export function placeholderNumber(brand, psp, overrides = PROCESSOR_PLACEHOLDER_NUMBERS) {
461
+ const own = psp !== undefined && Object.prototype.hasOwnProperty.call(overrides, psp) ? overrides[psp] : undefined;
462
+ if (own && Object.prototype.hasOwnProperty.call(own, brand))
463
+ return own[brand];
464
+ return Object.prototype.hasOwnProperty.call(PLACEHOLDER_NUMBERS, brand) ? PLACEHOLDER_NUMBERS[brand] : undefined;
465
+ }
426
466
  /**
427
467
  * The recognizer says this processor is preparation-required, and no preparation
428
468
  * was passed. Its card may be sent only after the cardholder approved a
@@ -920,6 +960,71 @@ function maskCreditguardTxId(path) {
920
960
  const PATH_SECRET_HOSTS = ['proxy.wspay.biz', 'proxy-test.wspay.biz', 'ipg.monri.com', 'ipgtest.monri.com'];
921
961
  /** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) or after an escaped slash. */
922
962
  const PATH_SECRET = /(\/payment(?:\/|%2f))[^/]*/gi;
963
+ /**
964
+ * Gravy (Gr4vy) gives every merchant hosts of its own under gr4vy.app, and Secure Fields sends the
965
+ * card to /checkout/sessions/<session id>/fields with no auth header, so the session id is that
966
+ * request's only credential (payment-core definitions.js): with it anyone can write card fields
967
+ * into the session until it expires. On those hosts the segment after /checkout/sessions/, in any
968
+ * case or after an escaped slash, reads {session}, as does any other id shaped like Gravy's UUID
969
+ * session ids.
970
+ */
971
+ const GRAVY_HOST = /(?:^|\.)gr4vy\.app\.?$/i;
972
+ const GRAVY_SESSION = /(\/checkout\/sessions(?:\/|%2f))[^/]*/gi;
973
+ const GRAVY_SESSION_ID = /[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi;
974
+ function maskGravySession(path) {
975
+ return path.replace(GRAVY_SESSION, '$1{session}').replace(GRAVY_SESSION_ID, '{session}');
976
+ }
977
+ /**
978
+ * Rapyd's hosted pages send the card to api.rapyd.net (sandboxapi.rapyd.net in test) with no auth
979
+ * header: the page's own id in the path, checkout_<id> for a checkout page and hp_card_<id> for a
980
+ * card page, is the request's only credential (payment-core definitions.js), and with it anyone can
981
+ * read that page and post a card to it until it expires. On Rapyd's hosts every id of those shapes
982
+ * (hp_reuse_<id>, a payment link's, too) and the segment after /hosted/collect/card/, in any case or
983
+ * after an escaped slash, read {token}.
984
+ */
985
+ const RAPYD_HOST = /(?:^|\.)rapyd\.net\.?$/i;
986
+ const RAPYD_CARD_PAGE = /(\/hosted\/collect\/card(?:\/|%2f))[^/]*/gi;
987
+ const RAPYD_PAGE_ID = /(?:checkout|hp_[a-z]+)_[a-z0-9]+/gi;
988
+ function maskRapydToken(path) {
989
+ return path.replace(RAPYD_CARD_PAGE, '$1{token}').replace(RAPYD_PAGE_ID, '{token}');
990
+ }
991
+ /**
992
+ * Stripe Checkout's pages confirm a Checkout Session at /v1/payment_pages/<session id>/confirm on
993
+ * api.stripe.com with the merchant's publishable key, which is public, so the session id is what
994
+ * lets anyone confirm a payment on that checkout until it expires. There the segment after
995
+ * /payment_pages/, in any case or after an escaped slash, reads {session}. A PaymentIntent or
996
+ * SetupIntent id stays: confirming one also needs its client_secret, which rides in the body.
997
+ */
998
+ const STRIPE_API_HOST = /^api\.stripe\.com\.?$/i;
999
+ const STRIPE_PAYMENT_PAGE = /(\/payment_pages(?:\/|%2f))[^/]*/gi;
1000
+ function maskStripeSession(path) {
1001
+ return path.replace(STRIPE_PAYMENT_PAGE, '$1{session}');
1002
+ }
1003
+ /**
1004
+ * redactAciCheckoutId (aci.ts) takes the checkout id out of the paths ACI's widget sends. This rule
1005
+ * takes any other copy of it out on ACI's hosts: a trailing dot on the host, an escaped or doubled
1006
+ * slash, another case, a string that is not a URL. The segment after /v1/checkouts/ and any id of
1007
+ * ACI's shape (<32 hex>.<cluster>-vm-tx<nn>) read {checkoutId}.
1008
+ */
1009
+ const ACI_HOST = /^(?:eu-prod\.|eu-test\.|test\.)?oppwa\.com\.?$/i;
1010
+ const ACI_CHECKOUT = /(\/v1\/checkouts(?:\/|%2f))[^/]*/gi;
1011
+ const ACI_CHECKOUT_ID = /[0-9a-f]{32}\.[a-z0-9]{1,16}-vm-tx[0-9]{1,3}/gi;
1012
+ function maskAciCheckout(path) {
1013
+ return path.replace(ACI_CHECKOUT, '$1{checkoutId}').replace(ACI_CHECKOUT_ID, '{checkoutId}');
1014
+ }
1015
+ /**
1016
+ * The processors whose card request carries its own credential in the URL path, with the hosts
1017
+ * that send it, the text that names those hosts in a string that is not a URL, and the mask. The
1018
+ * guard in redact-url.test.mjs fails for a payment-core recognizer that no one has sorted into
1019
+ * this list or out of it.
1020
+ */
1021
+ const PATH_CREDENTIALS = [
1022
+ { host: CREDITGUARD_HOST, named: /creditguard\.co\.il/i, mask: maskCreditguardTxId },
1023
+ { host: GRAVY_HOST, named: /gr4vy\.app/i, mask: maskGravySession },
1024
+ { host: RAPYD_HOST, named: /rapyd\.net/i, mask: maskRapydToken },
1025
+ { host: STRIPE_API_HOST, named: /api\.stripe\.com/i, mask: maskStripeSession },
1026
+ { host: ACI_HOST, named: /oppwa\.com/i, mask: maskAciCheckout },
1027
+ ];
923
1028
  /**
924
1029
  * A URL as it may appear in an error message: origin + path only. A paused
925
1030
  * request's URL can carry a client secret in its query string, and error
@@ -929,8 +1034,13 @@ const PATH_SECRET = /(\/payment(?:\/|%2f))[^/]*/gi;
929
1034
  * so it reads {txId}, as does any other id-shaped segment there.
930
1035
  * On the Monri and WSPay hosts the secret is a path segment, so it is replaced
931
1036
  * with `{client_secret}`: https://proxy.wspay.biz/v2/payment/{client_secret}/confirm.
1037
+ * On Gravy's hosts the checkout session id is the credential, so it reads
1038
+ * `{session}`: https://api.<merchant>.gr4vy.app/checkout/sessions/{session}/fields.
1039
+ * On Rapyd's hosts the hosted page's id reads `{token}`: https://api.rapyd.net/v1/checkout/{token}.
1040
+ * On api.stripe.com a Checkout Session id reads `{session}`:
1041
+ * https://api.stripe.com/v1/payment_pages/{session}/confirm.
932
1042
  * A string that is not a URL keeps the part before any `?` or `#`, with the
933
- * same segment masked.
1043
+ * same segments masked.
934
1044
  */
935
1045
  export function redactUrl(raw) {
936
1046
  // An ACI checkout id is that checkout's only capability; it never rides on an event.
@@ -940,12 +1050,18 @@ export function redactUrl(raw) {
940
1050
  try {
941
1051
  const u = new URL(raw);
942
1052
  // A trailing dot names the same host, so it cannot let the secret through.
943
- const path = PATH_SECRET_HOSTS.includes(u.hostname.replace(/\.$/, '')) ? u.pathname.replace(PATH_SECRET, '$1{client_secret}') : u.pathname;
944
- return `${u.origin}${CREDITGUARD_HOST.test(u.hostname) ? maskCreditguardTxId(path) : path}`;
1053
+ let path = PATH_SECRET_HOSTS.includes(u.hostname.replace(/\.$/, '')) ? u.pathname.replace(PATH_SECRET, '$1{client_secret}') : u.pathname;
1054
+ for (const rule of PATH_CREDENTIALS)
1055
+ if (rule.host.test(u.hostname))
1056
+ path = rule.mask(path);
1057
+ return `${u.origin}${path}`;
945
1058
  }
946
1059
  catch {
947
- const bare = raw.split(/[?#]/)[0].replace(PATH_SECRET, '$1{client_secret}');
948
- return /creditguard\.co\.il/i.test(bare) ? maskCreditguardTxId(bare) : bare;
1060
+ let bare = raw.split(/[?#]/)[0].replace(PATH_SECRET, '$1{client_secret}');
1061
+ for (const rule of PATH_CREDENTIALS)
1062
+ if (rule.named.test(bare))
1063
+ bare = rule.mask(bare);
1064
+ return bare;
949
1065
  }
950
1066
  }
951
1067
  /**
@@ -1000,6 +1116,17 @@ const EPROTECT_PREPARATION_REFUSALS = ['merchant_profile_mismatch'];
1000
1116
  const SECURE_ACCEPTANCE_PREPARATION_REFUSALS = [
1001
1117
  'processor_unavailable', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'unsupported_checkout', 'merchant_profile_mismatch',
1002
1118
  ];
1119
+ /**
1120
+ * The API's refusals of a Paddle VGS preparation, before any preparation exists, that prepareCheckout
1121
+ * surfaces by name: the lane is held to Agentcard's own account (`processor_unavailable`), the environment
1122
+ * is not the client's test or live mode, a live checkout's merchant is not the name the reviewed seller pin
1123
+ * carries (`merchant_profile_mismatch`), and the locked card is on a network Paddle's card request is not
1124
+ * paid on (`paddle_vgs_card_unsupported`). An unreviewed seller's page answers `unsupported_checkout`, which
1125
+ * PREPARATION_REFUSALS names for every checkout. Only for a Paddle checkout.
1126
+ */
1127
+ const PADDLE_VGS_PREPARATION_REFUSALS = [
1128
+ 'processor_unavailable', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'merchant_profile_mismatch', 'paddle_vgs_card_unsupported',
1129
+ ];
1003
1130
  /**
1004
1131
  * The reason the cardholder's device declines a bound prepared row with, per processor, when its own
1005
1132
  * send boundary passed before the card left it (the Vault's sendWindow, 12 seconds after it last saw
@@ -1405,12 +1532,17 @@ export class VaultClient {
1405
1532
  * preselects that card: without a pin the page preselects its own choice (its recommended
1406
1533
  * card, or the first card the cardholder's device can open), which can be another card. After
1407
1534
  * prepare(), pass the prepared checkout's `cardId`, the card the cardholder already approved.
1408
- * The facts hold only while the cardholder pays with that card. Throws PlaceholderCardError
1409
- * when there is no such card, its network has no placeholder, or it carries no expiry.
1535
+ * The facts hold only while the cardholder pays with that card. `psp` names the processor
1536
+ * whose card form the agent types into (the recognizer's name, `worldline_mycheckout`): a
1537
+ * processor that needs its own number for the card's network gets it, and every other gets the
1538
+ * network's default. Throws PlaceholderCardError when there is no such card, its network has no
1539
+ * placeholder, or it carries no expiry.
1410
1540
  */
1411
1541
  async placeholderCard(input) {
1412
1542
  if (!input || typeof input.user !== 'string' || !input.user)
1413
1543
  throw new Error('placeholderCard needs the user whose stored card will pay.');
1544
+ if (input.psp !== undefined && (typeof input.psp !== 'string' || !input.psp))
1545
+ throw new Error('placeholderCard takes psp as the processor\'s name, such as worldline_mycheckout.');
1414
1546
  // The card list lives under /api/v2 only: the API aliases /v2/checkout alone without the prefix.
1415
1547
  const list = await this.get(`/api/v2/vault_cards?user_id=${encodeURIComponent(input.user)}`, input.signal);
1416
1548
  const cards = Array.isArray(list?.data) ? list.data : [];
@@ -1418,7 +1550,7 @@ export class VaultClient {
1418
1550
  if (!card || typeof card !== 'object' || typeof card.id !== 'string' || !card.id)
1419
1551
  throw new PlaceholderCardError('card_not_found');
1420
1552
  const brand = typeof card.brand === 'string' ? card.brand.toLowerCase() : '';
1421
- const number = Object.prototype.hasOwnProperty.call(PLACEHOLDER_NUMBERS, brand) ? PLACEHOLDER_NUMBERS[brand] : undefined;
1553
+ const number = placeholderNumber(brand, input.psp);
1422
1554
  if (!number)
1423
1555
  throw new PlaceholderCardError('brand_unsupported');
1424
1556
  const month = card.expiry_month, year = card.expiry_year;
@@ -1507,6 +1639,12 @@ export class VaultClient {
1507
1639
  const amadeus = input.psp === AMADEUS_CHECKOUT_PSP;
1508
1640
  if (amadeus && !input.cardId)
1509
1641
  throw fail('card_required');
1642
+ // Paddle VGS: Paddle's /pay sends the expiry and the card network the agent typed into Paddle's form
1643
+ // (placeholderCard) beside the card the device stores, so the preparation names that card, and the API
1644
+ // approves no other; refusing here asks nobody to approve a preparation that could never bind.
1645
+ const paddle = input.psp === PADDLE_VGS_PSP;
1646
+ if (paddle && !input.cardId)
1647
+ throw fail('card_required');
1510
1648
  // CyberSource Flex, tokenized before Pay: the loaded card field's capture context and fields,
1511
1649
  // verified here against the keys payment-core pins before anything is created (the API and the
1512
1650
  // cardholder's device verify them again).
@@ -1631,6 +1769,8 @@ export class VaultClient {
1631
1769
  || (secureAcceptance && (state.secure_acceptance_profile_id !== secureAcceptanceProfileId || state.card_id !== input.cardId))
1632
1770
  // Amadeus: the card the cardholder approved is the one whose expiry the agent typed.
1633
1771
  || (amadeus && state.card_id !== input.cardId)
1772
+ // Paddle VGS: the card the cardholder approved is the one whose facts the agent typed.
1773
+ || (paddle && state.card_id !== input.cardId)
1634
1774
  // The API says a preparation tokenizes before Pay exactly when this one carried a capture context.
1635
1775
  || (state.tokenize_before_pay === true) !== !!flexContext)
1636
1776
  throw fail('ready_unconfirmed', id);
@@ -1675,6 +1815,7 @@ export class VaultClient {
1675
1815
  && ((error.status >= 400 && error.status < 500 && (PREPARATION_REFUSALS.includes(error.code)
1676
1816
  || (input.psp === 'fiserv' && FISERV_PREPARATION_REFUSALS.includes(error.code))
1677
1817
  || (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))
1818
+ || (input.psp === PADDLE_VGS_PSP && PADDLE_VGS_PREPARATION_REFUSALS.includes(error.code))
1678
1819
  || (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
1679
1820
  || (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
1680
1821
  throw fail(error.code);
@@ -2104,9 +2245,12 @@ export class VaultClient {
2104
2245
  // A prepared CyberSource Secure Acceptance token create too, for 30 seconds:
2105
2246
  // the merchant's page asks its frame for the reply for 30 seconds from Save,
2106
2247
  // and the API names 500 ms as such a row's poll interval.
2248
+ // A prepared Paddle VGS card request too, for 20 seconds: Paddle's frame aborts it 10 seconds after
2249
+ // Pay and sends it once more, and the API names 500 ms as such a row's poll interval.
2107
2250
  const preparedFastPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000
2108
- : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000
2109
- : preparation?.psp === SECURE_ACCEPTANCE_PSP ? Date.now() + SECURE_ACCEPTANCE_FAST_POLL_MS : 0;
2251
+ : preparation?.psp === PADDLE_VGS_PSP ? Date.now() + 20_000
2252
+ : preparation?.psp === EPROTECT_PSP ? Date.now() + 15_000
2253
+ : preparation?.psp === SECURE_ACCEPTANCE_PSP ? Date.now() + SECURE_ACCEPTANCE_FAST_POLL_MS : 0;
2110
2254
  // A preparation that tokenized before Pay: the cardholder's device already holds CyberSource's
2111
2255
  // token and approves as soon as this bind reaches it, while the page may give createToken only
2112
2256
  // seconds (Alaska Airlines' flight checkout gives it three). Poll every 100 ms for 10 seconds,
@@ -2652,11 +2796,12 @@ export class VaultClient {
2652
2796
  * approval produced: a SetupIntent confirm that carries no card and pays with
2653
2797
  * exactly the approved payment method (the page's own, or written by the
2654
2798
  * adapters in place of a card the confirm carries again).
2655
- * The API allows a card save only for a company Agentcard turned it on for,
2656
- * for an approval given on the Vault page, not by voice, with no amount, and
2657
- * a SetupIntent Stripe reports still confirmable on the same Stripe account;
2658
- * any other answer rejects with a CheckoutApiError whose code says why (for
2659
- * example `continuation_invalid` where it is off, `amount_mismatch`). Nothing
2799
+ * The API allows a card save for any company, for an approval given on the
2800
+ * Vault page, not by voice, with no amount, and a SetupIntent Stripe reports
2801
+ * still confirmable on the same Stripe account; any other answer rejects with
2802
+ * a CheckoutApiError whose code says why (for example `amount_mismatch`, or
2803
+ * `continuation_invalid` for a confirm that is not a card-free SetupIntent
2804
+ * confirm). Nothing
2660
2805
  * is charged: the adapters continue the page's request once this resolves.
2661
2806
  */
2662
2807
  async checkStripeSetupContinuation(authorizationId, request) {
@@ -7,9 +7,12 @@ export declare const CONVERGE_CAPABILITY = "converge_hpp_process";
7
7
  * the device refuses with, so an integrator reads one code whichever of them refused.
8
8
  * - A shape the Vault never pays: `converge_wallet_refused`, `converge_debit_card_refused`,
9
9
  * `converge_push_to_card_refused`, `converge_gift_card_refused`, `converge_payment_type_refused`,
10
- * `converge_captcha_refused`, `converge_service_fee_refused`, `converge_recurring_refused`,
10
+ * `converge_service_fee_refused`, `converge_recurring_refused`,
11
11
  * `converge_card_saving_refused` (ssl_get_token or ssl_add_token), `converge_amount_add_on_refused`
12
12
  * (a tip or cash back) and `converge_currency_refused` (anything but US dollars).
13
+ * - A CAPTCHA answer Google never issued: `converge_captcha_refused` (a captchaToken that is not a
14
+ * non-empty string of at most 4,096 URL-safe characters). A page with its CAPTCHA on pauses like
15
+ * any other once a person solved it, and the answer rides to Converge unchanged.
13
16
  * - A follow-up after a step the Vault cannot complete: `converge_3ds_follow_up_refused` (3-D
14
17
  * Secure or Pay Defense) and `converge_surcharge_refused`.
15
18
  * - A body the cardholder has nothing to approve on: `converge_amount_missing` and
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 009a8b902de51c4dadddae592b32d2edbd5b394f130938e1a41cd62a06a234a8
3
+ // artifact-sha256: 136e1c2b0fd2bdee0cfbf5efbbfaf1a92ace7d1cc52e17a3fd5a4584370e5e53
4
4
  // src/elavon-converge.js
5
5
  var CONVERGE_HOSTS = /* @__PURE__ */ Object.freeze(["api.convergepay.com", "api.demo.convergepay.com", "www.convergepay.com"]);
6
6
  var CONVERGE_DEMO_HOST = "api.demo.convergepay.com";
@@ -77,6 +77,7 @@ var ONE_TIME_RECURRING_FLAG = "false";
77
77
  var CARD_SAVING_FLAGS = /* @__PURE__ */ Object.freeze(["ssl_get_token", "ssl_add_token"]);
78
78
  var CREDENTIAL_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_account_id", "ssl_merchant_id", "ssl_user_id", "ssl_pin", "ssl_vendor_id"]);
79
79
  var AMOUNT_ADD_ON_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_tip_amount", "ssl_cashback_amount"]);
80
+ var SOLVED_CAPTCHA = /^[A-Za-z0-9_-]{1,4096}$/;
80
81
  var CARD_NUMBER = /^\d{12,19}$/;
81
82
  var EXPIRY_MMYY = /^(0[1-9]|1[0-2])\d{2}$/;
82
83
  var CVC = /^\d{3,4}$/;
@@ -132,7 +133,7 @@ function convergeBodyVerdict(body) {
132
133
  return refuse("converge_credential_refused");
133
134
  if (WALLET_FIELDS.some((key) => owns(data, key)))
134
135
  return refuse("converge_wallet_refused");
135
- if (owns(parsed, "captchaToken"))
136
+ if (owns(parsed, "captchaToken") && !(typeof parsed.captchaToken === "string" && SOLVED_CAPTCHA.test(parsed.captchaToken)))
136
137
  return refuse("converge_captcha_refused");
137
138
  if (SERVICE_FEE_FIELDS.some((key) => owns(data, key)))
138
139
  return refuse("converge_service_fee_refused");
@@ -25,9 +25,12 @@ export const CONVERGE_CAPABILITY = 'converge_hpp_process';
25
25
  * the device refuses with, so an integrator reads one code whichever of them refused.
26
26
  * - A shape the Vault never pays: `converge_wallet_refused`, `converge_debit_card_refused`,
27
27
  * `converge_push_to_card_refused`, `converge_gift_card_refused`, `converge_payment_type_refused`,
28
- * `converge_captcha_refused`, `converge_service_fee_refused`, `converge_recurring_refused`,
28
+ * `converge_service_fee_refused`, `converge_recurring_refused`,
29
29
  * `converge_card_saving_refused` (ssl_get_token or ssl_add_token), `converge_amount_add_on_refused`
30
30
  * (a tip or cash back) and `converge_currency_refused` (anything but US dollars).
31
+ * - A CAPTCHA answer Google never issued: `converge_captcha_refused` (a captchaToken that is not a
32
+ * non-empty string of at most 4,096 URL-safe characters). A page with its CAPTCHA on pauses like
33
+ * any other once a person solved it, and the answer rides to Converge unchanged.
31
34
  * - A follow-up after a step the Vault cannot complete: `converge_3ds_follow_up_refused` (3-D
32
35
  * Secure or Pay Defense) and `converge_surcharge_refused`.
33
36
  * - A body the cardholder has nothing to approve on: `converge_amount_missing` and
@@ -106,7 +106,14 @@ export interface CheckoutController {
106
106
  resume(result: ResumeResult): Promise<void>;
107
107
  /** Await device consent before the caller starts the first native Pay action. One use per attachment. */
108
108
  prepare(options: PrepareCheckoutOptions): Promise<PreparedCheckout>;
109
- /** Stop locally and best-effort retire an unbound preparation. Does not cancel a processor payment. */
109
+ /**
110
+ * Stop locally and best-effort retire an unbound preparation. Does not cancel a processor payment.
111
+ * A checkout that already ended keeps its state and reason (`completed`, `declined`, `failed`,
112
+ * `timed_out`, `unsupported`, `cancelled`, `outcome_unknown`) once no card request is in flight, or
113
+ * once the request in flight has its own outcome recorded (a cancel from onStateChange or the `failed` event).
114
+ * Any other reads `outcome_unknown` once a card request is in flight or an authorization exists,
115
+ * and `cancelled` before, with reason `local_cancel`.
116
+ */
110
117
  cancel(): void;
111
118
  /** Ask the application's merchant resolver. A rejection records unknown; never automatically retries payment. */
112
119
  reconcile(): Promise<Readonly<CheckoutState>>;
@@ -131,6 +138,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
131
138
  private state;
132
139
  private held;
133
140
  private active;
141
+ private settled;
134
142
  private cancelled;
135
143
  private merchantAborted;
136
144
  private unboundStripeToken;
package/dist/lifecycle.js CHANGED
@@ -2,6 +2,20 @@ import { AdyenTestPlatformRefusedError, ApprovalDeclinedError, ApprovalTimeoutEr
2
2
  import { CardEndpointRefusedError } from './card-endpoint.js';
3
3
  import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
4
4
  import { isWspayChallenge } from './wspay.js';
5
+ /**
6
+ * The statuses that end an attempt: its outcome is known (`completed`, `declined`, `failed`,
7
+ * `timed_out`, `unsupported`, `cancelled`), or the SDK already reported it unknown with its own
8
+ * reason (`outcome_unknown`). cancel() stops and holds the attachment but leaves these as they are,
9
+ * so an application that cancels at cleanup still reads the outcome, once no card request is in
10
+ * flight or the request in flight has its own outcome recorded. A checkout not started (`idle`) or
11
+ * still under way (`awaiting_approval`, `ready_to_submit`, `awaiting_merchant`, `requires_user_action`,
12
+ * or any status while a request whose outcome is not recorded is in flight) has none:
13
+ * cancelling it reads `outcome_unknown` once a request is in flight or an authorization exists,
14
+ * since a card or a token may still move, and `cancelled` before.
15
+ */
16
+ const FINAL_STATUSES = new Set([
17
+ 'completed', 'declined', 'failed', 'timed_out', 'unsupported', 'cancelled', 'outcome_unknown',
18
+ ]);
5
19
  /** How many times one approval's card-free Stripe confirmation may be checked before it is simply held. */
6
20
  export const MAX_STRIPE_CONTINUATION_CHECKS = 3;
7
21
  /**
@@ -29,6 +43,10 @@ export class CheckoutLifecycle {
29
43
  state = { status: 'idle', authorizationId: null };
30
44
  held = false;
31
45
  active = false;
46
+ // The card request in flight has its own outcome recorded (failed(), handedOff(), merchantNeverRetried()), and
47
+ // the adapter has yet to call end(): it still answers the page and tidies up. An application that cancels
48
+ // from onStateChange or the `failed` event runs in that window, and the request waits on nothing there.
49
+ settled = false;
32
50
  cancelled = false;
33
51
  merchantAborted = false;
34
52
  unboundStripeToken = false;
@@ -113,6 +131,7 @@ export class CheckoutLifecycle {
113
131
  }
114
132
  begin() {
115
133
  this.active = true;
134
+ this.settled = false;
116
135
  this.set({ status: 'awaiting_approval', authorizationId: null, ...(this.state.preparationId ? { preparationId: this.state.preparationId } : {}) });
117
136
  }
118
137
  end() { this.active = false; }
@@ -137,9 +156,20 @@ export class CheckoutLifecycle {
137
156
  catch { /* observer only */ }
138
157
  }
139
158
  cancel() {
159
+ // A checkout that already ended keeps its outcome: a refusal the processor gave, a payment the
160
+ // merchant confirmed, an unknown the SDK already named. Stopping the attachment afterwards
161
+ // changes nothing that happened, so only an attempt still under way reads as stopped here.
162
+ // A card request still in flight is under way whatever the state says: its authorization can
163
+ // still be approved on the cardholder's device, so cancelling it reads outcome_unknown as before.
164
+ // One whose own outcome is recorded (settled) is not: the adapter only has to finish answering
165
+ // the page, so a cancel from onStateChange or the `failed` event keeps that outcome.
166
+ // Read before the abort, so the state the application saw decides.
167
+ const ended = FINAL_STATUSES.has(this.state.status) && (!this.active || this.settled);
140
168
  this.cancelled = true;
141
169
  this.held = true;
142
170
  this.abort.abort();
171
+ if (ended)
172
+ return;
143
173
  this.set({ ...this.state, status: this.active || this.state.authorizationId ? 'outcome_unknown' : 'cancelled', reason: 'local_cancel' });
144
174
  }
145
175
  /** The exact browser request is gone; a late approval cannot reopen it. */
@@ -180,6 +210,7 @@ export class CheckoutLifecycle {
180
210
  merchantNeverRetried(authorizationId) {
181
211
  if (this.cancelled)
182
212
  return;
213
+ this.settled = true;
183
214
  this.set({ ...this.state, authorizationId, status: 'declined', reason: 'merchant_never_retried' });
184
215
  }
185
216
  /**
@@ -324,6 +355,7 @@ export class CheckoutLifecycle {
324
355
  handedOff(replay) {
325
356
  if (this.merchantAborted)
326
357
  return;
358
+ this.settled = true;
327
359
  if (this.ownedShopOrderId) {
328
360
  this.held = true;
329
361
  this.set({ status: 'completed', authorizationId: replay.authorizationId, mode: 'token', orderId: this.ownedShopOrderId });
@@ -359,6 +391,8 @@ export class CheckoutLifecycle {
359
391
  failed(error, handoffStarted = false) {
360
392
  if (this.cancelled)
361
393
  return;
394
+ // Before any state is set, so an observer that cancels from onStateChange sees the outcome as recorded.
395
+ this.settled = true;
362
396
  if (this.merchantAborted) {
363
397
  this.merchantRequestAborted();
364
398
  return;
@@ -0,0 +1,50 @@
1
+ declare var PADDLE_VGS_PSP: string;
2
+ declare var PADDLE_VGS_CAPABILITY: string;
3
+ declare var PADDLE_VGS_OPERATION: string;
4
+ declare var PADDLE_VGS_CONTENT_TYPE: string;
5
+ declare var PADDLE_VGS_CARDS_URLS: Readonly<{
6
+ production: "https://vault.paddle.com/cards";
7
+ sandbox: "https://sandbox-vault.paddle.com/cards";
8
+ }>;
9
+ declare var PADDLE_VGS_REFUSAL_CODES: readonly string[];
10
+ declare function paddleVgsEnvironment(url: any): "production" | "sandbox" | null;
11
+ declare function isPaddleVgsCardsUrl(url: any): boolean;
12
+ declare function paddleVgsBodyVerdict(body: any): Readonly<{
13
+ verdict: "refuse";
14
+ code: any;
15
+ }> | Readonly<{
16
+ verdict: "card";
17
+ }>;
18
+ declare function paddleVgsRequestRefusal(request: any): any;
19
+ declare function isPreparedPaddleVgsRequest(url: any, method: any, body: any, environment: any, headers: any): boolean;
20
+ declare var PADDLE_VGS_MERCHANT_PINS: readonly any[];
21
+ declare function paddleVgsMerchantPin(merchantOrigin: any, pins?: readonly any[]): any;
22
+ declare function paddleVgsMerchantRefusal(environment: any, merchantOrigin: any, authorization: any, pins?: readonly any[]): "unsupported_environment" | "paddle_vgs_merchant_unreviewed" | "paddle_vgs_seller_unreviewed" | null;
23
+ declare function paddleVgsSellerClaimsPinned(pins?: readonly any[]): boolean;
24
+ declare function paddleVgsRefusalMessage(code: any): any;
25
+ declare var PADDLE_VGS_CARD_NETWORKS: readonly string[];
26
+ declare function paddleVgsCardRefusal(body: any, card: any): any;
27
+ declare function paddleVgsStoredCardRefusal(brand: any): "paddle_vgs_card_unsupported" | null;
28
+ declare function paddleVgsTypedCardRefusal(body: any, stored: any): any;
29
+ declare function injectPaddleVgsCard(body: any, card: any): {
30
+ body: string;
31
+ where: string[];
32
+ };
33
+ declare function paddleVgsRefusalCode(text: any): string | null;
34
+ declare function paddleVgsAnswerOutcome(status: any, text: any, card: any): {
35
+ outcome: string;
36
+ reason: string;
37
+ code?: undefined;
38
+ body?: undefined;
39
+ } | {
40
+ outcome: string;
41
+ code: string;
42
+ reason?: undefined;
43
+ body?: undefined;
44
+ } | {
45
+ outcome: string;
46
+ body: string;
47
+ reason?: undefined;
48
+ code?: undefined;
49
+ };
50
+ export { PADDLE_VGS_CAPABILITY, PADDLE_VGS_CARDS_URLS, PADDLE_VGS_CARD_NETWORKS, PADDLE_VGS_CONTENT_TYPE, PADDLE_VGS_MERCHANT_PINS, PADDLE_VGS_OPERATION, PADDLE_VGS_PSP, PADDLE_VGS_REFUSAL_CODES, injectPaddleVgsCard, isPaddleVgsCardsUrl, isPreparedPaddleVgsRequest, paddleVgsAnswerOutcome, paddleVgsBodyVerdict, paddleVgsCardRefusal, paddleVgsEnvironment, paddleVgsMerchantPin, paddleVgsMerchantRefusal, paddleVgsRefusalCode, paddleVgsRefusalMessage, paddleVgsRequestRefusal, paddleVgsSellerClaimsPinned, paddleVgsStoredCardRefusal, paddleVgsTypedCardRefusal };