@agent-cards/sdk 0.23.11 → 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/CHANGELOG.md +10 -0
- package/PREFLIGHT.md +2 -0
- package/README.md +9 -7
- package/dist/card-fields.generated.js +72 -1
- package/dist/cdp.js +28 -7
- package/dist/client.d.ts +20 -6
- package/dist/client.js +127 -16
- package/dist/paddle-vgs.generated.d.ts +50 -0
- package/dist/paddle-vgs.generated.js +289 -0
- package/dist/payrails.generated.js +8 -12
- package/dist/prepared-processor.d.ts +12 -4
- package/dist/prepared-processor.js +17 -5
- package/dist/primer.generated.js +88 -17
- package/dist/stripe-checkout.generated.js +47 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Keep a Rapyd hosted page's id and a Stripe Checkout Session's id out of your events and error messages. Rapyd's checkout page sends the card to `/v1/checkout/checkout_<id>` on `api.rapyd.net` (a card page to `/v1/hosted/collect/card/hp_card_<id>/payment_method`) with no auth header, so the page's id is the only credential that request needs: anyone who holds it can read the page and post a card to it until it expires. Stripe Checkout confirms a session at `/v1/payment_pages/<session id>/confirm` with the merchant's publishable key, which is public, so the session id is what lets anyone confirm a payment on it. Earlier releases named both ids in the `card_request_paused` event and in the errors `authorize()` throws, so they reached your `onEvent` handler and every log that recorded the event or the error. `redactUrl`, every event and every error message now show them as `{token}` and `{session}`, as in `https://api.rapyd.net/v1/checkout/{token}` and `https://api.stripe.com/v1/payment_pages/{session}/confirm`. `redactUrl` also masks an ACI checkout id in the shapes it used to keep: a host with a trailing dot, an escaped or doubled slash, and a string that is not a URL. A PaymentIntent or SetupIntent id stays, since confirming one also needs its client_secret. No API or Vault release is needed.
|
|
6
|
+
|
|
7
|
+
- Keep a Gravy checkout session out of your events and error messages. Gravy's Secure Fields send the card to `/checkout/sessions/<session id>/fields` on the merchant's Gravy host with no auth header, so the session id is the only credential that request needs: anyone who holds it can write card fields into the session until it expires. Earlier releases named the full URL in the `card_request_paused` event and in the error `authorize()` throws for a request in the wrong method, so the session id reached your `onEvent` handler and every log that recorded the event or the error. `redactUrl`, every event and every error message now show it as `{session}`, as in `https://api.<merchant>.gr4vy.app/checkout/sessions/{session}/fields`. No API or Vault release is needed.
|
|
8
|
+
|
|
9
|
+
- Pay FlixBus's checkout on Payrails web SDK 6.7.0. FlixBus's page now loads web SDK 6.7.0 in place of 5.50.0, and 6.7.0's Pay now button sends the card-form payment without the `enrollInstrumentToNetworkOffers` member that 5.50.0 sent as `false`. 0.23.11 required that member and aborted every such payment as `unsupported_payrails_card_payment` before creating anything, so no FlixBus payment reached an approval. A payment that names no `enrollInstrumentToNetworkOffers` now pauses as 5.50.0's does: Payrails reads a missing flag as `false`, and 6.7.0 sends no other member in its place. A payment that sets the flag `true` or keeps the card (`storeInstrument: true`, or `meta.order.processingType` `CardOnFile` or `UnscheduledCardOnFile`) still aborts as `payrails_card_on_file_consent_required`, and the flag in another case, in another place or as anything but a boolean aborts as `unsupported_payrails_card_payment`. A payment also needs the matching API and Vault releases.
|
|
10
|
+
|
|
11
|
+
- Continue a Stripe card save for every company. The API judged a SetupIntent confirm after a `/v1/payment_methods` approval only for a company Agentcard had turned card-save continuations on for, and answered every other company `continuation_invalid`, so the SDK aborted the confirm and the page reported the card could not be added. Every company now gets the card save under the same rules: an approval the cardholder gave on the Vault page, not by voice, with no `amount` and no `currency`, paid with exactly the approved payment method on the same Stripe account, and a SetupIntent Stripe reports still confirmable. No SDK upgrade is needed: 0.22.0 and later continue the confirm once the API release deploys.
|
|
12
|
+
|
|
5
13
|
- Refuse a Worldline MyCheckout card submit whose card product is not the one its page chose. A MyCheckout page that offers several card products asks the shopper to choose one, then looks the typed number's IIN up with Worldline and switches the form to the product the answer names. With the agent's placeholder typed, that answer describes the placeholder: on 2026-10-12 CLZ's page read the Visa placeholder (411111) as Visa Electron (122) and switched the form from the Visa (1) the agent chose, and the cardholder's device, which checks only the card's network, would have submitted a Visa credit card as Visa Electron. Both adapters now remember the product each hosted checkout's select-payment-product step chose (that post carries no card and continues untouched) and abort a card submit that names another, before any create or prompt, with a `blocked` event, `worldline_mycheckout_product_changed`, or `worldline_mycheckout_product_missing` for one that names no product. Like the other per-request refusals it neither holds the attachment nor changes its state. A card-group page, and a hosted checkout whose choice the attachment never saw, are judged as before. `placeholderCard()` also takes `psp`, the processor whose card form the agent types into, so a processor can have its own placeholder for a network; none is set yet, so every processor still gets the network's default. Worldline stays dark for every company Agentcard has not turned it on for. No API or Vault release is needed.
|
|
6
14
|
|
|
7
15
|
- Keep a checkout's outcome when your code cancels the attachment after the checkout ended. `cancel()` rewrote every state: a checkout with an authorization or a card request in flight read `outcome_unknown` with reason `local_cancel`, and any other read `cancelled`, so an integration that cancels at cleanup lost a definite answer. On 2026-10-12 a Rapyd checkout the processor refused read `declined` (`processor_refused`) until the runner's cleanup cancel turned it into `outcome_unknown`; a `completed` checkout would have gone the same way. A checkout that reads `completed`, `declined`, `failed`, `timed_out`, `unsupported`, `cancelled` or `outcome_unknown` now keeps its status and reason through `cancel()` once no card request is in flight, or once the request in flight has its own outcome recorded, so an application that cancels from `onStateChange` or the `failed` event after a processor refusal reads `declined`. `cancel()` still stops polling, aborts any paused request and refuses every later card request, and reports no state change. A checkout still under way is unchanged, and so is one whose card request is still in flight with no outcome of its own, such as a request the page dropped mid-approval, since that authorization can still be approved on the cardholder's device: `outcome_unknown` with reason `local_cancel` once a card request is in flight or an authorization exists, `cancelled` before. No API or Vault release is needed.
|
|
@@ -88,6 +96,8 @@
|
|
|
88
96
|
|
|
89
97
|
- Read the card number on the D-EDGE card frame the way the frame groups each card network: 4-6-5 for an American Express number, 4-6-4 for a Diners Club number starting 36 or 300 to 305, and groups of four for any other. The SDK's copy of D-EDGE's rules read a grouped number only in groups of four, so an American Express or Diners Club placeholder as the frame groups it was not D-EDGE's reviewed card request, and the same number in groups of four, which the frame never posts, was. D-EDGE stays in `observe`, so its card post still gets the page that says Agentcard has not turned it on.
|
|
90
98
|
|
|
99
|
+
- Pay a Paddle checkout that sends the card to Paddle's card vault, from Agentcard's own account only for now (dark launch): Agentcard turns it on for another company once it has confirmed that a card stored for one Paddle seller cannot pay another. On a checkout whose card method names VGS, Paddle's checkout frame on `buy.paddle.com` posts the card to `https://vault.paddle.com/cards` (`https://sandbox-vault.paddle.com/cards` in test) and sends that request once more when it gets no answer within 10 seconds, too soon for an approval after Pay. So the card is paid only through `prepare({ psp: 'paddle_vgs', environment: 'production' })` (`'sandbox'` for Paddle's sandbox) on an attachment whose `cardId` names the card whose `placeholderCard()` facts you typed into Paddle's form; without one, `prepare()` fails with `card_required`, and the approval is locked to that card. The sync now also asks for `capabilities=paddle_vgs_cards`. Once the cardholder approves, the frame's request pauses, the cardholder's device sends the card to Paddle's vault itself, and the frame receives the stored card's id and nothing else; the frame's second request after its 10-second timeout, the same body under the same bearer, gets the same answer, and any other request is refused. Paddle's request names no amount, so the cardholder approves the `amount` and `currency` you pass. The lane pays Visa, Mastercard and American Express cards: `prepare()` fails with `paddle_vgs_card_unsupported` for a card on another network, and the create fails with `paddle_vgs_card_mismatch` when the expiry or placeholder you typed is not the card's. On Paddle's live vault, Agentcard pays only a seller it has reviewed, at the checkout page it reviewed (Rectangle Pro at `https://rectangleapp.com` today), and the approval names that seller and says it can keep the card on file, because the card stays stored in Paddle's vault. Paddle's own `/pay` call, which follows the stored card's id, still carries the placeholder's first six and last four digits that you typed; whether Paddle accepts them beside the real card is not proven yet. A Paddle checkout that sends the card through TokenEx or Spreedly is not paid. No live payment has gone through this route yet. A payment also needs the matching API and Vault releases.
|
|
100
|
+
|
|
91
101
|
## 0.23.0
|
|
92
102
|
|
|
93
103
|
### Upgrading from 0.22.0
|
package/PREFLIGHT.md
CHANGED
|
@@ -205,6 +205,8 @@ Shiji STS (`shiji_sts`), the card leg of MyCheck Wallet V3 at La Quinta by Wyndh
|
|
|
205
205
|
|
|
206
206
|
Gravy Secure Fields (`gravy`) is identified from the loader its merchants load, `/secure-fields/latest/secure-fields.js` on `cdn.<merchant>.gr4vy.app` (or `cdn.sandbox.<merchant>.gr4vy.app`), but it is not in this table. It launches dark: the direct SDK pauses Gravy's card request, a `PUT` from Gravy's controller frame, only after `syncRegistry()` and only for an org Agentcard has enabled, so the bundled catalog leaves it out and a Gravy checkout stays `unknown`.
|
|
207
207
|
|
|
208
|
+
Paddle (`paddle_vgs`) has no discovery rule in this table. Paddle's checkout frame on `buy.paddle.com` names the tokenizer it sends the card through only after the buyer's first step, and Paddle can name another one for each checkout, so nothing on the page before that step says which one a checkout uses, and a Paddle checkout stays `unknown`. The direct SDK pauses the card request a VGS checkout sends to `vault.paddle.com/cards` only after `syncRegistry()`, only through a prepared checkout, and only for an org Agentcard has enabled. Read the Paddle row of the [README](./README.md) processor table for what the SDK covers.
|
|
209
|
+
|
|
208
210
|
The bundled direct SDK profile declares processor support for `one_time` purchases. The catalog carries each processor's request limitations and known exclusions. A detection rule does not cover every product or regional variant sold under the processor's name. For example, Moneris Hosted Tokenization and Moneris Checkout use different payment paths; the implemented adapter covers Hosted Tokenization.
|
|
209
211
|
|
|
210
212
|
The catalog also carries reviewed hosted, regional, and versioned variants observed on live checkouts: Adyen secured fields on the regional live hosts and any release, the Square card element frame, dLocal Smart Fields releases, Mercado Pago secure fields and the guest Checkout Pro card form, and the Rapyd hosted checkout page. A variant the implemented adapter does not admit, such as the Tranzila terminal-root page, the Authorize.Net hosted payment form, or a Worldpay hosted payment page, identifies its processor and returns `unknown` with `processor_variant_unverified`. A new fingerprint never declares payment support by itself.
|
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
|
|
|
262
262
|
| Fiserv Commerce Hub | implemented (mode `cse`, prepared checkout only) and off until Agentcard turns it on for your company: the card frame's card capture on `connect.fiservapis.com` (or `connect-cert.fiservapis.com` in the sandbox), with the approved card encrypted on the cardholder's device under a key Agentcard reviewed for the merchant. A merchant whose frame posts to Fiserv's `payment-link` endpoint is not recognized. No live merchant purchase is verified yet |
|
|
263
263
|
| Worldline MyCheckout | implemented (mode `hosted_form`) and off until Agentcard turns it on for your company: the card form on Worldline's hosted payment page, a top-level form POST to `/checkout` on `payment.pay1` or `payment.pay2` under `checkout.worldline-solutions.com` (pre-production: `payment.pay1.preprod` and `payment.pay2.preprod`). The cardholder's device submits Worldline's form with the approved card; the paused navigation resolves to the synthetic page, and you poll the merchant's order state. The form names no amount, so pass `amount` and `currency`, above zero: that is what the cardholder approves. A card submit with a checked "remember" box, a Google Pay token or a Click to Pay answer is refused. Google Pay and Click to Pay payments that carry no card number continue untouched, as the page's cancel and its other payment methods do: no authorization, no prompt. A MyCheckout page on a merchant's own subdomain is not recognized. On a page that asks the shopper to choose a card product first, the adapters hold the card submit to that product: Worldline's IIN lookup on the agent's placeholder can switch the form to another product of the same network (CLZ's page read the Visa placeholder as Visa Electron), and a submit whose `paymentProductId` is not the product chosen is aborted before any create, with a `blocked` event, `worldline_mycheckout_product_changed` (`worldline_mycheckout_product_missing` when it names none), without holding the attachment. `placeholderCard({ psp })` returns a processor's own placeholder for the card's network where one is reviewed; none is yet for Worldline, so its Visa placeholder can still be read as Visa Electron, and that submit is then refused. No live merchant purchase is verified yet |
|
|
264
264
|
| Verifone eCommerce (verifone.js) | implemented (mode `cse`, prepared checkout only) and off until Agentcard turns it on for your company: the SDK pauses a request at a merchant whose page encrypts the card with verifone.js and posts it to the merchant's own server only when Agentcard reviewed that merchant as a Verifone profile. The Container Store is the one profile, in `observe`, and Agentcard has reviewed its card request but not its key yet, so the SDK reports and aborts a request there that carries a verifone.js blob, and nothing pays (see "Verifone merchants that encrypt the card in the page"). Agentcard has not verified a live merchant purchase yet |
|
|
265
|
-
| Gravy (Gr4vy) Secure Fields | dark launch: for an org Agentcard has enabled, after `vault.syncRegistry()`, which declares `gravy_session_fields_put`; for any other org the SDK leaves Gravy's requests alone and the request continues to Gravy untouched. The card request is Secure Fields' update of a checkout session: a JSON `PUT` to `/checkout/sessions/<session id>/fields` on the merchant's own host `api.<merchant>.gr4vy.app` (sandbox: `api.sandbox.<merchant>.gr4vy.app`), sent from Gravy's hidden controller frame and carrying `payment_method.number`, `payment_method.expiration_date` as one `MM/YY` value and `payment_method.security_code`. The SDK pauses that `PUT`, never a `POST` to the same URL; the cardholder's device sends the card as the same `PUT`, and the frame receives Gravy's answer, a `204` with no body. The request names no amount, so pass `amount` and `currency`. Updating the session stores the card with Gravy; the merchant creates the transaction from the session on its own server, so a completed request is not a payment. Gravy Embed, the hosted frame on `embed.<merchant>.gr4vy.app`, is not covered |
|
|
265
|
+
| Gravy (Gr4vy) Secure Fields | dark launch: for an org Agentcard has enabled, after `vault.syncRegistry()`, which declares `gravy_session_fields_put`; for any other org the SDK leaves Gravy's requests alone and the request continues to Gravy untouched. The card request is Secure Fields' update of a checkout session: a JSON `PUT` to `/checkout/sessions/<session id>/fields` on the merchant's own host `api.<merchant>.gr4vy.app` (sandbox: `api.sandbox.<merchant>.gr4vy.app`), sent from Gravy's hidden controller frame and carrying `payment_method.number`, `payment_method.expiration_date` as one `MM/YY` value and `payment_method.security_code`. The SDK pauses that `PUT`, never a `POST` to the same URL; the cardholder's device sends the card as the same `PUT`, and the frame receives Gravy's answer, a `204` with no body. The request names no amount, so pass `amount` and `currency`. Updating the session stores the card with Gravy; the merchant creates the transaction from the session on its own server, so a completed request is not a payment. The session id is the request's only credential, so event URLs and error messages show it as `{session}`. Gravy Embed, the hosted frame on `embed.<merchant>.gr4vy.app`, is not covered |
|
|
266
266
|
| GMO Payment Gateway | dark launch (mode `token`): only for an org Agentcard has enabled; see [Pay a GMO Payment Gateway checkout](#pay-a-gmo-payment-gateway-checkout) |
|
|
267
267
|
| CyberSource Flex Microform | dark launch: Microform 2.12.1 card fields only, after `vault.syncRegistry()`, and only for an org Agentcard has enabled. For any other org the API does not serve the recognizer, so nothing pauses there. The cardholder's device seals the card for CyberSource and the card field receives the transient token. Validated in local Chromium; no live merchant purchase yet. See [Pay a CyberSource Flex Microform checkout](#pay-a-cybersource-flex-microform-checkout) |
|
|
268
268
|
| Juspay Express Checkout | dark launch: the new-card flow Swiggy's website sends from its own page, for an org Agentcard has enabled, after `vault.syncRegistry()`. Validated in local Chromium with no live merchant purchase yet; see [Pay a Juspay checkout](#pay-a-juspay-checkout) |
|
|
@@ -278,6 +278,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
|
|
|
278
278
|
| WSPay Components (WSPay's build of Monri Components) | dark launch: the card frame's confirm of a new card, after `vault.syncRegistry()`, with `amount` and `currency`, and only for an org Agentcard has enabled; for any other org the SDK leaves WSPay's requests alone. The cardholder's device sends the card to WSPay and the frame receives WSPay's answer. A confirm that asks WSPay to keep the card on file, as Valamar's does on every booking, is paid only as a hotel's EUR 1.00 card guarantee with a card the cardholder stored. Validated in local Chromium against a model of the card frame; no WSPay sandbox run or live merchant purchase yet. See [Pay a WSPay Components checkout](#pay-a-wspay-components-checkout) |
|
|
279
279
|
| Shiji STS (MyCheck Wallet V3; Wyndham's booking pages) | dark launch, mode `cse` under a pinned key, and only for an org Agentcard has enabled. The card token a hotel booking mints guarantees the stay, so the cardholder approves with a card they stored, under a line that says the hotel can keep it on file. See [Shiji STS](#shiji-sts-mycheck-wallet-v3) |
|
|
280
280
|
| Amadeus Checkout (Air France, KLM) | dark: only for an org Agentcard enabled, and only through `prepare({ psp: 'amadeus_checkout', cardId })`. The Checkout Web SDK's card add pauses; every other action on its URL continues. Air France and KLM keep the expiry in their own form, so the agent types the stored card's real expiry there and the checkout pays only with that card. Every Data Collection Page store is refused as `amadeus_dcp_field_config_unreviewed`. No live purchase yet. See "Prepare an Amadeus Checkout card step" |
|
|
281
|
+
| Paddle checkout (Paddle's card vault; Rectangle Pro) | dark launch: only in Agentcard's own account for now, after `vault.syncRegistry()`, and only through `prepare({ psp: 'paddle_vgs', environment: 'production' })` (`'sandbox'` for Paddle's sandbox) on an attachment whose `cardId` names the paying card; without one, `prepare()` fails with `card_required`. On a Paddle checkout whose card method names VGS, the frame's post to `vault.paddle.com/cards` pauses, the cardholder's device sends the card itself, and the frame receives the stored card's id; its second request after its 10-second timeout gets the same answer. Type `placeholderCard()`'s facts for that card into Paddle's form, and pass `amount` and `currency`, the total the cardholder approves. Only Visa, Mastercard and American Express cards are paid (`paddle_vgs_card_unsupported` otherwise), and a typed expiry or placeholder that is not the card's fails the create with `paddle_vgs_card_mismatch`. On the live vault only a reviewed seller at its reviewed page is paid (Rectangle Pro at `https://rectangleapp.com`), and the approval says that seller can keep the card on file. Paddle's `/pay` still carries the placeholder's first six and last four digits beside the stored card's id, and whether Paddle accepts them is not proven yet. A Paddle checkout that sends the card through TokenEx or Spreedly is not supported, and no live payment has gone through this route yet |
|
|
281
282
|
|
|
282
283
|
The recognizer list, and the status of each reviewed Adyen merchant profile on
|
|
283
284
|
your client, are fetched from the API at runtime (`agentcard.checkout.syncRegistry()`),
|
|
@@ -1066,9 +1067,9 @@ and the reason becomes `stripe_payment_continued`. One confirm continues per
|
|
|
1066
1067
|
approval; every other follow-up, including an unrelated intent, a changed
|
|
1067
1068
|
amount and a retry, stays blocked.
|
|
1068
1069
|
|
|
1069
|
-
A card save can follow a `/v1/payment_methods` approval the same way,
|
|
1070
|
-
|
|
1071
|
-
|
|
1070
|
+
A card save can follow a `/v1/payment_methods` approval the same way, for
|
|
1071
|
+
every company: the page's SetupIntent confirm pays with the approved payment
|
|
1072
|
+
method. A Payment Element
|
|
1072
1073
|
page that creates the method itself (`paymentMethodCreation: 'manual'`, then
|
|
1073
1074
|
`confirmSetup({ elements, confirmParams: { payment_method } })`) confirms with
|
|
1074
1075
|
that method and no card, and the SDK continues the confirm untouched once the
|
|
@@ -1084,9 +1085,10 @@ emits `stripe_setup_continued` with
|
|
|
1084
1085
|
`{ authorizationId, setupIntentId, rewritten }`, where `rewritten` is true
|
|
1085
1086
|
when the SDK wrote the approved payment method in place of a card the confirm
|
|
1086
1087
|
carried, and the reason becomes `stripe_setup_continued`; the merchant then
|
|
1087
|
-
keeps the card for later charges.
|
|
1088
|
-
|
|
1089
|
-
|
|
1088
|
+
keeps the card for later charges. Refused, the confirm is aborted, and the
|
|
1089
|
+
error's code says why: `amount_mismatch` for an approval that named an
|
|
1090
|
+
amount, for example. Either kind of intent uses up the approval's one
|
|
1091
|
+
continuation.
|
|
1090
1092
|
|
|
1091
1093
|
Unrecognized merchant-server endpoints remain outside this guard unless listed
|
|
1092
1094
|
in `paymentEndpoints`; this is not a guarantee against a merchant charging a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: c8e0308b7790c3577c56ec0428577a42c308ccdbeff3eb041c6103d9e9b338a8
|
|
4
4
|
// src/card-number-key.js
|
|
5
5
|
var CARD_NUMBER_KEY = /^(number|card_number|cardnumber|cardNum|card_num|pan)$/i;
|
|
6
6
|
// src/json-path.js
|
|
@@ -340,8 +340,79 @@ function amadeusBodyKind(body) {
|
|
|
340
340
|
return record2(value.data) && holdsPan(value.data.mopdata) ? "card" : "other";
|
|
341
341
|
return "other";
|
|
342
342
|
}
|
|
343
|
+
// src/paddle-vgs.js
|
|
344
|
+
var PADDLE_VGS_PSP = "paddle_vgs";
|
|
345
|
+
var PADDLE_VGS_CARDS_URLS = /* @__PURE__ */ Object.freeze({
|
|
346
|
+
production: "https://vault.paddle.com/cards",
|
|
347
|
+
sandbox: "https://sandbox-vault.paddle.com/cards"
|
|
348
|
+
});
|
|
349
|
+
var BODY_LIMIT2 = 4096;
|
|
350
|
+
var ROOT_KEYS2 = /* @__PURE__ */ Object.freeze(["data"]);
|
|
351
|
+
var DATA_KEYS = /* @__PURE__ */ Object.freeze(["type", "attributes"]);
|
|
352
|
+
var ATTRIBUTE_KEYS = /* @__PURE__ */ Object.freeze(["pan", "cvc", "exp_month", "exp_year", "cardholder"]);
|
|
353
|
+
var CARD_NUMBER4 = /^[0-9]{12,19}$/;
|
|
354
|
+
var SECURITY_CODE2 = /^[0-9]{3,4}$/;
|
|
355
|
+
var NAME_LIMIT = 256;
|
|
356
|
+
var CONTROL = /[\u0000-\u001f\u007f]/;
|
|
357
|
+
var cardLikeName = (name) => (name.match(/[0-9]/g) ?? []).length >= 12;
|
|
358
|
+
var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
359
|
+
var sameKeys = (value, keys) => {
|
|
360
|
+
const own = Object.keys(value);
|
|
361
|
+
return own.length === keys.length && own.every((key, at) => key === keys[at]);
|
|
362
|
+
};
|
|
363
|
+
var refuse4 = (code) => Object.freeze({ verdict: "refuse", code });
|
|
364
|
+
var CARD4 = /* @__PURE__ */ Object.freeze({ verdict: "card" });
|
|
365
|
+
function paddleVgsEnvironment(url) {
|
|
366
|
+
if (url === PADDLE_VGS_CARDS_URLS.production)
|
|
367
|
+
return "production";
|
|
368
|
+
if (url === PADDLE_VGS_CARDS_URLS.sandbox)
|
|
369
|
+
return "sandbox";
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
function isPaddleVgsCardsUrl(url) {
|
|
373
|
+
return paddleVgsEnvironment(url) !== null;
|
|
374
|
+
}
|
|
375
|
+
function readBody2(body) {
|
|
376
|
+
if (typeof body !== "string" || body === "" || body.length > BODY_LIMIT2)
|
|
377
|
+
return null;
|
|
378
|
+
let value;
|
|
379
|
+
try {
|
|
380
|
+
value = JSON.parse(body);
|
|
381
|
+
if (!record3(value) || JSON.stringify(value) !== body)
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
function paddleVgsBodyVerdict(body) {
|
|
390
|
+
const value = readBody2(body);
|
|
391
|
+
if (value === null || !sameKeys(value, ROOT_KEYS2) || !record3(value.data) || !sameKeys(value.data, DATA_KEYS) || value.data.type !== "cards" || !record3(value.data.attributes) || !sameKeys(value.data.attributes, ATTRIBUTE_KEYS)) {
|
|
392
|
+
return refuse4("paddle_vgs_body_unsupported");
|
|
393
|
+
}
|
|
394
|
+
const card = value.data.attributes;
|
|
395
|
+
const holder = card.cardholder;
|
|
396
|
+
if (typeof card.pan !== "string" || !CARD_NUMBER4.test(card.pan) || typeof card.cvc !== "string" || !SECURITY_CODE2.test(card.cvc) || !Number.isInteger(card.exp_month) || card.exp_month < 1 || card.exp_month > 12 || !Number.isInteger(card.exp_year) || card.exp_year < 0 || card.exp_year > 99 || !record3(holder) || !sameKeys(holder, ["name"]) || typeof holder.name !== "string" || holder.name.length > NAME_LIMIT || CONTROL.test(holder.name) || cardLikeName(holder.name)) {
|
|
397
|
+
return refuse4("paddle_vgs_body_unsupported");
|
|
398
|
+
}
|
|
399
|
+
return CARD4;
|
|
400
|
+
}
|
|
401
|
+
var pin2 = (fields) => Object.freeze({
|
|
402
|
+
...fields,
|
|
403
|
+
origins: Object.freeze([...fields.origins]),
|
|
404
|
+
sellerClaim: fields.sellerClaim ? Object.freeze({ ...fields.sellerClaim }) : null
|
|
405
|
+
});
|
|
406
|
+
var PADDLE_VGS_MERCHANT_PINS = /* @__PURE__ */ Object.freeze([
|
|
407
|
+
pin2({ id: "rectangle_pro", merchant: "Rectangle Pro", origins: ["https://rectangleapp.com"], sellerClaim: null })
|
|
408
|
+
]);
|
|
343
409
|
// src/card-fields.js
|
|
344
410
|
function processorGate(recognizer, url, body) {
|
|
411
|
+
if (recognizer && recognizer.psp === PADDLE_VGS_PSP) {
|
|
412
|
+
if (url !== void 0 && !isPaddleVgsCardsUrl(url))
|
|
413
|
+
return { verdict: "refuse", code: "paddle_vgs_url_refused" };
|
|
414
|
+
return paddleVgsBodyVerdict(body);
|
|
415
|
+
}
|
|
345
416
|
if (recognizer && recognizer.psp === GRAVY_PSP)
|
|
346
417
|
return gravyBodyVerdict(body);
|
|
347
418
|
if (recognizer && recognizer.psp === MONERIS_PSP) {
|
package/dist/cdp.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ACI_CHECKOUT_REUSED, AciGate, playwrightReplacesNavigationBody } from '
|
|
|
10
10
|
import { EprotectGate } from './eprotect.js';
|
|
11
11
|
import { dispatchPreClaim, eventUrl } from './pre-claim.js';
|
|
12
12
|
import { GMO_TOKEN_V1_PATTERNS, gmoRequestShop, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
|
|
13
|
+
import { isPaddleVgsCardsUrl } from './paddle-vgs.generated.js';
|
|
13
14
|
import { DatatransRepeats } from './datatrans.js';
|
|
14
15
|
import { SHIJI_STS_COMPANION_WINDOW_MS, ShijiStsCompanions, ShijiStsError, isShijiStsHost, shijiStsTokenContinuation, } from './shiji-sts.js';
|
|
15
16
|
import { isArmedEkashuUrl } from './ekashu.js';
|
|
@@ -1241,14 +1242,32 @@ function bodyShape(body) {
|
|
|
1241
1242
|
* the one the Flex gate read from the field iframe (cybersource-flex.ts: microform type, fields,
|
|
1242
1243
|
* the capture context's key id and the context itself), never the ciphertext.
|
|
1243
1244
|
*/
|
|
1244
|
-
function requestIdentity(opts, url, method, body, documentUrl, pageAmount, flexShape) {
|
|
1245
|
+
function requestIdentity(opts, url, method, body, documentUrl, pageAmount, flexShape, headers) {
|
|
1245
1246
|
const shaped = flexShape !== undefined ? { shape: flexShape, namesAmount: false } : bodyShape(body);
|
|
1246
1247
|
// A GMO token is minted for the one shop its request names, and the body's shape leaves that value
|
|
1247
1248
|
// out, so the shop joins the identity: a retry that names another shop needs its own approval.
|
|
1248
1249
|
const shop = gmoRequestShop(url, body);
|
|
1249
|
-
|
|
1250
|
+
const identity = shop === undefined ? shaped.shape : `${shaped.shape};gmo_shop:${JSON.stringify(shop)}`;
|
|
1251
|
+
return { url, method, documentUrl, pageAmount, body: `${identity}${paddleVgsRetryKey(url, body, headers)}`,
|
|
1250
1252
|
amountEvidence: opts.amount != null || pageAmount !== undefined || shaped.namesAmount };
|
|
1251
1253
|
}
|
|
1254
|
+
/**
|
|
1255
|
+
* What a Paddle VGS card request's retry must repeat byte for byte, beyond its shape: Paddle's frame
|
|
1256
|
+
* sends its second request with the same body under the same bearer, and the bearer names the
|
|
1257
|
+
* checkout the card is stored for. So the whole body and the bearer and content type join the
|
|
1258
|
+
* identity: a request under another checkout's bearer, or with any value changed, never receives
|
|
1259
|
+
* the stored card's id the cardholder approved for the first. Empty for every other request.
|
|
1260
|
+
*/
|
|
1261
|
+
function paddleVgsRetryKey(url, body, headers) {
|
|
1262
|
+
if (!isPaddleVgsCardsUrl(url))
|
|
1263
|
+
return '';
|
|
1264
|
+
const record = headers !== null && typeof headers === 'object' ? headers : {};
|
|
1265
|
+
const one = (name) => {
|
|
1266
|
+
const found = Object.keys(record).filter((key) => key.toLowerCase() === name);
|
|
1267
|
+
return found.length === 1 && typeof record[found[0]] === 'string' ? record[found[0]] : null;
|
|
1268
|
+
};
|
|
1269
|
+
return `;paddle_vgs:${JSON.stringify([one('authorization'), one('content-type'), body])}`;
|
|
1270
|
+
}
|
|
1252
1271
|
/** A CDP frame or request URL with its fragment, which CDP reports apart (urlFragment). */
|
|
1253
1272
|
function withFragment(value) {
|
|
1254
1273
|
if (typeof value?.url !== 'string')
|
|
@@ -1291,7 +1310,9 @@ function sameCheckoutRequest(first, retry) {
|
|
|
1291
1310
|
* answer once the page has moved on), a prepared checkout (single use, bound
|
|
1292
1311
|
* to one native request by design) other than a Bambora token create, whose
|
|
1293
1312
|
* card frame gives up 5 seconds after sending it and whose page asks again
|
|
1294
|
-
* with the same body (preparedApprovalAnswersRetry),
|
|
1313
|
+
* with the same body (preparedApprovalAnswersRetry), or a Paddle VGS card
|
|
1314
|
+
* request, whose frame sends its second request byte for byte under the same
|
|
1315
|
+
* bearer (paddleVgsRetryKey), from that same card frame
|
|
1295
1316
|
* only (sameFrame), a native Stripe Checkout step, a
|
|
1296
1317
|
* Mercado Pago request whose claim is bound to one document read, a Juspay
|
|
1297
1318
|
* /txns (the device pays that request's own order; a page that clicks Pay
|
|
@@ -2524,7 +2545,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2524
2545
|
if (awaitingApproval && activeRequest?.attempt.lost && !terminal && !setupStop.signal.aborted && !lifecycle.isCancelled()
|
|
2525
2546
|
&& typeof networkId === 'string' && networkId) {
|
|
2526
2547
|
const retryBody = pausedBody(request);
|
|
2527
|
-
const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape);
|
|
2548
|
+
const retry = retryBody === null ? null : requestIdentity(opts, request.url, request.method, retryBody, documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape, request.headers);
|
|
2528
2549
|
// A prepared approval is answered only in the card frame it was bound in: the same request from
|
|
2529
2550
|
// another frame (a replacement for a removed one, or a second card frame) is refused below.
|
|
2530
2551
|
if (retry && activeRequest?.attempt.lost && sameCheckoutRequest(activeRequest.identity, retry)
|
|
@@ -2647,7 +2668,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
2647
2668
|
}
|
|
2648
2669
|
attempt.bind({ requestId, sessionId, request });
|
|
2649
2670
|
activeRequest = { attempt, requestId, networkId, sessionId, frameId, request,
|
|
2650
|
-
identity: requestIdentity(opts, request.url, request.method, body, undefined, undefined, flexPause?.shape),
|
|
2671
|
+
identity: requestIdentity(opts, request.url, request.method, body, undefined, undefined, flexPause?.shape, request.headers),
|
|
2651
2672
|
preparedFrame: preparation ? { sessionId, frameId } : undefined };
|
|
2652
2673
|
// An ACI card-number request: the page's CVC and expiry requests for its checkout are held
|
|
2653
2674
|
// until this approval ends (see AciGate). One whose checkout already has a card-number
|
|
@@ -3515,7 +3536,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3515
3536
|
// for, after its own request timed out: bind it to that approval (see
|
|
3516
3537
|
// merchantAttempt and the same check in attachToCdp).
|
|
3517
3538
|
if (awaitingApproval && activeRequest?.attempt.lost && !terminal && !setupStop.signal.aborted && !lifecycle.isCancelled()) {
|
|
3518
|
-
const retry = requestIdentity(opts, request.url(), request.method(), request.postData() ?? '', documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape);
|
|
3539
|
+
const retry = requestIdentity(opts, request.url(), request.method(), request.postData() ?? '', documentKey(await documentUrlOf(readDocumentUrl, lifecycle.abort.signal)), await pageAmountOf(opts), flexPause?.shape, request.headers());
|
|
3519
3540
|
const retryFrames = [];
|
|
3520
3541
|
try {
|
|
3521
3542
|
for (let frame = request.frame?.(); frame; frame = frame.parentFrame?.())
|
|
@@ -3638,7 +3659,7 @@ export async function attachToPlaywright(page, opts) {
|
|
|
3638
3659
|
const payToInterceptMs = readPayToInterceptMs(opts.payClickedAt, approvalStartedAt);
|
|
3639
3660
|
attempt.bind({ route, request, frames });
|
|
3640
3661
|
activeRequest = { request, frames, attempt,
|
|
3641
|
-
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined, flexPause?.shape),
|
|
3662
|
+
identity: requestIdentity(opts, request.url(), request.method(), body, undefined, undefined, flexPause?.shape, request.headers()),
|
|
3642
3663
|
preparedFrames: preparation ? [...frames] : undefined };
|
|
3643
3664
|
// An ACI card-number request: see the same step in attachToCdp.
|
|
3644
3665
|
if (!aci.begin(request.url(), request.method(), body, preparation?.merchantOrigin)) {
|
package/dist/client.d.ts
CHANGED
|
@@ -204,6 +204,14 @@ export declare const SUPPORTED_REGISTRY_FEATURES: readonly string[];
|
|
|
204
204
|
* the same terms: only to a build that lists it, for a company Agentcard turned
|
|
205
205
|
* Moneris on for. A gated recognizer never sits in BUILTIN_REGISTRY, so it is paused
|
|
206
206
|
* only after a sync.
|
|
207
|
+
*
|
|
208
|
+
* `paddle_vgs_cards`: the card request Paddle's checkout frame sends to its VGS card
|
|
209
|
+
* vault (vault.paddle.com/cards) on a checkout that names VGS, only through a prepared
|
|
210
|
+
* checkout: the frame aborts the request 10 seconds after Pay and sends it once more,
|
|
211
|
+
* and this build answers that second request from the same approval. The cardholder's
|
|
212
|
+
* device sends the card itself and the frame gets back the stored card's id alone.
|
|
213
|
+
* Served on the same terms: only to a build that lists it, for a company Agentcard
|
|
214
|
+
* turned Paddle on for.
|
|
207
215
|
*/
|
|
208
216
|
export declare const SUPPORTED_CAPABILITIES: readonly string[];
|
|
209
217
|
/**
|
|
@@ -1088,8 +1096,13 @@ export declare class CheckoutAuthError extends Error {
|
|
|
1088
1096
|
* so it reads {txId}, as does any other id-shaped segment there.
|
|
1089
1097
|
* On the Monri and WSPay hosts the secret is a path segment, so it is replaced
|
|
1090
1098
|
* with `{client_secret}`: https://proxy.wspay.biz/v2/payment/{client_secret}/confirm.
|
|
1099
|
+
* On Gravy's hosts the checkout session id is the credential, so it reads
|
|
1100
|
+
* `{session}`: https://api.<merchant>.gr4vy.app/checkout/sessions/{session}/fields.
|
|
1101
|
+
* On Rapyd's hosts the hosted page's id reads `{token}`: https://api.rapyd.net/v1/checkout/{token}.
|
|
1102
|
+
* On api.stripe.com a Checkout Session id reads `{session}`:
|
|
1103
|
+
* https://api.stripe.com/v1/payment_pages/{session}/confirm.
|
|
1091
1104
|
* A string that is not a URL keeps the part before any `?` or `#`, with the
|
|
1092
|
-
* same
|
|
1105
|
+
* same segments masked.
|
|
1093
1106
|
*/
|
|
1094
1107
|
export declare function redactUrl(raw: string): string;
|
|
1095
1108
|
export interface VaultClientOptions {
|
|
@@ -1428,11 +1441,12 @@ export declare class VaultClient {
|
|
|
1428
1441
|
* approval produced: a SetupIntent confirm that carries no card and pays with
|
|
1429
1442
|
* exactly the approved payment method (the page's own, or written by the
|
|
1430
1443
|
* adapters in place of a card the confirm carries again).
|
|
1431
|
-
* The API allows a card save
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1444
|
+
* The API allows a card save for any company, for an approval given on the
|
|
1445
|
+
* Vault page, not by voice, with no amount, and a SetupIntent Stripe reports
|
|
1446
|
+
* still confirmable on the same Stripe account; any other answer rejects with
|
|
1447
|
+
* a CheckoutApiError whose code says why (for example `amount_mismatch`, or
|
|
1448
|
+
* `continuation_invalid` for a confirm that is not a card-free SetupIntent
|
|
1449
|
+
* confirm). Nothing
|
|
1436
1450
|
* is charged: the adapters continue the page's request once this resolves.
|
|
1437
1451
|
*/
|
|
1438
1452
|
checkStripeSetupContinuation(authorizationId: string, request: {
|
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
|
|
@@ -949,6 +960,71 @@ function maskCreditguardTxId(path) {
|
|
|
949
960
|
const PATH_SECRET_HOSTS = ['proxy.wspay.biz', 'proxy-test.wspay.biz', 'ipg.monri.com', 'ipgtest.monri.com'];
|
|
950
961
|
/** The segment after a `payment` segment, in any case (the WSPay proxy is IIS) or after an escaped slash. */
|
|
951
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
|
+
];
|
|
952
1028
|
/**
|
|
953
1029
|
* A URL as it may appear in an error message: origin + path only. A paused
|
|
954
1030
|
* request's URL can carry a client secret in its query string, and error
|
|
@@ -958,8 +1034,13 @@ const PATH_SECRET = /(\/payment(?:\/|%2f))[^/]*/gi;
|
|
|
958
1034
|
* so it reads {txId}, as does any other id-shaped segment there.
|
|
959
1035
|
* On the Monri and WSPay hosts the secret is a path segment, so it is replaced
|
|
960
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.
|
|
961
1042
|
* A string that is not a URL keeps the part before any `?` or `#`, with the
|
|
962
|
-
* same
|
|
1043
|
+
* same segments masked.
|
|
963
1044
|
*/
|
|
964
1045
|
export function redactUrl(raw) {
|
|
965
1046
|
// An ACI checkout id is that checkout's only capability; it never rides on an event.
|
|
@@ -969,12 +1050,18 @@ export function redactUrl(raw) {
|
|
|
969
1050
|
try {
|
|
970
1051
|
const u = new URL(raw);
|
|
971
1052
|
// A trailing dot names the same host, so it cannot let the secret through.
|
|
972
|
-
|
|
973
|
-
|
|
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}`;
|
|
974
1058
|
}
|
|
975
1059
|
catch {
|
|
976
|
-
|
|
977
|
-
|
|
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;
|
|
978
1065
|
}
|
|
979
1066
|
}
|
|
980
1067
|
/**
|
|
@@ -1029,6 +1116,17 @@ const EPROTECT_PREPARATION_REFUSALS = ['merchant_profile_mismatch'];
|
|
|
1029
1116
|
const SECURE_ACCEPTANCE_PREPARATION_REFUSALS = [
|
|
1030
1117
|
'processor_unavailable', 'sandbox_host_on_live_row', 'live_host_on_sandbox_row', 'unsupported_checkout', 'merchant_profile_mismatch',
|
|
1031
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
|
+
];
|
|
1032
1130
|
/**
|
|
1033
1131
|
* The reason the cardholder's device declines a bound prepared row with, per processor, when its own
|
|
1034
1132
|
* send boundary passed before the card left it (the Vault's sendWindow, 12 seconds after it last saw
|
|
@@ -1541,6 +1639,12 @@ export class VaultClient {
|
|
|
1541
1639
|
const amadeus = input.psp === AMADEUS_CHECKOUT_PSP;
|
|
1542
1640
|
if (amadeus && !input.cardId)
|
|
1543
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');
|
|
1544
1648
|
// CyberSource Flex, tokenized before Pay: the loaded card field's capture context and fields,
|
|
1545
1649
|
// verified here against the keys payment-core pins before anything is created (the API and the
|
|
1546
1650
|
// cardholder's device verify them again).
|
|
@@ -1665,6 +1769,8 @@ export class VaultClient {
|
|
|
1665
1769
|
|| (secureAcceptance && (state.secure_acceptance_profile_id !== secureAcceptanceProfileId || state.card_id !== input.cardId))
|
|
1666
1770
|
// Amadeus: the card the cardholder approved is the one whose expiry the agent typed.
|
|
1667
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)
|
|
1668
1774
|
// The API says a preparation tokenizes before Pay exactly when this one carried a capture context.
|
|
1669
1775
|
|| (state.tokenize_before_pay === true) !== !!flexContext)
|
|
1670
1776
|
throw fail('ready_unconfirmed', id);
|
|
@@ -1709,6 +1815,7 @@ export class VaultClient {
|
|
|
1709
1815
|
&& ((error.status >= 400 && error.status < 500 && (PREPARATION_REFUSALS.includes(error.code)
|
|
1710
1816
|
|| (input.psp === 'fiserv' && FISERV_PREPARATION_REFUSALS.includes(error.code))
|
|
1711
1817
|
|| (input.psp === EPROTECT_PSP && EPROTECT_PREPARATION_REFUSALS.includes(error.code))
|
|
1818
|
+
|| (input.psp === PADDLE_VGS_PSP && PADDLE_VGS_PREPARATION_REFUSALS.includes(error.code))
|
|
1712
1819
|
|| (!!flexContext && CYBERSOURCE_FLEX_PREPARATION_REFUSALS.includes(error.code))))
|
|
1713
1820
|
|| (input.psp === 'fiserv' && error.status === 502 && error.code === 'cse_key_unavailable')))
|
|
1714
1821
|
throw fail(error.code);
|
|
@@ -2138,9 +2245,12 @@ export class VaultClient {
|
|
|
2138
2245
|
// A prepared CyberSource Secure Acceptance token create too, for 30 seconds:
|
|
2139
2246
|
// the merchant's page asks its frame for the reply for 30 seconds from Save,
|
|
2140
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.
|
|
2141
2250
|
const preparedFastPollUntil = preparation?.psp === 'mercado_pago' ? Date.now() + 10_000
|
|
2142
|
-
: preparation?.psp ===
|
|
2143
|
-
: preparation?.psp ===
|
|
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;
|
|
2144
2254
|
// A preparation that tokenized before Pay: the cardholder's device already holds CyberSource's
|
|
2145
2255
|
// token and approves as soon as this bind reaches it, while the page may give createToken only
|
|
2146
2256
|
// seconds (Alaska Airlines' flight checkout gives it three). Poll every 100 ms for 10 seconds,
|
|
@@ -2686,11 +2796,12 @@ export class VaultClient {
|
|
|
2686
2796
|
* approval produced: a SetupIntent confirm that carries no card and pays with
|
|
2687
2797
|
* exactly the approved payment method (the page's own, or written by the
|
|
2688
2798
|
* adapters in place of a card the confirm carries again).
|
|
2689
|
-
* The API allows a card save
|
|
2690
|
-
*
|
|
2691
|
-
*
|
|
2692
|
-
*
|
|
2693
|
-
*
|
|
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
|
|
2694
2805
|
* is charged: the adapters continue the page's request once this resolves.
|
|
2695
2806
|
*/
|
|
2696
2807
|
async checkStripeSetupContinuation(authorizationId, request) {
|