@agent-cards/sdk 0.23.10 → 0.23.11

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - 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
+
7
+ - 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.
8
+
5
9
  - Pay IKEA US's ACI checkout as its live payment step sends it. With ACI's ReD Shield screening on, the widget posts the device fingerprint, `customer.browserFingerprint.value` alone, to the checkout's card URL on load, and 0.23.9 refused it as `aci_unsupported_request`. That refusal also failed the checkout's CVC request at once whenever it reached the SDK before the card-number request at Pay, so the widget's Pay failed. The fingerprint now continues untouched, up to the 4,096 characters ACI takes; a longer one is still refused, and a refused request that names no card number no longer fails the checkout's CVC or expiry requests, which wait for the card-number request as before. `/payment` now continues with `shopperResultTarget=_top`, the value IKEA's widget options set and ACI's own default for its final redirect, which 0.23.9 refused after the cardholder's device had already sent the card; any other target is still refused as `aci_unsupported_request`. No API or Vault release is needed.
6
10
 
7
11
  - Pay an APCOA airport drop-off entry on NMI's hosted payment page (`ekashu`). Every APCOA entry carries APCOA's own ASP.NET Core anti-forgery token, `__RequestVerificationToken`, and the API refused it as a field of NMI's card page (`hosted_form_field_refused`, reason `not_an_entry`), so an attached checkout at Heathrow's drop-off could not get past the create. The API now takes the merchant's token and still refuses an entry that carries `ekashu_stage`, `ekashu_context` or `ekashu_submission_id`. No SDK upgrade is needed: the API takes the entry once it deploys, and the cardholder's phone fills NMI's card page from the next Vault release. NMI stays off for every company Agentcard has not turned it on for.
@@ -20,9 +24,13 @@
20
24
 
21
25
  - Pay a Wyndham hotel booking through Shiji STS (MyCheck Wallet V3), for a company Agentcard turned Shiji STS on for. Wyndham's wallet sends its `getTokenId` with no `keySequence`, because its merchant records hold no `clientKey`, and the SDK refused every Shiji `getTokenId` in the browser as `shiji_sts_keep_on_file` while the API could bind none without a `keySequence`. The SDK now pauses that request for the one prompt, writes the cardholder's `pan` and `expires` over the page's own and continues it, then continues the page's `getCvvId` once with the cardholder's CVV under the key whose SHA-256 the API serves with the approval. That field is now `shiji_sts_companion.key_sequence_sha256` and replaces `key_sequences`: an approval in the old shape resolves as `PaymentOutcomeUnknownError` (`cse_substitutions_malformed`) and nothing continues. The cardholder approves with a card they stored, under a line that says the hotel can keep it on file, and the API refuses a card a company added with `409 card_not_stored`. Shiji STS stays dark for every company Agentcard has not turned on, and a payment also needs the matching API and Vault releases.
22
26
 
23
- - Hold a Tranzila card form to one charge that reports to the merchant or to Tranzila, read the way Tranzila's server reads it. Tranzila's own card form, on `direct.tranzila.com` and `directng.tranzila.com`, names its callbacks `success_url`, `fail_url` and `notify_url`, and the API held only the `*_url_address` names the merchant posts to Tranzila to the merchant's host. It now holds all six to the merchant's host or to Tranzila's own result page exactly (`https://direct.tranzila.com/done/`, `https://directng.tranzila.com/process/fail`) and refuses any other with `callback_host`. A form that asks for installments, credit or recurring billing (`cred_type` or `tash` other than 1, `npay` other than 0, any `fpay`, `spay` or `recur_*` value, a checked `make_recur_transaction`) is refused with `special_flow` where it passed as a plain sale. Tranzila's PHP server acts on the last copy of a repeated field and reads `cred.type` as `cred_type`, while the API read the first copy under the name as written, so a second `sum` could charge another amount than the one approved: a field posted twice with different values is now refused with `duplicate_field`, and a name PHP renames with `malformed_field`. Every Tranzila card form read on 2026-10-10 also carries Tranzila's ALTCHA field, which the API still refuses first with `hosted_form_gated`.
27
+ - Hold a Tranzila card form to one charge that reports to the merchant or to Tranzila, read the way Tranzila's server reads it. Tranzila's own card form, on `direct.tranzila.com` and `directng.tranzila.com`, names its callbacks `success_url`, `fail_url` and `notify_url`, and the API held only the `*_url_address` names the merchant posts to Tranzila to the merchant's host. It now holds all six to the merchant's host or to Tranzila's own result page exactly (`https://direct.tranzila.com/done/`, `https://directng.tranzila.com/process/fail`) and refuses any other with `callback_host`. A form that asks for installments, credit or recurring billing (`cred_type` or `tash` other than 1, `npay` other than 0, any `fpay`, `spay` or `recur_*` value, a checked `make_recur_transaction`) is refused with `special_flow` where it passed as a plain sale. Tranzila's PHP server acts on the last copy of a repeated field and reads `cred.type` as `cred_type`, while the API read the first copy under the name as written, so a second `sum` could charge another amount than the one approved: a field posted twice with different values is now refused with `duplicate_field`, and a name PHP renames with `malformed_field`.
28
+
29
+ - Pay a Tranzila card form that carries Tranzila's own ALTCHA proof of work. Every Tranzila card form read on 2026-10-10 (`iframenew.php` on `direct.tranzila.com` and `directng.tranzila.com`, the legacy `iframe.php` and the terminal page) runs an ALTCHA widget that searches for the number whose SHA-256 with the page's salt is its challenge, writes that proof into `altcha` and only then submits, and the API refused every such form with `hosted_form_gated`. The API now takes that one field, on Tranzila only, when it holds one ALTCHA proof whose hash holds, and the cardholder's device sends it once with the card, as the page wrote it. Anything else in `altcha` is refused with `hosted_form_field_refused`, reason `malformed_field`, and every other filled-in challenge field (reCAPTCHA, hCaptcha, Turnstile, or `altcha` on another processor) is still `hosted_form_gated`. No SDK or Vault release is needed: the API takes the form once it deploys.
30
+
31
+ - Pause an Elavon Converge payment page opened on `www.convergepay.com`, where every Converge Buy Button and pay link sends the buyer, for a company Agentcard turned Converge on for. That page posts the card to its own origin, `https://www.convergepay.com/hosted-payments/service/payment/hpp/process`, and the SDK recognized the process call only on `api.convergepay.com` and the demo host, so the agent's placeholder card reached Converge. `www.convergepay.com` is a production host: a test row never pays there. A Buy Button's page also sends `recurring` as `"false"` in every request; that one-time sale now pauses where it was refused as `converge_recurring_refused`, and `"true"` or any other value is still refused.
24
32
 
25
- - Pause an Elavon Converge payment page opened on `www.convergepay.com`, where every Converge Buy Button and pay link sends the buyer, for a company Agentcard turned Converge on for. That page posts the card to its own origin, `https://www.convergepay.com/hosted-payments/service/payment/hpp/process`, and the SDK recognized the process call only on `api.convergepay.com` and the demo host, so the agent's placeholder card reached Converge. `www.convergepay.com` is a production host: a test row never pays there. A Buy Button's page also sends `recurring` as `"false"` in every request; that one-time sale now pauses where it was refused as `converge_recurring_refused`, and `"true"` or any other value is still refused. A page with its CAPTCHA on still stops with `converge_captcha_refused`, and every Buy Button page read so far has one.
33
+ - Pause an Elavon Converge payment page with its CAPTCHA on once a person solved it, for a company Agentcard turned Converge on for. Every Buy Button and pay link page read so far shows a reCAPTCHA and sends its card request only after it is solved, with the answer in `captchaToken`, and earlier releases refused every such request as `converge_captcha_refused` before any create. The request now pauses like any other, and the cardholder's device sends the answer to Converge unchanged beside the card. A person solves the CAPTCHA in the browser, never your agent, and Google takes the answer once, for two minutes after the solve, so the cardholder approves inside that window. A `captchaToken` that is empty, not text, longer than 4,096 characters, or holds a character other than `A-Z`, `a-z`, `0-9`, `_` and `-` is still refused as `converge_captcha_refused`. A payment also needs the matching API and Vault releases.
26
34
 
27
35
  - Answer a prepared Bambora checkout inside Custom Checkout's 5 seconds. The card frame fails its tokenization request 5 seconds after Pay, and 0.23.9 read a prepared Bambora approval every 2 seconds, so a cardholder's answer that missed the first read waited for the read 4 seconds after the bind, at or past the frame's timer. The SDK now reads it every 100 ms for its first 10 seconds, and the API's bind names that interval on the row, so no read is refused with `slow_down`. When the frame gives up first anyway, the approval now outlives it, as an unprepared one does: once the device answers, the controller reads `ready_to_submit` with reason `awaiting_merchant_retry`, and when the merchant's next Pay click sends the same request again from the same card frame, the SDK answers it from the same approval with no second prompt. The approval stays with that card frame: the SDK refuses the same request from any other frame, and the page removing the card frame stops the attachment, as it does while the request is still paused, so a replacement frame never receives the token. 0.23.9 ended that checkout `outcome_unknown` and needed a new attachment and a second approval. Every other prepared checkout still answers only the request it was bound to. A payment also needs the matching API release.
