@agent-cards/sdk 0.23.14 → 0.23.16

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
@@ -16,6 +16,7 @@ import { SHIJI_STS_CAPABILITY, SHIJI_STS_PSP, isShijiStsServedCompanion, isShiji
16
16
  import { AMADEUS_CHECKOUT_CAPABILITY, AMADEUS_CHECKOUT_PSP } from './amadeus-checkout.generated.js';
17
17
  import { PADDLE_VGS_CAPABILITY, PADDLE_VGS_PSP, paddleVgsCardFactsOf } from './paddle-vgs.generated.js';
18
18
  import { PADDLE_TOKENEX_CAPABILITY, PADDLE_TOKENEX_PSP, isPaddleTokenexServedFacts, isPaddleTokenexServedSubstitution } from './paddle-tokenex.js';
19
+ import { WIX_PAYMENTS_CAPABILITY, WIX_PAYMENTS_PSP } from './wix-payments.generated.js';
19
20
  import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
20
21
  import { VoiceModule } from './voice.js';
21
22
  import { BuyModule } from './buy.js';
@@ -224,9 +225,17 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
224
225
  * card tab then runs in the agent's browser. The cardholder's device sends the card through the
225
226
  * checkout relay. Served on the same terms: only to a build that lists it, for a company
226
227
  * Agentcard turned Payfast on for.
228
+ *
229
+ * `wix_payments_tokenize`: the card request Wix Cashier's card frame sends to Wix's
230
+ * tokenizer (cashier-pci.wixapps.net), only through a prepared checkout: Wix's payment
231
+ * widget fails the payment 30 seconds after it asks the frame for a token. The
232
+ * cardholder's device sends the card through the checkout relay and the frame gets back
233
+ * Wix's token. A second card request from the frame, which Wix's frame sends after a 401
234
+ * or 403 under a fresh instance, is never answered from the same approval. Served on the
235
+ * same terms: only to a build that lists it, for a company Agentcard turned Wix on for.
227
236
  */
228
237
  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, PADDLE_TOKENEX_CAPABILITY,
229
- 'redsys_hosted_payment_page', 'lyra_krypton_payment', 'payfast_card_pay'];
238
+ 'redsys_hosted_payment_page', 'lyra_krypton_payment', 'payfast_card_pay', WIX_PAYMENTS_CAPABILITY];
230
239
  /**
231
240
  * Processors whose paused request names no amount at all, so the pair the
232
241
  * agent passes is the amount the cardholder approves: authorize() refuses one
@@ -252,14 +261,16 @@ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CY
252
261
  * which the prepared checkout carries, is the total the cardholder approves. Payfast's
253
262
  * card pay carries none either: the merchant's entry bound the total to the payment,
254
263
  * so the agent sends the total Payfast's page shows, above zero (PAYFAST_PSP below),
255
- * and the API holds it to the payment Payfast itself reports.
264
+ * and the API holds it to the payment Payfast itself reports. Wix's tokenize request
265
+ * carries none either: the merchant's page charges Wix's token for its own order, so the
266
+ * agent's pair, which the prepared checkout carries, is the total the cardholder approves.
256
267
  */
257
268
  const MONERIS_PSP = 'moneris';
258
269
  const PAYFAST_PSP = 'payfast';
259
- const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP, PADDLE_VGS_PSP, PAYFAST_PSP];
270
+ const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP, PADDLE_VGS_PSP, PAYFAST_PSP, WIX_PAYMENTS_PSP];
260
271
  /** How AmountRequiredError names a processor. */
261
272
  const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO', [PELECARD_PSP]: 'Pelecard', [WSPAY_PSP]: 'WSPay',