28
36
 
package/README.md CHANGED
@@ -260,7 +260,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
260
260
  | ACI Worldwide COPYandPAY (Halfords) | dark launch: served only after `syncRegistry()` and only for an org Agentcard enabled; prepared checkout only. The card-number request is replayed from the cardholder's device, the page's own CVC and expiry requests are answered locally, and `/payment` continues once. See [Pay an ACI COPYandPAY checkout](#pay-an-aci-copyandpay-checkout) |
261
261
  | Tranzila | supported (mode `hosted_form`): the cardholder finishes on Tranzila's own page; the paused form navigation resolves to a synthetic page, and you poll the merchant's order state |
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
- | 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. No live merchant purchase is verified yet |
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
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 |
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) |
@@ -707,8 +707,11 @@ is gone resolves as `PaymentOutcomeUnknownError` with reason
707
707
  callback URLs); all three are terminal for that page. A Tranzila form is
708
708
  also refused with `duplicate_field` (a field posted twice with different
709
709
  values: Tranzila acts on the last copy) or `malformed_field` (a field name
710
- Tranzila's server reads as another name, such as `cred.type`), terminal
711
- the same way. An Authorize.Net DPM or SIM form adds more reasons
710
+ Tranzila's server reads as another name, such as `cred.type`, or an
711
+ `altcha` field holding anything but the ALTCHA proof of work Tranzila's
712
+ own page writes there before it submits), terminal the same way. That
713
+ proof is the only filled-in challenge field the cardholder's device sends
714
+ with the card. An Authorize.Net DPM or SIM form adds more reasons
712
715
  (`duplicate_field`, `not_a_card_payment`, `relay_response_off`,
713
716
  `form_step`, `merchant_credential`, `test_request_on_live_host`,
714
717
  `malformed_field`), terminal the same way,
@@ -1279,8 +1282,15 @@ Lost authorization polling, local approval timeouts, or interrupted browser
1279
1282
  handoffs produce `outcome_unknown` and block automatic retry. The thrown
1280
1283
  `PaymentOutcomeUnknownError` carries `authorizationId` when creation was
1281
1284
  acknowledged. `checkout.cancel()` stops the local attachment and polling; it
1282
- does not revoke a pending approval link or undo a processor payment. Cancellation
1283
- after an attempt starts is therefore unknown until reconciled. A cancelled
1285
+ does not revoke a pending approval link or undo a processor payment. Cancelling
1286
+ an attempt still under way (a card request in flight, or an authorization not
1287
+ yet settled) therefore reads `outcome_unknown` with reason `local_cancel` until
1288
+ reconciled. A checkout that already ended keeps its state and reason:
1289
+ `completed`, `declined`, `failed`, `timed_out`, `unsupported`, `cancelled` and
1290
+ `outcome_unknown` read the same after `cancel()` once no card request is in
1291
+ flight, or once the request in flight has its own outcome recorded, as when
1292
+ your code cancels from `onStateChange` or the `failed` event. A cleanup that
1293
+ cancels never hides a processor refusal or a confirmed payment. A cancelled
1284
1294
  attachment cannot restart.
1285
1295
 
1286
1296
  ### Adyen merchants that take the card on their own server
@@ -1931,16 +1941,26 @@ const checkout = await attachToPlaywright(page, {
1931
1941
  - A Buy Button's page sends `recurring` as `"false"` in every request unless the
1932
1942
  buyer ticked the button's recurring box. That request is a one-time sale and
1933
1943
  pauses like any other; `"true"`, or any other value, is refused as recurring.
1944
+ - A page with its CAPTCHA on, as every Buy Button page read so far has it,
1945
+ sends its card request only after the CAPTCHA is solved, with the answer in
1946
+ `captchaToken`. A person solves it in the browser, never your agent, so a
1947
+ browser that solves CAPTCHAs itself, such as Kernel with stealth on, must not
1948
+ run a Converge page. The SDK
1949
+ pauses that request like any other, and the cardholder's device sends the
1950
+ answer to Converge unchanged beside the card. Google takes the answer once,
1951
+ for two minutes after the solve, so the cardholder approves inside that
1952
+ window.
1934
1953
  - Before any prompt, the SDK refuses every other shape on that URL with
1935
1954
  payment-core's own code (`ConvergeRefusalReason`, listed in
1936
1955
  `CONVERGE_REFUSAL_REASONS`), the one the API and the cardholder's device
1937
1956
  would answer: a wallet, the debit, gift card and push-to-card tabs, a
1938
- follow-up after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA, a
1939
- public-sector service fee, recurring or installment fields, card saving, a
1940
- tip or cash back, another currency, no amount, no page session, and anything
1941
- that is not the page's own request. The page's request is aborted, nothing is
1942
- created, and the attachment is not held, so the page's next payment is judged
1943
- afresh. The table below gives each code's meaning and the next step.
1957
+ follow-up after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA
1958
+ answer Google never issued, a public-sector service fee, recurring or
1959
+ installment fields, card saving, a tip or cash back, another currency, no
1960
+ amount, no page session, and anything that is not the page's own request.
1961
+ The page's request is aborted, nothing is created, and the attachment is not
1962
+ held, so the page's next payment is judged afresh. The table below gives each
1963
+ code's meaning and the next step.
1944
1964
  - A terminal with 3-D Secure 2, Pay Defense or a surcharge answers the first
1945
1965
  request with a step instead of a result. The Vault never hands that answer to
1946
1966
  the page: the authorization ends declined as a processor refusal with
@@ -1983,7 +2003,7 @@ of the same code. `converge_step_unsupported`, `converge_declined` and
1983
2003
  | `converge_gift_card_refused` | The payment came from the page's gift card tab. | Pay with the credit card form. |
1984
2004
  | `converge_push_to_card_refused` | The payment would send money to the card instead of charging it. | None: an Agentcard card never receives money. |
1985
2005
  | `converge_payment_type_refused` | The body names a payment type other than a credit card sale, or an electronic check or financing payment carries a card number. | Pay with the credit card form. |
1986
- | `converge_captcha_refused` | The page sent a CAPTCHA answer with the payment. | The buyer pays this merchant without Agentcard: the cardholder's device cannot answer a CAPTCHA. |
2006
+ | `converge_captcha_refused` | The request's `captchaToken` is not a solved reCAPTCHA answer: it is empty, not text, longer than 4,096 characters, or holds a character other than `A-Z`, `a-z`, `0-9`, `_` and `-`. | Have a person solve the page's CAPTCHA, then let the page send its own request. |
1987
2007
  | `converge_service_fee_refused` | The merchant adds a public-sector service fee, which Converge charges as a second payment. | The buyer pays this merchant without Agentcard: a one-time card cannot pay the second charge. |
1988
2008
  | `converge_3ds_follow_up_refused` | The body follows a 3-D Secure or Pay Defense step the page already ran. | Start a new checkout from the merchant's page. A terminal that runs the step on every payment cannot be paid through Agentcard yet. |
1989
2009
  | `converge_surcharge_refused` | The body follows a surcharge quote, or skips it with the debit bypass. | Start a new checkout from the merchant's page. A surcharging terminal cannot be paid through Agentcard yet. |
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 6a0cf6104aac41a93fb4788532c63075d6831419127b76b87228dd7839c2a106
3
+ // artifact-sha256: 35f4549be2f1addcdcbaf1c7874a5022db72c1b74f27d85119b8c7da7757bd8c
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
@@ -75,6 +75,7 @@ var ONE_TIME_RECURRING_FLAG = "false";
75
75
  var CARD_SAVING_FLAGS = /* @__PURE__ */ Object.freeze(["ssl_get_token", "ssl_add_token"]);
76
76
  var CREDENTIAL_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_account_id", "ssl_merchant_id", "ssl_user_id", "ssl_pin", "ssl_vendor_id"]);
77
77
  var AMOUNT_ADD_ON_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_tip_amount", "ssl_cashback_amount"]);
78
+ var SOLVED_CAPTCHA = /^[A-Za-z0-9_-]{1,4096}$/;
78
79
  var CARD_NUMBER = /^\d{12,19}$/;
79
80
  var EXPIRY_MMYY = /^(0[1-9]|1[0-2])\d{2}$/;
80
81
  var CVC = /^\d{3,4}$/;
@@ -130,7 +131,7 @@ function convergeBodyVerdict(body) {
130
131
  return refuse("converge_credential_refused");
131
132
  if (WALLET_FIELDS.some((key) => owns(data, key)))
132
133
  return refuse("converge_wallet_refused");
133
- if (owns(parsed, "captchaToken"))
134
+ if (owns(parsed, "captchaToken") && !(typeof parsed.captchaToken === "string" && SOLVED_CAPTCHA.test(parsed.captchaToken)))
134
135
  return refuse("converge_captcha_refused");
135
136
  if (SERVICE_FEE_FIELDS.some((key) => owns(data, key)))
136
137
  return refuse("converge_service_fee_refused");
package/dist/cdp.js CHANGED
@@ -30,6 +30,7 @@ import { CARD_ENDPOINT_REFUSED_STEP_ANSWER, CARD_ENDPOINT_WITHHELD_ANSWER, CardE
30
30
  import { CardEndpointFactsWatch } from './card-endpoint-facts.js';
31
31
  import { PAYRAILS_GUARD_PATTERNS, PAYRAILS_INIT_BODY_LIMIT, PayrailsSessionCapture, isPayrailsHost, payrailsInitCandidate, recognizesPayrails, } from './payrails.js';
32
32
  import { cardEndpointStoppedPage, refusedNavigationOutcome } from './card-endpoint-page.js';
33
+ import { WorldlineMyCheckoutProducts } from './worldline-mycheckout.js';
33
34
  /**
34
35
  * How an adapter fails a paused request it will not let through: as a network error for a
35
36
  * CyberSource Flex request (refusedRequestFailure, so the Microform field reports it to the page)
@@ -1988,6 +1989,8 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
1988
1989
  let lastSubmitted = null;
1989
1990
  // The approved Datatrans answers, per frameId: the repeat rule (datatrans.ts).
1990
1991
  const datatransRepeats = new DatatransRepeats();
1992
+ // The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
1993
+ const worldlineProducts = new WorldlineMyCheckoutProducts();
1991
1994
  // The Shiji STS getCvvId companion of the getTokenId this attachment last continued (shiji-sts.ts).
1992
1995
  // A cancelled attachment drops it, and the device's CVV ciphertext with it, at once: no getCvvId
1993
1996
  // can use it any more (pre-claim.ts refuses every one once the lifecycle is aborted). The page,
@@ -2455,6 +2458,7 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
2455
2458
  frame: typeof frameId === 'string' && frameId ? frameId : undefined,
2456
2459
  payrails,
2457
2460
  shijiSts: { companions: shijiCompanions, page: () => cdpShijiPage(sessionId, frameId) },
2461
+ worldlineMyCheckout: worldlineProducts,
2458
2462
  });
2459
2463
  const preClaim = preClaimResult instanceof Promise ? await preClaimResult : preClaimResult;
2460
2464
  if (preClaim.kind !== 'pause')
@@ -3318,6 +3322,8 @@ export async function attachToPlaywright(page, opts) {
3318
3322
  let lastSubmitted = null;
3319
3323
  // The approved Datatrans answers, per Frame: the repeat rule (datatrans.ts).
3320
3324
  const datatransRepeats = new DatatransRepeats();
3325
+ // The card product each Worldline MyCheckout hosted checkout chose (worldline-mycheckout.ts).
3326
+ const worldlineProducts = new WorldlineMyCheckoutProducts();
3321
3327
  const frameOf = (request) => { try {
3322
3328
  return request.frame?.() ?? undefined;
3323
3329
  }
@@ -3457,6 +3463,7 @@ export async function attachToPlaywright(page, opts) {
3457
3463
  frame: frameOf(request),
3458
3464
  payrails,
3459
3465
  shijiSts: { companions: shijiCompanions, page: async () => (await playwrightShijiDocumentPage(request)) ?? `pw:none:${++playwrightShijiSeq}` },
3466
+ worldlineMyCheckout: worldlineProducts,
3460
3467
  });
3461
3468
  const preClaim = preClaimResult instanceof Promise ? await preClaimResult : preClaimResult;
3462
3469
  if (preClaim.kind !== 'pause')
package/dist/client.d.ts CHANGED
@@ -739,6 +739,12 @@ export interface PlaceholderCard {
739
739
  /** The stored card's expiry year, four digits ("2031"). */
740
740
  readonly expiryYear: string;
741
741
  }
742
+ /**
743
+ * The placeholder number for a network (lowercase, as GET /api/v2/vault_cards names it) on a
744
+ * processor's page: the processor's own number for that network when `overrides` names one, else
745
+ * the network's default, else undefined for a network with no placeholder.
746
+ */
747
+ export declare function placeholderNumber(brand: string, psp?: string, overrides?: Readonly<Record<string, Readonly<Record<string, string>>>>): string | undefined;
742
748
  /**
743
749
  * The recognizer says this processor is preparation-required, and no preparation
744
750
  * was passed. Its card may be sent only after the cardholder approved a
@@ -1336,12 +1342,16 @@ export declare class VaultClient {
1336
1342
  * preselects that card: without a pin the page preselects its own choice (its recommended
1337
1343
  * card, or the first card the cardholder's device can open), which can be another card. After
1338
1344
  * prepare(), pass the prepared checkout's `cardId`, the card the cardholder already approved.
1339
- * The facts hold only while the cardholder pays with that card. Throws PlaceholderCardError
1340
- * when there is no such card, its network has no placeholder, or it carries no expiry.
1345
+ * The facts hold only while the cardholder pays with that card. `psp` names the processor
1346
+ * whose card form the agent types into (the recognizer's name, `worldline_mycheckout`): a
1347
+ * processor that needs its own number for the card's network gets it, and every other gets the
1348
+ * network's default. Throws PlaceholderCardError when there is no such card, its network has no
1349
+ * placeholder, or it carries no expiry.
1341
1350
  */
1342
1351
  placeholderCard(input: {
1343
1352
  user: string;
1344
1353
  cardId?: string;
1354
+ psp?: string;
1345
1355
  signal?: AbortSignal;
1346
1356
  }): Promise<PlaceholderCard>;
1347
1357
  /** Wait for real device approval before the caller starts native tokenization. */
package/dist/client.js CHANGED
@@ -423,6 +423,35 @@ const PLACEHOLDER_NUMBERS = Object.freeze({
423
423
  discover: '6011000990139424',
424
424
  jcb: '3566111111111113',
425
425
  });
426
+ /**
427
+ * The placeholder number a processor needs in place of its network's default above, by processor
428
+ * (the recognizer's `psp`) and network. A processor whose page reads the typed number's IIN to
429
+ * pick a card product can read the default as another product than the card that pays:
430
+ * Worldline MyCheckout read 4111111111111111 as Visa Electron (122), not Visa (1), on CLZ's live
431
+ * page on 2026-10-12, and switched its form's product. The SDK refuses that submit
432
+ * (worldline-mycheckout.ts), so until this table names a Visa number Worldline reads as product 1,
433
+ * a Visa card cannot pay on a MyCheckout page that offers Visa Electron. The default table stays
434
+ * as it is: CyberSource Flex's Microform rule, ACI's brand check and EBANX's eligibility guard
435
+ * read its Visa and Mastercard numbers.
436
+ *
437
+ * TODO(Felipe): pick worldline_mycheckout's Visa number from Worldline Connect's own test-card
438
+ * list, then confirm it with one production IIN check on a MyCheckout card form that offers
439
+ * products 1 and 122: type it, wait for the IIN answer, read paymentProductId and the logo, clear
440
+ * the field, never Pay. Add it here as `worldline_mycheckout: { visa: '<number>' }`, with the
441
+ * check's date and merchant, and to the live runner's placeholder allowlist.
442
+ */
443
+ const PROCESSOR_PLACEHOLDER_NUMBERS = Object.freeze({});
444
+ /**
445
+ * The placeholder number for a network (lowercase, as GET /api/v2/vault_cards names it) on a
446
+ * processor's page: the processor's own number for that network when `overrides` names one, else
447
+ * the network's default, else undefined for a network with no placeholder.
448
+ */
449
+ export function placeholderNumber(brand, psp, overrides = PROCESSOR_PLACEHOLDER_NUMBERS) {
450
+ const own = psp !== undefined && Object.prototype.hasOwnProperty.call(overrides, psp) ? overrides[psp] : undefined;
451
+ if (own && Object.prototype.hasOwnProperty.call(own, brand))
452
+ return own[brand];
453
+ return Object.prototype.hasOwnProperty.call(PLACEHOLDER_NUMBERS, brand) ? PLACEHOLDER_NUMBERS[brand] : undefined;
454
+ }
426
455
  /**
427
456
  * The recognizer says this processor is preparation-required, and no preparation
428
457
  * was passed. Its card may be sent only after the cardholder approved a
@@ -1405,12 +1434,17 @@ export class VaultClient {
1405
1434
  * preselects that card: without a pin the page preselects its own choice (its recommended
1406
1435
  * card, or the first card the cardholder's device can open), which can be another card. After
1407
1436
  * prepare(), pass the prepared checkout's `cardId`, the card the cardholder already approved.
1408
- * The facts hold only while the cardholder pays with that card. Throws PlaceholderCardError
1409
- * when there is no such card, its network has no placeholder, or it carries no expiry.
1437
+ * The facts hold only while the cardholder pays with that card. `psp` names the processor
1438
+ * whose card form the agent types into (the recognizer's name, `worldline_mycheckout`): a
1439
+ * processor that needs its own number for the card's network gets it, and every other gets the
1440
+ * network's default. Throws PlaceholderCardError when there is no such card, its network has no
1441
+ * placeholder, or it carries no expiry.
1410
1442
  */
1411
1443
  async placeholderCard(input) {
1412
1444
  if (!input || typeof input.user !== 'string' || !input.user)
1413
1445
  throw new Error('placeholderCard needs the user whose stored card will pay.');
1446
+ if (input.psp !== undefined && (typeof input.psp !== 'string' || !input.psp))
1447
+ throw new Error('placeholderCard takes psp as the processor\'s name, such as worldline_mycheckout.');
1414
1448
  // The card list lives under /api/v2 only: the API aliases /v2/checkout alone without the prefix.
1415
1449
  const list = await this.get(`/api/v2/vault_cards?user_id=${encodeURIComponent(input.user)}`, input.signal);
1416
1450
  const cards = Array.isArray(list?.data) ? list.data : [];
@@ -1418,7 +1452,7 @@ export class VaultClient {
1418
1452
  if (!card || typeof card !== 'object' || typeof card.id !== 'string' || !card.id)
1419
1453
  throw new PlaceholderCardError('card_not_found');
1420
1454
  const brand = typeof card.brand === 'string' ? card.brand.toLowerCase() : '';
1421
- const number = Object.prototype.hasOwnProperty.call(PLACEHOLDER_NUMBERS, brand) ? PLACEHOLDER_NUMBERS[brand] : undefined;
1455
+ const number = placeholderNumber(brand, input.psp);
1422
1456
  if (!number)
1423
1457
  throw new PlaceholderCardError('brand_unsupported');
1424
1458
  const month = card.expiry_month, year = card.expiry_year;
@@ -7,9 +7,12 @@ export declare const CONVERGE_CAPABILITY = "converge_hpp_process";
7
7
  * the device refuses with, so an integrator reads one code whichever of them refused.
8
8
  * - A shape the Vault never pays: `converge_wallet_refused`, `converge_debit_card_refused`,
9
9
  * `converge_push_to_card_refused`, `converge_gift_card_refused`, `converge_payment_type_refused`,
10
- * `converge_captcha_refused`, `converge_service_fee_refused`, `converge_recurring_refused`,
10
+ * `converge_service_fee_refused`, `converge_recurring_refused`,
11
11
  * `converge_card_saving_refused` (ssl_get_token or ssl_add_token), `converge_amount_add_on_refused`
12
12
  * (a tip or cash back) and `converge_currency_refused` (anything but US dollars).
13
+ * - A CAPTCHA answer Google never issued: `converge_captcha_refused` (a captchaToken that is not a
14
+ * non-empty string of at most 4,096 URL-safe characters). A page with its CAPTCHA on pauses like
15
+ * any other once a person solved it, and the answer rides to Converge unchanged.
13
16
  * - A follow-up after a step the Vault cannot complete: `converge_3ds_follow_up_refused` (3-D
14
17
  * Secure or Pay Defense) and `converge_surcharge_refused`.
15
18
  * - A body the cardholder has nothing to approve on: `converge_amount_missing` and
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 009a8b902de51c4dadddae592b32d2edbd5b394f130938e1a41cd62a06a234a8
3
+ // artifact-sha256: 136e1c2b0fd2bdee0cfbf5efbbfaf1a92ace7d1cc52e17a3fd5a4584370e5e53
4
4
  // src/elavon-converge.js
5
5
  var CONVERGE_HOSTS = /* @__PURE__ */ Object.freeze(["api.convergepay.com", "api.demo.convergepay.com", "www.convergepay.com"]);
6
6
  var CONVERGE_DEMO_HOST = "api.demo.convergepay.com";
@@ -77,6 +77,7 @@ var ONE_TIME_RECURRING_FLAG = "false";
77
77
  var CARD_SAVING_FLAGS = /* @__PURE__ */ Object.freeze(["ssl_get_token", "ssl_add_token"]);
78
78
  var CREDENTIAL_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_account_id", "ssl_merchant_id", "ssl_user_id", "ssl_pin", "ssl_vendor_id"]);
79
79
  var AMOUNT_ADD_ON_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_tip_amount", "ssl_cashback_amount"]);
80
+ var SOLVED_CAPTCHA = /^[A-Za-z0-9_-]{1,4096}$/;
80
81
  var CARD_NUMBER = /^\d{12,19}$/;
81
82
  var EXPIRY_MMYY = /^(0[1-9]|1[0-2])\d{2}$/;
82
83
  var CVC = /^\d{3,4}$/;
@@ -132,7 +133,7 @@ function convergeBodyVerdict(body) {
132
133
  return refuse("converge_credential_refused");
133
134
  if (WALLET_FIELDS.some((key) => owns(data, key)))
134
135
  return refuse("converge_wallet_refused");
135
- if (owns(parsed, "captchaToken"))
136
+ if (owns(parsed, "captchaToken") && !(typeof parsed.captchaToken === "string" && SOLVED_CAPTCHA.test(parsed.captchaToken)))
136
137
  return refuse("converge_captcha_refused");
137
138
  if (SERVICE_FEE_FIELDS.some((key) => owns(data, key)))
138
139
  return refuse("converge_service_fee_refused");
@@ -25,9 +25,12 @@ export const CONVERGE_CAPABILITY = 'converge_hpp_process';
25
25
  * the device refuses with, so an integrator reads one code whichever of them refused.
26
26
  * - A shape the Vault never pays: `converge_wallet_refused`, `converge_debit_card_refused`,
27
27
  * `converge_push_to_card_refused`, `converge_gift_card_refused`, `converge_payment_type_refused`,
28
- * `converge_captcha_refused`, `converge_service_fee_refused`, `converge_recurring_refused`,
28
+ * `converge_service_fee_refused`, `converge_recurring_refused`,
29
29
  * `converge_card_saving_refused` (ssl_get_token or ssl_add_token), `converge_amount_add_on_refused`
30
30
  * (a tip or cash back) and `converge_currency_refused` (anything but US dollars).
31
+ * - A CAPTCHA answer Google never issued: `converge_captcha_refused` (a captchaToken that is not a
32
+ * non-empty string of at most 4,096 URL-safe characters). A page with its CAPTCHA on pauses like
33
+ * any other once a person solved it, and the answer rides to Converge unchanged.
31
34
  * - A follow-up after a step the Vault cannot complete: `converge_3ds_follow_up_refused` (3-D
32
35
  * Secure or Pay Defense) and `converge_surcharge_refused`.
33
36
  * - A body the cardholder has nothing to approve on: `converge_amount_missing` and
@@ -106,7 +106,14 @@ export interface CheckoutController {
106
106
  resume(result: ResumeResult): Promise<void>;
107
107
  /** Await device consent before the caller starts the first native Pay action. One use per attachment. */
108
108
  prepare(options: PrepareCheckoutOptions): Promise<PreparedCheckout>;
109
- /** Stop locally and best-effort retire an unbound preparation. Does not cancel a processor payment. */
109
+ /**
110
+ * Stop locally and best-effort retire an unbound preparation. Does not cancel a processor payment.
111
+ * A checkout that already ended keeps its state and reason (`completed`, `declined`, `failed`,
112
+ * `timed_out`, `unsupported`, `cancelled`, `outcome_unknown`) once no card request is in flight, or
113
+ * once the request in flight has its own outcome recorded (a cancel from onStateChange or the `failed` event).
114
+ * Any other reads `outcome_unknown` once a card request is in flight or an authorization exists,
115
+ * and `cancelled` before, with reason `local_cancel`.
116
+ */
110
117
  cancel(): void;
111
118
  /** Ask the application's merchant resolver. A rejection records unknown; never automatically retries payment. */
112
119
  reconcile(): Promise<Readonly<CheckoutState>>;
@@ -131,6 +138,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
131
138
  private state;
132
139
  private held;
133
140
  private active;
141
+ private settled;
134
142
  private cancelled;
135
143
  private merchantAborted;
136
144
  private unboundStripeToken;
package/dist/lifecycle.js CHANGED
@@ -2,6 +2,20 @@ import { AdyenTestPlatformRefusedError, ApprovalDeclinedError, ApprovalTimeoutEr
2
2
  import { CardEndpointRefusedError } from './card-endpoint.js';
3
3
  import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
4
4
  import { isWspayChallenge } from './wspay.js';
5
+ /**
6
+ * The statuses that end an attempt: its outcome is known (`completed`, `declined`, `failed`,
7
+ * `timed_out`, `unsupported`, `cancelled`), or the SDK already reported it unknown with its own
8
+ * reason (`outcome_unknown`). cancel() stops and holds the attachment but leaves these as they are,
9
+ * so an application that cancels at cleanup still reads the outcome, once no card request is in
10
+ * flight or the request in flight has its own outcome recorded. A checkout not started (`idle`) or
11
+ * still under way (`awaiting_approval`, `ready_to_submit`, `awaiting_merchant`, `requires_user_action`,
12
+ * or any status while a request whose outcome is not recorded is in flight) has none:
13
+ * cancelling it reads `outcome_unknown` once a request is in flight or an authorization exists,
14
+ * since a card or a token may still move, and `cancelled` before.
15
+ */
16
+ const FINAL_STATUSES = new Set([
17
+ 'completed', 'declined', 'failed', 'timed_out', 'unsupported', 'cancelled', 'outcome_unknown',
18
+ ]);
5
19
  /** How many times one approval's card-free Stripe confirmation may be checked before it is simply held. */
6
20
  export const MAX_STRIPE_CONTINUATION_CHECKS = 3;
7
21
  /**
@@ -29,6 +43,10 @@ export class CheckoutLifecycle {
29
43
  state = { status: 'idle', authorizationId: null };
30
44
  held = false;
31
45
  active = false;
46
+ // The card request in flight has its own outcome recorded (failed(), handedOff(), merchantNeverRetried()), and
47
+ // the adapter has yet to call end(): it still answers the page and tidies up. An application that cancels
48
+ // from onStateChange or the `failed` event runs in that window, and the request waits on nothing there.
49
+ settled = false;
32
50
  cancelled = false;
33
51
  merchantAborted = false;
34
52
  unboundStripeToken = false;
@@ -113,6 +131,7 @@ export class CheckoutLifecycle {
113
131
  }
114
132
  begin() {
115
133
  this.active = true;
134
+ this.settled = false;
116
135
  this.set({ status: 'awaiting_approval', authorizationId: null, ...(this.state.preparationId ? { preparationId: this.state.preparationId } : {}) });
117
136
  }
118
137
  end() { this.active = false; }
@@ -137,9 +156,20 @@ export class CheckoutLifecycle {
137
156
  catch { /* observer only */ }
138
157
  }
139
158
  cancel() {
159
+ // A checkout that already ended keeps its outcome: a refusal the processor gave, a payment the
160
+ // merchant confirmed, an unknown the SDK already named. Stopping the attachment afterwards
161
+ // changes nothing that happened, so only an attempt still under way reads as stopped here.
162
+ // A card request still in flight is under way whatever the state says: its authorization can
163
+ // still be approved on the cardholder's device, so cancelling it reads outcome_unknown as before.
164
+ // One whose own outcome is recorded (settled) is not: the adapter only has to finish answering
165
+ // the page, so a cancel from onStateChange or the `failed` event keeps that outcome.
166
+ // Read before the abort, so the state the application saw decides.
167
+ const ended = FINAL_STATUSES.has(this.state.status) && (!this.active || this.settled);
140
168
  this.cancelled = true;
141
169
  this.held = true;
142
170
  this.abort.abort();
171
+ if (ended)
172
+ return;
143
173
  this.set({ ...this.state, status: this.active || this.state.authorizationId ? 'outcome_unknown' : 'cancelled', reason: 'local_cancel' });
144
174
  }
145
175
  /** The exact browser request is gone; a late approval cannot reopen it. */
@@ -180,6 +210,7 @@ export class CheckoutLifecycle {
180
210
  merchantNeverRetried(authorizationId) {
181
211
  if (this.cancelled)
182
212
  return;
213
+ this.settled = true;
183
214
  this.set({ ...this.state, authorizationId, status: 'declined', reason: 'merchant_never_retried' });
184
215
  }
185
216
  /**
@@ -324,6 +355,7 @@ export class CheckoutLifecycle {
324
355
  handedOff(replay) {
325
356
  if (this.merchantAborted)
326
357
  return;
358
+ this.settled = true;
327
359
  if (this.ownedShopOrderId) {
328
360
  this.held = true;
329
361
  this.set({ status: 'completed', authorizationId: replay.authorizationId, mode: 'token', orderId: this.ownedShopOrderId });
@@ -359,6 +391,8 @@ export class CheckoutLifecycle {
359
391
  failed(error, handoffStarted = false) {
360
392
  if (this.cancelled)
361
393
  return;
394
+ // Before any state is set, so an observer that cancels from onStateChange sees the outcome as recorded.
395
+ this.settled = true;
362
396
  if (this.merchantAborted) {
363
397
  this.merchantRequestAborted();
364
398
  return;
@@ -12,6 +12,7 @@ import { type CybersourceFlexDecision, type CybersourceFlexPause, type FlexReque
12
12
  import { type CardEndpointProfile } from './card-endpoint.js';
13
13
  import { type PayrailsSessionCapture, type PayrailsSidecar } from './payrails.js';
14
14
  import { type VerifoneProfile } from './verifone.js';
15
+ import { type WorldlineMyCheckoutProducts } from './worldline-mycheckout.js';
15
16
  /** The native Stripe step the gate claimed, or null when none was: exactly what StripeCheckoutGate.claim returns. */
16
17
  type StripeStep = ReturnType<StripeCheckoutGate['claim']>;
17
18
  /** A synthetic response a gate fulfills locally. The effect applies CORS and encoding. */
@@ -183,6 +184,11 @@ export interface PreClaimContext {
183
184
  companions: ShijiStsCompanions;
184
185
  page: () => string | Promise<string>;
185
186
  };
187
+ /**
188
+ * The card products this attachment saw a Worldline MyCheckout page choose (worldline-mycheckout.ts).
189
+ * Absent, no MyCheckout card submit is held to a product here.
190
+ */
191
+ worldlineMyCheckout?: WorldlineMyCheckoutProducts;
186
192
  }
187
193
  /**
188
194
  * The URL an event may name for a request. For an armed merchant profile's
@@ -204,6 +210,7 @@ export declare const fiservPreClaim: PreClaimGate;
204
210
  export declare const ekashuPreClaim: PreClaimGate;
205
211
  export declare const paypalHostedFieldsPreClaim: PreClaimGate;
206
212
  export declare const cybersourceSecureAcceptancePreClaim: PreClaimGate;
213
+ export declare const worldlineMyCheckoutPreClaim: PreClaimGate;
207
214
  /** The processor gates, in order. A new family adds one entry here; the terminal
208
215
  * arbiter runs after all of them and is not part of this list. A gate appended
209
216
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
package/dist/pre-claim.js CHANGED
@@ -17,7 +17,8 @@
17
17
  // no new card, Shiji STS's getTokenId and getCvvId, which also carry
18
18
  // requests the vault must never pay, and PayPal's legacy Hosted Fields
19
19
  // confirm and CyberSource Secure Acceptance's Silent Order POST, whose card
20
- // no Vault mode takes over.
20
+ // no Vault mode takes over, and Worldline MyCheckout's card form, held to the
21
+ // card product its page chose.
21
22
  // The first three used to be
22
23
  // written inline in Fetch.requestPaused and again in the page.route handler, so
23
24
  // a decision could drift between the two adapters and a new family had to be
@@ -77,6 +78,7 @@ import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone
77
78
  import { ekashuEntryUrl, isEkashuEntry, isEkashuHost } from './ekashu.js';
78
79
  import { PAYPAL_HOSTED_FIELDS_REASON, isPaypalHostedFieldsConfirm, paypalHostedFieldsBody } from './paypal-hosted-fields.js';
79
80
  import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, isSecureAcceptanceFramePost, } from './cybersource-secure-acceptance.js';
81
+ import { isWorldlineMyCheckoutEndpoint } from './worldline-mycheckout.js';
80
82
  /**
81
83
  * The URL an event may name for a request. For an armed merchant profile's
82
84
  * endpoint or a sibling of it: the endpoint origin, the path TEMPLATE and the
@@ -1288,11 +1290,11 @@ async function payrailsUnsupported(ctx, reason) {
1288
1290
  // www.convergepay.com or api.demo.convergepay.com, also carries electronic check and financing payments
1289
1291
  // with no card, and card-shaped bodies the Vault never pays: a wallet with a card
1290
1292
  // number the server supplied, the debit, gift card and push-to-card tabs, the
1291
- // follow-ups after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA, a
1292
- // service fee, recurring or installment fields, card saving, a tip, another
1293
- // currency. So a POST the registry recognizes on a Converge host that carries a
1294
- // card is judged here, before the terminal arbiter would pause it, by the
1295
- // whole-request check the API runs at create:
1293
+ // follow-ups after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA answer
1294
+ // that is not a solved reCAPTCHA token, a service fee, recurring or installment
1295
+ // fields, card saving, a tip, another currency. So a POST the registry recognizes
1296
+ // on a Converge host that carries a card is judged here, before the terminal
1297
+ // arbiter would pause it, by the whole-request check the API runs at create:
1296
1298
  // - the page's own first credit card sale defers, and the arbiter pauses it
1297
1299
  // for approval;
1298
1300
  // - anything else aborts with a `blocked` event naming payment-core's code
@@ -1697,6 +1699,44 @@ async function cybersourceSecureAcceptanceStop(ctx) {
1697
1699
  await ctx.effects.abort().catch(() => { });
1698
1700
  return { kind: 'abort' };
1699
1701
  }
1702
+ // Worldline MyCheckout (worldline-mycheckout.ts). Once the registry serves its recognizer (a build
1703
+ // that declares worldline_mycheckout_form, for an org Agentcard turned Worldline on for), every POST
1704
+ // to its four card-form endpoints reaches this gate first:
1705
+ // - a product selection (no card, and neither the Pay nor the cancel pair: the post of the page's
1706
+ // select-payment-product step) is remembered for its hosted checkout and defers, and the adapter
1707
+ // continues it untouched as before (passWithoutCard);
1708
+ // - a card submit whose paymentProductId is another product than the one chosen for its hosted
1709
+ // checkout aborts with a `blocked` event, `worldline_mycheckout_product_changed`, and one that
1710
+ // names no product with `worldline_mycheckout_product_missing`, with no claim, create or prompt.
1711
+ // The page's IIN lookup on the agent's placeholder switched the product, and the cardholder's
1712
+ // device, which checks only the real card's network, would submit the card under that product.
1713
+ // Like Converge's refusals it neither holds the attachment nor changes its state, so the page's
1714
+ // next submit is judged afresh;
1715
+ // - anything else defers: a card submit with the chosen product, one on the card group, one on a
1716
+ // hosted checkout whose choice this attachment never saw, the page's cancel, a redirect product.
1717
+ // Synchronous until the abort, so a card submit pauses with no await. Exported for the gate
1718
+ // registry's tests.
1719
+ export const worldlineMyCheckoutPreClaim = (ctx) => {
1720
+ const { url, method } = ctx.request;
1721
+ if (!ctx.worldlineMyCheckout || String(method).toUpperCase() !== 'POST' || !isWorldlineMyCheckoutEndpoint(url)
1722
+ || !ctx.vault.isCardRequest(url, method))
1723
+ return undefined;
1724
+ let body;
1725
+ // A reader that throws (Playwright's postData() can) proves nothing either way.
1726
+ try {
1727
+ body = ctx.request.body;
1728
+ }
1729
+ catch {
1730
+ body = null;
1731
+ }
1732
+ const refusal = ctx.worldlineMyCheckout.judge(url, body);
1733
+ return refusal === null ? undefined : worldlineMyCheckoutBlock(ctx, refusal);
1734
+ };
1735
+ async function worldlineMyCheckoutBlock(ctx, reason) {
1736
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
1737
+ await ctx.effects.abort();
1738
+ return { kind: 'abort' };
1739
+ }
1700
1740
  // The terminal arbiter, always last. A request that no gate claimed and that is
1701
1741
  // neither a native Stripe step nor a recognized card request continues, unless
1702
1742
  // it matches a declared payment endpoint we do not support (then it aborts as an
@@ -1731,7 +1771,7 @@ async function unsupportedContinue(ctx) {
1731
1771
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
1732
1772
  * pauses each one itself. */
1733
1773
  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, paypalHostedFieldsPreClaim,
1734
- cybersourceSecureAcceptancePreClaim];
1774
+ cybersourceSecureAcceptancePreClaim, worldlineMyCheckoutPreClaim];
1735
1775
  /** Run each gate in order; the first to return a decision wins. If none does,
1736
1776
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
1737
1777
  * Returns synchronously while every gate defers synchronously (an ordinary card
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: bbfd8946fd3963bf32fe66d5c17ba50dd69e141187c1d5896ee60c33b3aa0e41
3
+ // artifact-sha256: 3f5ac5b4125cab96cc0851e9b1fe705aa96635e1df05b9216e951140fb47cd43
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
@@ -83,6 +83,7 @@ var ONE_TIME_RECURRING_FLAG = "false";
83
83
  var CARD_SAVING_FLAGS = /* @__PURE__ */ Object.freeze(["ssl_get_token", "ssl_add_token"]);
84
84
  var CREDENTIAL_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_account_id", "ssl_merchant_id", "ssl_user_id", "ssl_pin", "ssl_vendor_id"]);
85
85
  var AMOUNT_ADD_ON_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_tip_amount", "ssl_cashback_amount"]);
86
+ var SOLVED_CAPTCHA = /^[A-Za-z0-9_-]{1,4096}$/;
86
87
  var CARD_NUMBER = /^\d{12,19}$/;
87
88
  var EXPIRY_MMYY = /^(0[1-9]|1[0-2])\d{2}$/;
88
89
  var CVC = /^\d{3,4}$/;
@@ -138,7 +139,7 @@ function convergeBodyVerdict(body) {
138
139
  return refuse("converge_credential_refused");
139
140
  if (WALLET_FIELDS.some((key) => owns(data, key)))
140
141
  return refuse("converge_wallet_refused");
141
- if (owns(parsed, "captchaToken"))
142
+ if (owns(parsed, "captchaToken") && !(typeof parsed.captchaToken === "string" && SOLVED_CAPTCHA.test(parsed.captchaToken)))
142
143
  return refuse("converge_captcha_refused");
143
144
  if (SERVICE_FEE_FIELDS.some((key) => owns(data, key)))
144
145
  return refuse("converge_service_fee_refused");
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 57dffa8575e71c819c694d818e272c955303add0ed4b083d69bab1606cd6150f
3
+ // artifact-sha256: 0abf1d62c20303c4496e1d26b64b1c36ad517919d23f2d14e3f0b6763694551f
4
4
  // src/deep-freeze.js
5
5
  function deepFreeze(value) {
6
6
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
@@ -211,7 +211,18 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
211
211
  // The result page Tranzila's card form names when the terminal set no
212
212
  // return URL of its own (read on both hosts on 2026-10-10): the result
213
213
  // goes back to Tranzila, which already holds it. Matched exactly.
214
- processorResultUrls: ["https://direct.tranzila.com/done/", "https://directng.tranzila.com/process/fail"]
214
+ processorResultUrls: ["https://direct.tranzila.com/done/", "https://directng.tranzila.com/process/fail"],
215
+ // Tranzila's own ALTCHA proof of work, on every card form read on both
216
+ // hosts on 2026-10-10 (altcha 1.0.7, SHA-256, searched up to 50,000):
217
+ // Tranzila's script hashes the page's challenge until it finds the
218
+ // number, writes base64 JSON {algorithm, challenge, number, salt,
219
+ // signature, took} into `altcha`, and only then submits the form.
220
+ // Nobody answers anything and the proof names no browser, cookie or
221
+ // address, so it rides the device's one send as the page wrote it, once
222
+ // the create finds it is one solved proof (backend.js isAltchaProof).
223
+ // Every other challenge field is still refused (hosted_form_gated).
224
+ // Removing this line refuses `altcha` again.
225
+ proofOfWork: "altcha"
215
226
  }
216
227
  },
217
228
  {
@@ -0,0 +1,27 @@
1
+ /** Whether `url` is one of MyCheckout's four card-form endpoints, written exactly (payment-core's rule). */
2
+ export declare function isWorldlineMyCheckoutEndpoint(url: string): boolean;
3
+ /** The `blocked` detail for a card submit whose product is not the one its page chose. */
4
+ export declare const WORLDLINE_MYCHECKOUT_PRODUCT_CHANGED = "worldline_mycheckout_product_changed";
5
+ /** The `blocked` detail for a card submit that names no product, on a hosted checkout whose page chose one. */
6
+ export declare const WORLDLINE_MYCHECKOUT_PRODUCT_MISSING = "worldline_mycheckout_product_missing";
7
+ export type WorldlineMyCheckoutRefusal = typeof WORLDLINE_MYCHECKOUT_PRODUCT_CHANGED | typeof WORLDLINE_MYCHECKOUT_PRODUCT_MISSING;
8
+ /**
9
+ * The card products one attachment saw chosen, per hosted checkout: its `publicMerchantId` and
10
+ * `token`, the pair payment-core's duplicate guard reads as one hosted checkout
11
+ * (WORLDLINE_MYCHECKOUT_DEDUPE).
12
+ */
13
+ export declare class WorldlineMyCheckoutProducts {
14
+ private readonly chosen;
15
+ /**
16
+ * Read one POST to a card-form endpoint. A product selection (no card, and neither the Pay nor
17
+ * the cancel pair) records the product it chose for its hosted checkout, or forgets the choice
18
+ * when it picks the card group, which leaves the product to the card. A card submit is judged:
19
+ * WORLDLINE_MYCHECKOUT_PRODUCT_CHANGED when its paymentProductId is another product than the one
20
+ * chosen for its hosted checkout, WORLDLINE_MYCHECKOUT_PRODUCT_MISSING when it names none. Null for
21
+ * every other post, and for a card submit on the card group or on a hosted checkout whose choice
22
+ * this attachment never saw: the API and the device judge those as before. A body payment-core
23
+ * cannot read is never judged here either.
24
+ */
25
+ judge(url: string, body: string | null): WorldlineMyCheckoutRefusal | null;
26
+ private remember;
27
+ }
@@ -0,0 +1,29 @@
1
+ declare var WORLDLINE_MYCHECKOUT_PSP: string;
2
+ declare var WORLDLINE_MYCHECKOUT_CAPABILITY: string;
3
+ declare var WORLDLINE_MYCHECKOUT_ENDPOINTS: Readonly<{
4
+ production: readonly string[];
5
+ sandbox: readonly string[];
6
+ }>;
7
+ declare function worldlineMyCheckoutEnvironment(url: any): "production" | "sandbox" | undefined;
8
+ declare function isWorldlineMyCheckoutUrl(url: any): boolean;
9
+ declare var WORLDLINE_MYCHECKOUT_CARD_PRODUCTS: Readonly<{
10
+ visa: readonly string[];
11
+ mastercard: readonly string[];
12
+ amex: readonly string[];
13
+ discover: readonly string[];
14
+ jcb: readonly string[];
15
+ diners: readonly string[];
16
+ }>;
17
+ declare function worldlineMyCheckoutProductNetwork(productId: any): string | null;
18
+ declare function parseWorldlineMyCheckoutForm(body: any): {
19
+ refusal: string;
20
+ pairs?: undefined;
21
+ } | {
22
+ pairs: string[][];
23
+ refusal?: undefined;
24
+ };
25
+ declare function worldlineMyCheckoutCardholderName(name: any): string | null;
26
+ declare function worldlineMyCheckoutTemplateRefusal(body: any): string | null;
27
+ declare function worldlineMyCheckoutRequestRefusal(request: any): string | null;
28
+ declare var WORLDLINE_MYCHECKOUT_DEDUPE: readonly string[];
29
+ export { WORLDLINE_MYCHECKOUT_CAPABILITY, WORLDLINE_MYCHECKOUT_CARD_PRODUCTS, WORLDLINE_MYCHECKOUT_DEDUPE, WORLDLINE_MYCHECKOUT_ENDPOINTS, WORLDLINE_MYCHECKOUT_PSP, isWorldlineMyCheckoutUrl, parseWorldlineMyCheckoutForm, worldlineMyCheckoutCardholderName, worldlineMyCheckoutEnvironment, worldlineMyCheckoutProductNetwork, worldlineMyCheckoutRequestRefusal, worldlineMyCheckoutTemplateRefusal };
@@ -0,0 +1,162 @@
1
+ // @ts-nocheck
2
+ // Generated from @agent-cards/payment-core. Do not edit.
3
+ // artifact-sha256: 486a32aa0f96f59666d4a43e42fee9f493cd3059b27ad3f92b97ad6b249133f5
4
+ // src/worldline-mycheckout.js
5
+ var WORLDLINE_MYCHECKOUT_PSP = "worldline_mycheckout";
6
+ var WORLDLINE_MYCHECKOUT_CAPABILITY = "worldline_mycheckout_form";
7
+ var WORLDLINE_MYCHECKOUT_ENDPOINTS = /* @__PURE__ */ Object.freeze({
8
+ production: /* @__PURE__ */ Object.freeze([
9
+ "https://payment.pay1.checkout.worldline-solutions.com/checkout",
10
+ "https://payment.pay2.checkout.worldline-solutions.com/checkout"
11
+ ]),
12
+ sandbox: /* @__PURE__ */ Object.freeze([
13
+ "https://payment.pay1.preprod.checkout.worldline-solutions.com/checkout",
14
+ "https://payment.pay2.preprod.checkout.worldline-solutions.com/checkout"
15
+ ])
16
+ });
17
+ function worldlineMyCheckoutEnvironment(url) {
18
+ if (WORLDLINE_MYCHECKOUT_ENDPOINTS.production.includes(url))
19
+ return "production";
20
+ if (WORLDLINE_MYCHECKOUT_ENDPOINTS.sandbox.includes(url))
21
+ return "sandbox";
22
+ return void 0;
23
+ }
24
+ function isWorldlineMyCheckoutUrl(url) {
25
+ return worldlineMyCheckoutEnvironment(url) !== void 0;
26
+ }
27
+ var WORLDLINE_MYCHECKOUT_CARD_PRODUCTS = /* @__PURE__ */ Object.freeze({
28
+ visa: /* @__PURE__ */ Object.freeze(["1", "114", "122"]),
29
+ mastercard: /* @__PURE__ */ Object.freeze(["3", "119"]),
30
+ amex: /* @__PURE__ */ Object.freeze(["2"]),
31
+ discover: /* @__PURE__ */ Object.freeze(["128"]),
32
+ jcb: /* @__PURE__ */ Object.freeze(["125"]),
33
+ diners: /* @__PURE__ */ Object.freeze(["132"])
34
+ });
35
+ function worldlineMyCheckoutProductNetwork(productId) {
36
+ for (const [network, ids] of Object.entries(WORLDLINE_MYCHECKOUT_CARD_PRODUCTS)) {
37
+ if (ids.includes(productId))
38
+ return network;
39
+ }
40
+ return null;
41
+ }
42
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
43
+ var FIELDS = {
44
+ publicMerchantId: /^[1-9]\d{0,9}$/,
45
+ variantCode: /^\d{1,10}$/,
46
+ locale: /^[a-z]{2}_[A-Z]{2}$/,
47
+ idempotenceKey: UUID,
48
+ // <merchant id>-<32 hex>:<hosted checkout id>:<32 hex>, the hosted checkout's own id.
49
+ token: /^[1-9]\d{0,9}-[0-9a-f]{32}:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[0-9a-f]{32}$/,
50
+ requestToken: UUID,
51
+ isPaymentProductDetailsShown: /^(?:true|false)$/,
52
+ isPaymentProductFiscalNumberShown: /^(?:true|false)$/,
53
+ jsEnabled: /^true$/,
54
+ isFramed: /^(?:true|false)$/,
55
+ paymentProductId: /^\d{1,4}$/,
56
+ paymentProductGroupId: /^cards$/,
57
+ colorDepth: /^\d{0,3}$/,
58
+ screenHeight: /^\d{0,5}$/,
59
+ screenWidth: /^\d{0,5}$/,
60
+ innerHeight: /^\d{0,5}$/,
61
+ innerWidth: /^\d{0,5}$/,
62
+ timezoneOffsetUtcMinutes: /^-?\d{0,4}$/,
63
+ browserLocale: /^[A-Za-z0-9-]{0,35}$/,
64
+ javaEnabled: /^(?:true|false)?$/,
65
+ cardNumber: /^[0-9 ]{12,23}$/,
66
+ // MM/YY as the page's mask writes it; the delimiter may be absent or padded.
67
+ expiryDate: /^(?:0[1-9]|1[0-2])(?: ?\/ ?)?\d{2}$/,
68
+ cvv: /^\d{3,4}$/,
69
+ cardholderName: /^[^\u0000-\u001f\u007f<>]{0,100}$/,
70
+ // The autofill helpers the page folds into expiryDate and then empties.
71
+ expiryDateMonth: /^$/,
72
+ expiryDateYear: /^$/,
73
+ // A checked "remember" asks Worldline to keep the card for later purchases.
74
+ remember: /^$/,
75
+ // Present, empty, on a page that offers Click to Pay; a value is a Click to Pay payment.
76
+ clickToPayCheckoutResponseSignature: /^$/,
77
+ // The Pay button's own pair. The click handler writes it into #buttonValue and appends one
78
+ // more copy per click, so it may repeat, always as doPayment=doPayment.
79
+ doPayment: /^doPayment$/
80
+ };
81
+ var REPEATABLE = /* @__PURE__ */ new Set(["doPayment"]);
82
+ var REQUIRED = ["publicMerchantId", "token", "requestToken", "idempotenceKey", "doPayment", "cardNumber", "expiryDate", "cvv"];
83
+ var NOT_A_CARD_SALE = /* @__PURE__ */ new Set(["cancelPayment", "encryptedPaymentData", "mobileCardholderName"]);
84
+ var BODY_LIMIT = 16384;
85
+ var VALUE_LIMIT = 2048;
86
+ function parseWorldlineMyCheckoutForm(body) {
87
+ if (typeof body !== "string" || body === "" || body.length > BODY_LIMIT || /[^\x21-\x7e]/.test(body))
88
+ return { refusal: "body" };
89
+ const pairs = [];
90
+ const seen = /* @__PURE__ */ new Set();
91
+ for (const chunk of body.split("&")) {
92
+ const eq = chunk.indexOf("=");
93
+ if (eq < 1)
94
+ return { refusal: "body" };
95
+ let key;
96
+ let value;
97
+ try {
98
+ key = decodeURIComponent(chunk.slice(0, eq).replace(/\+/g, " "));
99
+ value = decodeURIComponent(chunk.slice(eq + 1).replace(/\+/g, " "));
100
+ }
101
+ catch {
102
+ return { refusal: "body" };
103
+ }
104
+ if (value.length > VALUE_LIMIT || /[\u0000-\u001f\u007f]/.test(value))
105
+ return { refusal: "body" };
106
+ if (seen.has(key) && !REPEATABLE.has(key))
107
+ return { refusal: "body" };
108
+ seen.add(key);
109
+ pairs.push([key, value]);
110
+ }
111
+ return { pairs };
112
+ }
113
+ var SESSION_FIELDS = /* @__PURE__ */ new Set(["publicMerchantId", "token", "requestToken", "idempotenceKey"]);
114
+ var CARD_FIELDS = /* @__PURE__ */ new Set(["cardNumber", "expiryDate", "cvv", "cardholderName", "expiryDateMonth", "expiryDateYear"]);
115
+ function worldlineMyCheckoutCardholderName(name) {
116
+ if (typeof name !== "string")
117
+ return null;
118
+ const trimmed = name.trim();
119
+ return trimmed !== "" && FIELDS.cardholderName.test(trimmed) ? trimmed : null;
120
+ }
121
+ function worldlineMyCheckoutTemplateRefusal(body) {
122
+ const parsed = parseWorldlineMyCheckoutForm(body);
123
+ if (parsed.refusal)
124
+ return parsed.refusal;
125
+ const values = /* @__PURE__ */ new Map();
126
+ for (const [key, value] of parsed.pairs) {
127
+ if (NOT_A_CARD_SALE.has(key) || key === "clickToPayCheckoutResponseSignature" && value !== "")
128
+ return "not_a_card_sale";
129
+ if (key === "remember" && value !== "")
130
+ return "stored_card";
131
+ if (!Object.hasOwn(FIELDS, key))
132
+ return "unknown_field";
133
+ values.set(key, value);
134
+ }
135
+ for (const key of REQUIRED)
136
+ if (!values.has(key))
137
+ return "missing_field";
138
+ for (const [key, value] of parsed.pairs) {
139
+ if (FIELDS[key].test(value))
140
+ continue;
141
+ return SESSION_FIELDS.has(key) ? "session" : CARD_FIELDS.has(key) ? "card_fields" : "field_value";
142
+ }
143
+ if (values.get("token").split("-")[0] !== values.get("publicMerchantId"))
144
+ return "session";
145
+ const product = values.get("paymentProductId");
146
+ if (product === void 0 ? !values.has("paymentProductGroupId") : worldlineMyCheckoutProductNetwork(product) === null)
147
+ return "product";
148
+ return null;
149
+ }
150
+ function worldlineMyCheckoutRequestRefusal(request) {
151
+ if (!request || typeof request !== "object" || !isWorldlineMyCheckoutUrl(request.url))
152
+ return "endpoint";
153
+ if (request.method !== "POST")
154
+ return "method";
155
+ const headers = request.headers && typeof request.headers === "object" ? request.headers : {};
156
+ const types = Object.entries(headers).filter(([name]) => name.toLowerCase() === "content-type");
157
+ if (types.length !== 1 || typeof types[0][1] !== "string" || !/^application\/x-www-form-urlencoded(?:;\s*charset=utf-8)?$/i.test(types[0][1].trim()))
158
+ return "content_type";
159
+ return worldlineMyCheckoutTemplateRefusal(request.body);
160
+ }
161
+ var WORLDLINE_MYCHECKOUT_DEDUPE = /* @__PURE__ */ Object.freeze(["publicMerchantId", "token"]);
162
+ export { WORLDLINE_MYCHECKOUT_CAPABILITY, WORLDLINE_MYCHECKOUT_CARD_PRODUCTS, WORLDLINE_MYCHECKOUT_DEDUPE, WORLDLINE_MYCHECKOUT_ENDPOINTS, WORLDLINE_MYCHECKOUT_PSP, isWorldlineMyCheckoutUrl, parseWorldlineMyCheckoutForm, worldlineMyCheckoutCardholderName, worldlineMyCheckoutEnvironment, worldlineMyCheckoutProductNetwork, worldlineMyCheckoutRequestRefusal, worldlineMyCheckoutTemplateRefusal };
@@ -0,0 +1,93 @@
1
+ // Worldline Connect MyCheckout (psp `worldline_mycheckout`): the hosted payment page whose card
2
+ // form posts the card as a top-level navigation to /checkout on Worldline's own host. The form
3
+ // rules are payment-core's (worldline-mycheckout.generated.ts): the API applies them at create,
4
+ // and the cardholder's device applies them again before it rebuilds the form with the real card.
5
+ // This module holds the one thing only the SDK can decide: which card product the page chose.
6
+ //
7
+ // A MyCheckout page that offers several card products asks the shopper to choose one first (its
8
+ // select-payment-product page: Visa 1, Mastercard 3, Visa Electron 122, and so on). That choice
9
+ // posts to the same /checkout URL with no card, and continues untouched. The card form it opens
10
+ // then looks the typed number's IIN up with Worldline and, when the answer names another product
11
+ // the page offers, switches its own paymentProductId to that product. With the agent's placeholder
12
+ // typed, the answer describes the placeholder, never the card that pays: on 2026-10-12 CLZ's page
13
+ // read the SDK's Visa placeholder (411111) as Visa Electron (122) and switched the form from the
14
+ // Visa (1) the agent chose. The cardholder's device checks only that the product belongs to the
15
+ // real card's network (Visa takes 1, 114 and 122), so it would have submitted a Visa credit card
16
+ // as Visa Electron. The device never sees the choice and the API stores only the card form; this
17
+ // attachment sees both posts, so the SDK holds the card submit to the product chosen for its
18
+ // hosted checkout.
19
+ //
20
+ // The processor ships dark: the API serves the recognizer only to a build that declares
21
+ // `worldline_mycheckout_form`, for an org Agentcard turned Worldline on for. Without it no
22
+ // MyCheckout post pauses, and none reaches this module.
23
+ import { isWorldlineMyCheckoutUrl, parseWorldlineMyCheckoutForm } from './worldline-mycheckout.generated.js';
24
+ /** Whether `url` is one of MyCheckout's four card-form endpoints, written exactly (payment-core's rule). */
25
+ export function isWorldlineMyCheckoutEndpoint(url) {
26
+ return isWorldlineMyCheckoutUrl(url) === true;
27
+ }
28
+ /** The `blocked` detail for a card submit whose product is not the one its page chose. */
29
+ export const WORLDLINE_MYCHECKOUT_PRODUCT_CHANGED = 'worldline_mycheckout_product_changed';
30
+ /** The `blocked` detail for a card submit that names no product, on a hosted checkout whose page chose one. */
31
+ export const WORLDLINE_MYCHECKOUT_PRODUCT_MISSING = 'worldline_mycheckout_product_missing';
32
+ /** How many hosted checkouts one attachment remembers a choice for; the oldest is forgotten first. */
33
+ const REMEMBERED_CHECKOUTS = 32;
34
+ const PRODUCT_ID = /^\d{1,4}$/;
35
+ /**
36
+ * The card products one attachment saw chosen, per hosted checkout: its `publicMerchantId` and
37
+ * `token`, the pair payment-core's duplicate guard reads as one hosted checkout
38
+ * (WORLDLINE_MYCHECKOUT_DEDUPE).
39
+ */
40
+ export class WorldlineMyCheckoutProducts {
41
+ chosen = new Map();
42
+ /**
43
+ * Read one POST to a card-form endpoint. A product selection (no card, and neither the Pay nor
44
+ * the cancel pair) records the product it chose for its hosted checkout, or forgets the choice
45
+ * when it picks the card group, which leaves the product to the card. A card submit is judged:
46
+ * WORLDLINE_MYCHECKOUT_PRODUCT_CHANGED when its paymentProductId is another product than the one
47
+ * chosen for its hosted checkout, WORLDLINE_MYCHECKOUT_PRODUCT_MISSING when it names none. Null for
48
+ * every other post, and for a card submit on the card group or on a hosted checkout whose choice
49
+ * this attachment never saw: the API and the device judge those as before. A body payment-core
50
+ * cannot read is never judged here either.
51
+ */
52
+ judge(url, body) {
53
+ if (typeof body !== 'string' || !isWorldlineMyCheckoutUrl(url))
54
+ return null;
55
+ const parsed = parseWorldlineMyCheckoutForm(body);
56
+ if (!parsed.pairs)
57
+ return null;
58
+ const values = new Map();
59
+ for (const [key, value] of parsed.pairs)
60
+ if (!values.has(key))
61
+ values.set(key, value);
62
+ const merchant = values.get('publicMerchantId');
63
+ const token = values.get('token');
64
+ if (!merchant || !token)
65
+ return null;
66
+ const checkout = JSON.stringify([merchant, token]);
67
+ const product = values.get('paymentProductId');
68
+ // A card is a non-empty cardNumber, as the recognizer's card fields read it (card-fields.js).
69
+ if (!values.get('cardNumber')) {
70
+ if (values.has('doPayment') || values.has('cancelPayment'))
71
+ return null;
72
+ if (product !== undefined && PRODUCT_ID.test(product))
73
+ this.remember(checkout, product);
74
+ else if (values.get('paymentProductGroupId') === 'cards')
75
+ this.chosen.delete(checkout);
76
+ return null;
77
+ }
78
+ if (values.has('paymentProductGroupId'))
79
+ return null;
80
+ const chosen = this.chosen.get(checkout);
81
+ if (chosen === undefined)
82
+ return null;
83
+ if (!product)
84
+ return WORLDLINE_MYCHECKOUT_PRODUCT_MISSING;
85
+ return product === chosen ? null : WORLDLINE_MYCHECKOUT_PRODUCT_CHANGED;
86
+ }
87
+ remember(checkout, product) {
88
+ this.chosen.delete(checkout);
89
+ this.chosen.set(checkout, product);
90
+ while (this.chosen.size > REMEMBERED_CHECKOUTS)
91
+ this.chosen.delete(this.chosen.keys().next().value);
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/sdk",
3
- "version": "0.23.10",
3
+ "version": "0.23.11",
4
4
  "description": "Agentcard's SDK: one client, Agentcard, with two modules. checkout pauses a browser purchase and pays it with the person's own card; voice connects a person to your product once and approves a purchase by voice inside it. Card data never enters your process.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",