262
- [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle', [PAYFAST_PSP]: 'Payfast' };
273
+ [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris', [PADDLE_VGS_PSP]: 'Paddle', [PAYFAST_PSP]: 'Payfast', [WIX_PAYMENTS_PSP]: 'Wix' };
263
274
  /**
264
275
  * Processors whose request and answer name no amount at all, so the total the
265
276
  * payment page shows (`pageAmount`) is the one figure that ties the agent's pair
@@ -1091,6 +1102,19 @@ const LYRA_SESSION = /;jsessionid=[^/;?#]*/gi;
1091
1102
  function maskLyraSession(path) {
1092
1103
  return path.replace(LYRA_SESSION, ';jsessionid={session}');
1093
1104
  }
1105
+ /**
1106
+ * PayMongo's hosted checkout sends its card form's attach to its own page URL,
1107
+ * https://checkout.paymongo.com/<session> (payment-core paymongo.js), and the session id is the only
1108
+ * credential there, no cookies: with it anyone can open that checkout, its amount and its merchant, and
1109
+ * pay it. Every route of the host is a session page, so on it any run of 24 or more letters and digits,
1110
+ * with or without its cs_ prefix, in any case, after an escaped slash or with any character
1111
+ * percent-encoded (a path keeps %36 where it means 6), reads {session}.
1112
+ */
1113
+ const PAYMONGO_CHECKOUT_HOST = /^checkout\.paymongo\.com\.?$/i;
1114
+ const PAYMONGO_SESSION = /(?:cs_)?(?:[A-Za-z0-9]|%[0-9A-Fa-f]{2}){24,}/gi;
1115
+ function maskPaymongoSession(path) {
1116
+ return path.replace(PAYMONGO_SESSION, '{session}');
1117
+ }
1094
1118
  /**
1095
1119
  * The processors whose card request carries its own credential in the URL path, with the hosts
1096
1120
  * that send it, the text that names those hosts in a string that is not a URL, and the mask. The
@@ -1108,6 +1132,17 @@ const REDSYS_SESSION = /((?:;|%3b)jsessionid(?:=|%3d))[^/?#;]*/gi;
1108
1132
  function maskRedsysSession(path) {
1109
1133
  return path.replace(REDSYS_SESSION, '$1{session}');
1110
1134
  }
1135
+ /**
1136
+ * A PayPal order id opens PayPal's own checkout page for that order (/checkoutnow?token=<order id>) to
1137
+ * anyone who holds it, and PayPal's card confirms carry it in the path:
1138
+ * /v2/checkout/orders/<order id>/confirm-payment-source on www, api-m and cors.api. On PayPal's hosts the
1139
+ * segment after /orders/, in any case or after an escaped slash, reads {order}.
1140
+ */
1141
+ const PAYPAL_HOST = /(?:^|\.)paypal\.com\.?$/i;
1142
+ const PAYPAL_ORDER = /(\/orders(?:\/|%2f))[^/]*/gi;
1143
+ function maskPaypalOrder(path) {
1144
+ return path.replace(PAYPAL_ORDER, '$1{order}');
1145
+ }
1111
1146
  const PATH_CREDENTIALS = [
1112
1147
  { host: CREDITGUARD_HOST, named: /creditguard\.co\.il/i, mask: maskCreditguardTxId },
1113
1148
  { host: GRAVY_HOST, named: /gr4vy\.app/i, mask: maskGravySession },
@@ -1118,6 +1153,8 @@ const PATH_CREDENTIALS = [
1118
1153
  { host: PADDLE_CHECKOUT_HOST, named: /checkout-service\.paddle\.com/i, mask: maskPaddleCheckout },
1119
1154
  { host: REDSYS_HOST, named: /redsys\.es/i, mask: maskRedsysSession },
1120
1155
  { host: LYRA_HOST, named: /(?:payzen\.eu|systempay\.fr|societegenerale\.eu|lyra\.com)/i, mask: maskLyraSession },
1156
+ { host: PAYPAL_HOST, named: /paypal\.com/i, mask: maskPaypalOrder },
1157
+ { host: PAYMONGO_CHECKOUT_HOST, named: /checkout\.paymongo\.com/i, mask: maskPaymongoSession },
1121
1158
  ];
1122
1159
  /**
1123
1160
  * A URL as it may appear in an error message: origin + path only. A paused
@@ -1138,6 +1175,9 @@ const PATH_CREDENTIALS = [
1138
1175
  * On Redsys's hosts the session id after `;jsessionid=` reads `{session}`:
1139
1176
  * https://sis.redsys.es/sis/pagoTarjeta;jsessionid={session}.
1140
1177
  * On Lyra's API hosts the form token's session id reads `{session}`.
1178
+ * On PayPal's hosts an order id reads `{order}`:
1179
+ * https://www.paypal.com/v2/checkout/orders/{order}/confirm-payment-source.
1180
+ * On PayMongo's checkout host the session id reads `{session}`: https://checkout.paymongo.com/{session}.
1141
1181
  * A string that is not a URL keeps the part before any `?` or `#`, with the
1142
1182
  * same segments masked.
1143
1183
  */
@@ -1226,17 +1266,26 @@ const SECURE_ACCEPTANCE_PREPARATION_REFUSALS = [
1226
1266
  const PADDLE_VGS_PREPARATION_REFUSALS = [
1227
1267
  'processor_unavailable', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'merchant_profile_mismatch', 'paddle_vgs_card_unsupported',
1228
1268
  ];
1269
+ /**
1270
+ * The API's refusals of a Wix Payments preparation, before any preparation exists, that prepareCheckout
1271
+ * surfaces by name: the lane is held to Agentcard's own account (`processor_unavailable`), a test client
1272
+ * asked for it (`live_host_on_sandbox_row`: Wix has no test environment), and a checkout's merchant is not
1273
+ * the name the reviewed store pin carries (`merchant_profile_mismatch`). An unreviewed store answers
1274
+ * `unsupported_checkout`, which PREPARATION_REFUSALS names for every checkout. Only for a Wix checkout.
1275
+ */
1276
+ const WIX_PAYMENTS_PREPARATION_REFUSALS = ['processor_unavailable', 'live_host_on_sandbox_row', 'merchant_profile_mismatch'];
1229
1277
  /**
1230
1278
  * The reason the cardholder's device declines a bound prepared row with, per processor, when its own
1231
1279
  * send boundary passed before the card left it (the Vault's sendWindow, 12 seconds after it last saw
1232
- * the checkout unbound): a prepared Amadeus SDK add and a prepared Secure Acceptance token create. It
1233
- * names the same known non-payment the adapters' own send window closes on (amadeus.ts), which can
1234
- * close later, so authorize() ends the attempt the way that window does, as a CheckoutPreparationError
1235
- * with that reason, and never as the cardholder's "no".
1280
+ * the checkout unbound): a prepared Amadeus SDK add, a prepared Secure Acceptance token create and a
1281
+ * prepared Wix tokenize request. It names the same known non-payment the adapters' own send window
1282
+ * closes on (amadeus.ts), which can close later, so authorize() ends the attempt the way that window
1283
+ * does, as a CheckoutPreparationError with that reason, and never as the cardholder's "no".
1236
1284
  */
1237
1285
  const PREPARED_SEND_WINDOW_DECLINES = Object.freeze({
1238
1286
  amadeus_checkout: 'amadeus_send_window_closed',
1239
1287
  [SECURE_ACCEPTANCE_PSP]: 'secure_acceptance_send_window_closed',
1288
+ [WIX_PAYMENTS_PSP]: 'wix_payments_send_window_closed',
1240
1289
  });
1241
1290
  /**
1242
1291
  * Refusals a CyberSource Flex preparation that tokenizes before Pay answers (4xx) before any
@@ -1918,6 +1967,7 @@ export class VaultClient {
1918
1967
  || (input.psp === 'fiserv' && FISERV_PREPARATION_REFUSALS.includes(error.code))
1919
1968
  || (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))
1920
1969
  || (input.psp === PADDLE_VGS_PSP && PADDLE_VGS_PREPARATION_REFUSALS.includes(error.code))
1970
+ || (input.psp === WIX_PAYMENTS_PSP && WIX_PAYMENTS_PREPARATION_REFUSALS.includes(error.code))
1921
1971
  || (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
1922
1972
  || (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
1923
1973
  throw fail(error.code);
@@ -5,6 +5,7 @@ import type { Substitutions } from './substitute.js';
5
5
  import type { GmoUnsupportedReason } from './gmo.js';
6
6
  import type { ShijiStsUnsupportedReason } from './shiji-sts.js';
7
7
  import type { PaddleVgsPayRefusal } from './paddle-vgs-pay.js';
8
+ import type { PaypalCardFormReason } from './paypal-card-forms.js';
8
9
  /** The application's resolver confirmed the payment for this checkout authorization. */
9
10
  export interface MerchantPaymentConfirmation {
10
11
  kind: 'merchant_payment';
@@ -263,6 +264,9 @@ export declare class CheckoutLifecycle implements CheckoutController {
263
264
  * the cardholder's device can fill (processor_card_page: NMI's), a card
264
265
  * sent to PayPal's legacy Hosted Fields confirm, which no Vault mode takes
265
266
  * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts), a card
267
+ * sent through PayPal's Card Fields with no recognizer to pay it, or through
268
+ * PayPal's guest card form (unsupported_paypal_card_fields and
269
+ * unsupported_paypal_guest_checkout, paypal-card-forms.ts), or a card
266
270
  * a merchant's page posts to CyberSource Secure Acceptance's Silent Order
267
271
  * POST, which no Vault mode takes over either
268
272
  * (unsupported_cybersource_secure_acceptance, cybersource-secure-acceptance.ts), a
@@ -273,7 +277,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
273
277
  * send it (unsupported_ebanx_card_resend, ebanx.ts): once an approval exists the
274
278
  * state keeps it, and only the hold changes.
275
279
  */
276
- unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields' | 'unsupported_cybersource_secure_acceptance' | PaddleVgsPayRefusal | 'unsupported_ebanx_card_resend'): void;
280
+ unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields' | PaypalCardFormReason | 'unsupported_cybersource_secure_acceptance' | PaddleVgsPayRefusal | 'unsupported_ebanx_card_resend'): void;
277
281
  /**
278
282
  * A card request that only a prepared checkout pays (an Adyen merchant
279
283
  * profile's, or a Fiserv card capture) arrived with no preparation on this
package/dist/lifecycle.js CHANGED
@@ -263,6 +263,9 @@ export class CheckoutLifecycle {
263
263
  * the cardholder's device can fill (processor_card_page: NMI's), a card
264
264
  * sent to PayPal's legacy Hosted Fields confirm, which no Vault mode takes
265
265
  * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts), a card
266
+ * sent through PayPal's Card Fields with no recognizer to pay it, or through
267
+ * PayPal's guest card form (unsupported_paypal_card_fields and
268
+ * unsupported_paypal_guest_checkout, paypal-card-forms.ts), or a card
266
269
  * a merchant's page posts to CyberSource Secure Acceptance's Silent Order
267
270
  * POST, which no Vault mode takes over either
268
271
  * (unsupported_cybersource_secure_acceptance, cybersource-secure-acceptance.ts), a
@@ -0,0 +1,73 @@
1
+ /**
2
+ * PayPal's Card Fields and PayPal's guest card form, which this SDK refuses for every company until a
3
+ * synced recognizer takes the request.
4
+ *
5
+ * Read 2026-10-15 from PayPal's public bundles and card-free page reads (WooCommerce PayPal Payments
6
+ * 4.1.3 checkouts on PayPal JS SDK 5.0.582, card-field client 1.270.0; web-sdk v6 6.68.0):
7
+ *
8
+ * Card Fields v5 (components=card-fields): each field is a frame on www.paypal.com/smart/card-field,
9
+ * and at submit the number frame sends the typed card as plaintext JSON, a same-origin XHR:
10
+ * POST https://www.paypal.com/v2/checkout/orders/<order id>/confirm-payment-source
11
+ * {"payment_source":{"card":{"number","expiry":"YYYY-MM","security_code","name"?,"billing_address"?}}}
12
+ * Card Fields v6 (web-sdk v6): the same request, sent to api-m.paypal.com.
13
+ * Card Fields saving a card with no purchase: POST https://www.paypal.com/graphql?UpdateVaultSetupToken,
14
+ * the card at variables.paymentSource.card.
15
+ * PayPal's guest card form, behind the black "Debit or Credit Card" button: PayPal's checkout page
16
+ * sends the card as POST https://www.paypal.com/graphql?OnboardGuestMutation, the number at
17
+ * variables.card.cardNumber.
18
+ * PayPal's sandbox serves each on www.sandbox.paypal.com and api-m.sandbox.paypal.com.
19
+ *
20
+ * An SDK without this guard let the agent's placeholder card reach PayPal there, and PayPal declined it.
21
+ * Both adapters arm these paths whatever the synced registry serves, and the pre-claim gate
22
+ * (paypalCardFormsPreClaim, pre-claim.ts) judges each POST:
23
+ * - a Card Fields confirm whose body carries a number at payment_source.card.number aborts, reason
24
+ * `unsupported_paypal_card_fields`, unless the synced registry recognizes it as a card request: an
25
+ * org Agentcard turned PayPal Card Fields on for pauses and pays that confirm through the recognizer,
26
+ * which judges the body itself;
27
+ * - a GraphQL operation carrying a card aborts whatever the registry serves, since no recognizer pays
28
+ * one: the guest form's card as `unsupported_paypal_guest_checkout`, a card saved with no purchase as
29
+ * `unsupported_paypal_card_fields`;
30
+ * - a body with no card (a PayPal wallet confirm, a saved card, every other GraphQL operation the
31
+ * buttons send) continues untouched;
32
+ * - a confirm the adapter could not read aborts that request alone, since nothing proves it carries no
33
+ * card. A GraphQL post it could not read aborts only when its query names one of the two card
34
+ * operations: PayPal's buttons send many others, and failing them would break PayPal's own wallet.
35
+ * Each abort holds the attachment as every unsupported refusal does: the page's next submit would carry
36
+ * the card again, so another way to pay needs a new attachment.
37
+ *
38
+ * A guest form PayPal opens in a popup window runs in another page, which an attachment covers only when
39
+ * the agent attaches the SDK to that page as well. PayPal's BIN lookup (/v2/payments/find-bin-details)
40
+ * reads the first digits the agent types and pays nothing; neither adapter intercepts it.
41
+ */
42
+ import { paypalHostedFieldsBody } from './paypal-hosted-fields.js';
43
+ export declare const PAYPAL_CARD_FIELDS_REASON: "unsupported_paypal_card_fields";
44
+ export declare const PAYPAL_GUEST_CHECKOUT_REASON: "unsupported_paypal_guest_checkout";
45
+ export type PaypalCardFormReason = typeof PAYPAL_CARD_FIELDS_REASON | typeof PAYPAL_GUEST_CHECKOUT_REASON;
46
+ /** The hosts Card Fields sends its confirm to: v5 on www, v6 on api-m, live and sandbox. */
47
+ export declare const PAYPAL_CARD_FIELDS_HOSTS: readonly string[];
48
+ /** The hosts of PayPal's own pages, whose GraphQL endpoint takes the guest form's card and a card saved with no purchase. */
49
+ export declare const PAYPAL_GRAPHQL_HOSTS: readonly string[];
50
+ /**
51
+ * CDP Fetch patterns, armed on every attachment: the confirm on its four hosts and GraphQL on PayPal's
52
+ * two page hosts. Chrome sends the canonical URL, whose host is lower case, and the trailing `*` keeps a
53
+ * query or a trailing slash paused. Nothing else on www.paypal.com is armed.
54
+ */
55
+ export declare const PAYPAL_CARD_FORM_PATTERNS: readonly string[];
56
+ /** Whether this URL is the Card Fields confirm on one of its four hosts (https, standard port, no credentials). */
57
+ export declare function isPaypalCardFieldsConfirm(url: string): boolean;
58
+ /** Whether this URL is GraphQL on one of PayPal's two page hosts. */
59
+ export declare function isPaypalGraphql(url: string): boolean;
60
+ /** Whether this URL is any request the guard judges, for the Playwright route's predicate. */
61
+ export declare function isPaypalCardFormUrl(url: string): boolean;
62
+ /** Whether a GraphQL URL's query names one of the two card operations (`/graphql?OnboardGuestMutation`). */
63
+ export declare function paypalGraphqlNamesCardOperation(url: string): boolean;
64
+ /**
65
+ * What a GraphQL post carries, read as PayPal reads it (the last of a repeated key wins in both), for one
66
+ * operation or a batch of them: 'guest_card' when an operation's variables.card.cardNumber holds anything
67
+ * but null or an empty string, 'saved_card' when an operation's variables.paymentSource.card is an object,
68
+ * 'no_card' for any other JSON object or array of them, and 'unreadable' for a body the adapter could not
69
+ * read (null), an empty one, or one that is neither.
70
+ */
71
+ export declare function paypalGraphqlCard(body: string | null): 'guest_card' | 'saved_card' | 'no_card' | 'unreadable';
72
+ /** What a Card Fields confirm carries: its body is the Orders v2 shape Hosted Fields also sends. */
73
+ export declare const paypalCardFieldsBody: typeof paypalHostedFieldsBody;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * PayPal's Card Fields and PayPal's guest card form, which this SDK refuses for every company until a
3
+ * synced recognizer takes the request.
4
+ *
5
+ * Read 2026-10-15 from PayPal's public bundles and card-free page reads (WooCommerce PayPal Payments
6
+ * 4.1.3 checkouts on PayPal JS SDK 5.0.582, card-field client 1.270.0; web-sdk v6 6.68.0):
7
+ *
8
+ * Card Fields v5 (components=card-fields): each field is a frame on www.paypal.com/smart/card-field,
9
+ * and at submit the number frame sends the typed card as plaintext JSON, a same-origin XHR:
10
+ * POST https://www.paypal.com/v2/checkout/orders/<order id>/confirm-payment-source
11
+ * {"payment_source":{"card":{"number","expiry":"YYYY-MM","security_code","name"?,"billing_address"?}}}
12
+ * Card Fields v6 (web-sdk v6): the same request, sent to api-m.paypal.com.
13
+ * Card Fields saving a card with no purchase: POST https://www.paypal.com/graphql?UpdateVaultSetupToken,
14
+ * the card at variables.paymentSource.card.
15
+ * PayPal's guest card form, behind the black "Debit or Credit Card" button: PayPal's checkout page
16
+ * sends the card as POST https://www.paypal.com/graphql?OnboardGuestMutation, the number at
17
+ * variables.card.cardNumber.
18
+ * PayPal's sandbox serves each on www.sandbox.paypal.com and api-m.sandbox.paypal.com.
19
+ *
20
+ * An SDK without this guard let the agent's placeholder card reach PayPal there, and PayPal declined it.
21
+ * Both adapters arm these paths whatever the synced registry serves, and the pre-claim gate
22
+ * (paypalCardFormsPreClaim, pre-claim.ts) judges each POST:
23
+ * - a Card Fields confirm whose body carries a number at payment_source.card.number aborts, reason
24
+ * `unsupported_paypal_card_fields`, unless the synced registry recognizes it as a card request: an
25
+ * org Agentcard turned PayPal Card Fields on for pauses and pays that confirm through the recognizer,
26
+ * which judges the body itself;
27
+ * - a GraphQL operation carrying a card aborts whatever the registry serves, since no recognizer pays
28
+ * one: the guest form's card as `unsupported_paypal_guest_checkout`, a card saved with no purchase as
29
+ * `unsupported_paypal_card_fields`;
30
+ * - a body with no card (a PayPal wallet confirm, a saved card, every other GraphQL operation the
31
+ * buttons send) continues untouched;
32
+ * - a confirm the adapter could not read aborts that request alone, since nothing proves it carries no
33
+ * card. A GraphQL post it could not read aborts only when its query names one of the two card
34
+ * operations: PayPal's buttons send many others, and failing them would break PayPal's own wallet.
35
+ * Each abort holds the attachment as every unsupported refusal does: the page's next submit would carry
36
+ * the card again, so another way to pay needs a new attachment.
37
+ *
38
+ * A guest form PayPal opens in a popup window runs in another page, which an attachment covers only when
39
+ * the agent attaches the SDK to that page as well. PayPal's BIN lookup (/v2/payments/find-bin-details)
40
+ * reads the first digits the agent types and pays nothing; neither adapter intercepts it.
41
+ */
42
+ import { paypalHostedFieldsBody } from './paypal-hosted-fields.js';
43
+ export const PAYPAL_CARD_FIELDS_REASON = 'unsupported_paypal_card_fields';
44
+ export const PAYPAL_GUEST_CHECKOUT_REASON = 'unsupported_paypal_guest_checkout';
45
+ /** The hosts Card Fields sends its confirm to: v5 on www, v6 on api-m, live and sandbox. */
46
+ export const PAYPAL_CARD_FIELDS_HOSTS = Object.freeze(['www.paypal.com', 'www.sandbox.paypal.com',
47
+ 'api-m.paypal.com', 'api-m.sandbox.paypal.com']);
48
+ /** The hosts of PayPal's own pages, whose GraphQL endpoint takes the guest form's card and a card saved with no purchase. */
49
+ export const PAYPAL_GRAPHQL_HOSTS = Object.freeze(['www.paypal.com', 'www.sandbox.paypal.com']);
50
+ const CONFIRM_PATH = /^\/v2\/checkout\/orders\/[^/]+\/confirm-payment-source\/?$/;
51
+ const GRAPHQL_PATH = /^\/graphql\/?$/;
52
+ /** The two GraphQL operations that carry a card, as PayPal's clients name them in the query string. */
53
+ const CARD_OPERATIONS = ['OnboardGuestMutation', 'UpdateVaultSetupToken'];
54
+ /**
55
+ * CDP Fetch patterns, armed on every attachment: the confirm on its four hosts and GraphQL on PayPal's
56
+ * two page hosts. Chrome sends the canonical URL, whose host is lower case, and the trailing `*` keeps a
57
+ * query or a trailing slash paused. Nothing else on www.paypal.com is armed.
58
+ */
59
+ export const PAYPAL_CARD_FORM_PATTERNS = Object.freeze([
60
+ ...PAYPAL_CARD_FIELDS_HOSTS.map((host) => `https://${host}/v2/checkout/orders/*/confirm-payment-source*`),
61
+ ...PAYPAL_GRAPHQL_HOSTS.map((host) => `https://${host}/graphql*`),
62
+ ]);
63
+ function paypalUrl(url, hosts, path) {
64
+ let parsed;
65
+ try {
66
+ parsed = new URL(url);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ return parsed.protocol === 'https:' && !parsed.port && !parsed.username && !parsed.password
72
+ && hosts.includes(parsed.hostname.toLowerCase()) && path.test(parsed.pathname) ? parsed : null;
73
+ }
74
+ /** Whether this URL is the Card Fields confirm on one of its four hosts (https, standard port, no credentials). */
75
+ export function isPaypalCardFieldsConfirm(url) {
76
+ return paypalUrl(url, PAYPAL_CARD_FIELDS_HOSTS, CONFIRM_PATH) !== null;
77
+ }
78
+ /** Whether this URL is GraphQL on one of PayPal's two page hosts. */
79
+ export function isPaypalGraphql(url) {
80
+ return paypalUrl(url, PAYPAL_GRAPHQL_HOSTS, GRAPHQL_PATH) !== null;
81
+ }
82
+ /** Whether this URL is any request the guard judges, for the Playwright route's predicate. */
83
+ export function isPaypalCardFormUrl(url) {
84
+ return isPaypalCardFieldsConfirm(url) || isPaypalGraphql(url);
85
+ }
86
+ /** Whether a GraphQL URL's query names one of the two card operations (`/graphql?OnboardGuestMutation`). */
87
+ export function paypalGraphqlNamesCardOperation(url) {
88
+ const parsed = paypalUrl(url, PAYPAL_GRAPHQL_HOSTS, GRAPHQL_PATH);
89
+ return parsed !== null && CARD_OPERATIONS.some((name) => parsed.searchParams.has(name));
90
+ }
91
+ const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
92
+ const present = (value) => value !== undefined && value !== null && value !== '';
93
+ /**
94
+ * What a GraphQL post carries, read as PayPal reads it (the last of a repeated key wins in both), for one
95
+ * operation or a batch of them: 'guest_card' when an operation's variables.card.cardNumber holds anything
96
+ * but null or an empty string, 'saved_card' when an operation's variables.paymentSource.card is an object,
97
+ * 'no_card' for any other JSON object or array of them, and 'unreadable' for a body the adapter could not
98
+ * read (null), an empty one, or one that is neither.
99
+ */
100
+ export function paypalGraphqlCard(body) {
101
+ if (typeof body !== 'string' || body === '')
102
+ return 'unreadable';
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(body);
106
+ }
107
+ catch {
108
+ return 'unreadable';
109
+ }
110
+ const operations = Array.isArray(parsed) ? parsed : [parsed];
111
+ if (operations.length === 0 || !operations.every(record))
112
+ return 'unreadable';
113
+ let carried = 'no_card';
114
+ for (const operation of operations) {
115
+ const variables = operation.variables;
116
+ if (!record(variables))
117
+ continue;
118
+ if (record(variables.card) && present(variables.card.cardNumber))
119
+ return 'guest_card';
120
+ if (record(variables.paymentSource) && record(variables.paymentSource.card))
121
+ carried = 'saved_card';
122
+ }
123
+ return carried;
124
+ }
125
+ /** What a Card Fields confirm carries: its body is the Orders v2 shape Hosted Fields also sends. */
126
+ export const paypalCardFieldsBody = paypalHostedFieldsBody;
@@ -252,6 +252,7 @@ export declare function isArmedCreditguardCall(vault: {
252
252
  export declare const ekashuPreClaim: PreClaimGate;
253
253
  export declare const redsysPreClaim: PreClaimGate;
254
254
  export declare const paypalHostedFieldsPreClaim: PreClaimGate;
255
+ export declare const paypalCardFormsPreClaim: PreClaimGate;
255
256
  export declare const cybersourceSecureAcceptancePreClaim: PreClaimGate;
256
257
  export declare const worldlineMyCheckoutPreClaim: PreClaimGate;
257
258
  export declare const paddleVgsPayPreClaim: PreClaimGate;
package/dist/pre-claim.js CHANGED
@@ -16,8 +16,9 @@
16
16
  // card-saving bodies, WSPay's confirm URL, which also carries bodies with
17
17
  // no new card, Shiji STS's getTokenId and getCvvId, which also carry
18
18
  // requests the vault must never pay, PayPal's legacy Hosted Fields
19
- // confirm and CyberSource Secure Acceptance's Silent Order POST, whose card
20
- // no Vault mode takes over, Worldline MyCheckout's card form, held to the
19
+ // confirm, PayPal's Card Fields and guest card form, and CyberSource Secure
20
+ // Acceptance's Silent Order POST, whose card no Vault mode takes over until a
21
+ // recognizer does, Worldline MyCheckout's card form, held to the
21
22
  // card product its page chose, and EBANX's 3-D Secure calls and hosted confirm,
22
23
  // which send the card again after its token and leave only from the
23
24
  // cardholder's device, under the token request's approval.
@@ -80,6 +81,7 @@ import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone
80
81
  import { ekashuEntryUrl, isEkashuEntry, isEkashuHost } from './ekashu.js';
81
82
  import { isRedsysEntry, isRedsysHost, redsysEntryUrl } from './redsys.js';
82
83
  import { PAYPAL_HOSTED_FIELDS_REASON, isPaypalHostedFieldsConfirm, paypalHostedFieldsBody } from './paypal-hosted-fields.js';
84
+ import { PAYPAL_CARD_FIELDS_REASON, PAYPAL_GUEST_CHECKOUT_REASON, isPaypalCardFieldsConfirm, isPaypalGraphql, paypalCardFieldsBody, paypalGraphqlCard, paypalGraphqlNamesCardOperation, } from './paypal-card-forms.js';
83
85
  import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, isSecureAcceptanceFramePost, } from './cybersource-secure-acceptance.js';
84
86
  import { isWorldlineMyCheckoutEndpoint } from './worldline-mycheckout.js';
85
87
  import { paddleVgsPayEventUrl } from './paddle-vgs-pay.js';
@@ -1671,6 +1673,60 @@ async function paypalHostedFieldsUnreadable(ctx) {
1671
1673
  await ctx.effects.abort();
1672
1674
  return { kind: 'abort' };
1673
1675
  }
1676
+ // PayPal's Card Fields and guest card form (paypal-card-forms.ts). Every company's adapters hand each
1677
+ // POST to the Card Fields confirm (www and api-m, live and sandbox) and to GraphQL on www (live and
1678
+ // sandbox) to this gate:
1679
+ // - a confirm with a card number at payment_source.card.number aborts as an unsupported checkout,
1680
+ // reason `unsupported_paypal_card_fields`, with no claim, create or prompt, unless the synced
1681
+ // registry recognizes it as a card request: then it defers, and the PayPal Card Fields lane pauses
1682
+ // it for the cardholder's approval, judging the body itself;
1683
+ // - a GraphQL operation with a card aborts the same way whatever the registry serves:
1684
+ // `unsupported_paypal_guest_checkout` for the guest form's variables.card.cardNumber,
1685
+ // `unsupported_paypal_card_fields` for a card saved with no purchase (variables.paymentSource.card);
1686
+ // - a confirm the adapter could not read, and a GraphQL post it could not read whose query names one
1687
+ // of the two card operations, abort that request alone with a `blocked` event,
1688
+ // `unreadable_paypal_request`, and do not hold the attachment;
1689
+ // - anything else (a PayPal wallet confirm, a saved card, the buttons' other GraphQL operations, a GET
1690
+ // or a preflight) defers, and the arbiter continues it untouched.
1691
+ // Synchronous until it must abort, like every gate. Exported for the gate registry's tests.
1692
+ export const paypalCardFormsPreClaim = (ctx) => {
1693
+ const { url, method } = ctx.request;
1694
+ if (String(method).toUpperCase() !== 'POST')
1695
+ return undefined;
1696
+ const confirm = isPaypalCardFieldsConfirm(url);
1697
+ if (!confirm && !isPaypalGraphql(url))
1698
+ return undefined;
1699
+ if (confirm && ctx.vault.isCardRequest(url, method))
1700
+ return undefined;
1701
+ let body;
1702
+ // A reader that throws (Playwright's postData() can) proves nothing either way.
1703
+ try {
1704
+ body = ctx.request.body;
1705
+ }
1706
+ catch {
1707
+ body = null;
1708
+ }
1709
+ if (confirm) {
1710
+ const carried = paypalCardFieldsBody(body);
1711
+ if (carried === 'no_card')
1712
+ return undefined;
1713
+ return carried === 'card' ? paypalCardFormAbort(ctx, PAYPAL_CARD_FIELDS_REASON) : paypalHostedFieldsUnreadable(ctx);
1714
+ }
1715
+ const carried = paypalGraphqlCard(body);
1716
+ if (carried === 'guest_card')
1717
+ return paypalCardFormAbort(ctx, PAYPAL_GUEST_CHECKOUT_REASON);
1718
+ if (carried === 'saved_card')
1719
+ return paypalCardFormAbort(ctx, PAYPAL_CARD_FIELDS_REASON);
1720
+ return carried === 'unreadable' && paypalGraphqlNamesCardOperation(url) ? paypalHostedFieldsUnreadable(ctx) : undefined;
1721
+ };
1722
+ async function paypalCardFormAbort(ctx, reason) {
1723
+ const { url, method } = ctx.request;
1724
+ ctx.preparationGate.invalidate('unsupported_checkout');
1725
+ ctx.lifecycle.unsupported(reason);
1726
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: eventUrl(ctx.vault, url), method, reason } });
1727
+ await ctx.effects.abort();
1728
+ return { kind: 'abort' };
1729
+ }
1674
1730
  // CyberSource Secure Acceptance's Silent Order POST (cybersource-secure-acceptance.ts). The merchant's
1675
1731
  // page posts the card the agent typed, beside the fields its server signed, to a /silent/ path on
1676
1732
  // CyberSource's Secure Acceptance hosts, which no Vault mode takes over, so every company's adapters
@@ -2092,7 +2148,7 @@ async function unsupportedContinue(ctx) {
2092
2148
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
2093
2149
  * pauses each one itself. */
2094
2150
  export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, redsysPreClaim, paypalHostedFieldsPreClaim,
2095
- cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim, paddleVgsPayPreClaim, ebanxPreClaim];
2151
+ paypalCardFormsPreClaim, cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim, paddleVgsPayPreClaim, ebanxPreClaim];
2096
2152
  /** Run each gate in order; the first to return a decision wins. If none does,
2097
2153
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
2098
2154
  * Returns synchronously while every gate defers synchronously (an ordinary card
@@ -20,8 +20,12 @@
20
20
  * paddle_vgs: prepared only. Paddle's checkout frame aborts its card request to its VGS card vault 10
21
21
  * seconds after Pay and sends it once more, too soon for an approval after Pay. Its environments are its
22
22
  * two card URLs: vault.paddle.com (production) and sandbox-vault.paddle.com (sandbox).
23
+ *
24
+ * wix_payments: prepared only. Wix's payment widget fails the payment 30 seconds after it asks Wix
25
+ * Cashier's card frame for a token, too soon for an approval after Pay. Its one environment is
26
+ * production: Wix's tokenizer has no test host.
23
27
  */
24
- export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance' | 'paddle_vgs';
28
+ export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance' | 'paddle_vgs' | 'wix_payments';
25
29
  export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
26
30
  /**
27
31
  * How the approved card reaches the processor: the device's own request (token), or
@@ -14,6 +14,7 @@ import { amadeusEnvironment, amadeusPreparationUrls, amadeusRequestRefusal, isPr
14
14
  import { isPreparedVerifoneRequest } from './verifone.js';
15
15
  import { isPreparedSecureAcceptanceRequest, secureAcceptanceEnvironment, secureAcceptancePreparationUrl, secureAcceptanceRequestRefusal, } from './cybersource-secure-acceptance.generated.js';
16
16
  import { PADDLE_VGS_CARDS_URLS, isPreparedPaddleVgsRequest } from './paddle-vgs.generated.js';
17
+ import { WIX_PAYMENTS_TOKENIZE_URL, isPreparedWixPaymentsRequest } from './wix-payments.generated.js';
17
18
  export function preparationMode(psp) {
18
19
  return psp === 'adyen' || psp === 'fiserv' || psp === 'verifone' ? 'cse' : 'token';
19
20
  }
@@ -53,6 +54,9 @@ export function preparedApprovalAnswersRetry(psp) {
53
54
  export function validPreparationEnvironment(psp, environment) {
54
55
  if (psp === 'bambora' || psp === 'mercado_pago' || psp === 'recurly' || psp === 'spreedly')
55
56
  return environment === 'shared';
57
+ // Wix Payments has production alone: its tokenizer has no test host.
58
+ if (psp === 'wix_payments')
59
+ return environment === 'production';
56
60
  return ['square', 'braintree', 'worldpay', 'adyen', 'checkout_com', 'paysafe', 'fiserv', 'cybersource_flex', 'aci', 'creditguard', 'worldpay_eprotect', 'amadeus_checkout', 'verifone',
57
61
  'cybersource_secure_acceptance', 'paddle_vgs'].includes(psp) && ['production', 'sandbox'].includes(environment);
58
62
  }
@@ -99,6 +103,9 @@ export function preparationEndpoint(psp, environment) {
99
103
  // Paddle VGS: the environment's one card URL, the only URL the claim takes for it.
100
104
  if (psp === 'paddle_vgs')
101
105
  return PADDLE_VGS_CARDS_URLS[environment];
106
+ // Wix Payments: the tokenize URL, the only URL the claim takes for it.
107
+ if (psp === 'wix_payments')
108
+ return WIX_PAYMENTS_TOKENIZE_URL;
102
109
  // A Verifone endpoint is its reviewed profile's leg, never one per environment (verifone.ts).
103
110
  if (psp === 'verifone')
104
111
  throw new Error('unsupported_preparation_processor');
@@ -252,6 +259,10 @@ export function matchesPreparedRequest(psp, environment, requestUrl, method, bod
252
259
  // the JSON:API content type and the frame's body), the rule the API and the Vault bind with too.
253
260
  if (psp === 'paddle_vgs')
254
261
  return isPreparedPaddleVgsRequest(requestUrl, method.toUpperCase(), body ?? null, environment, headers ?? {});
262
+ // Wix Payments: production, the tokenize URL and payment-core's whole request rule (a JSON content type, the
263
+ // site's app instance and the card frame's body), the rule the API and the Vault bind with too.
264
+ if (psp === 'wix_payments')
265
+ return isPreparedWixPaymentsRequest(requestUrl, method.toUpperCase(), body ?? null, environment, headers ?? {});
255
266
  if (psp === 'creditguard')
256
267
  return matchesPreparedCreditguard(environment, requestUrl, body, headers, binding);
257
268
  // A Verifone request is judged only through the profile its preparation names (matchesPreparation).