@agent-cards/sdk 0.23.9 → 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.
Files changed (65) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/PREFLIGHT.md +7 -5
  3. package/README.md +211 -102
  4. package/dist/aci.d.ts +7 -5
  5. package/dist/aci.generated.d.ts +2 -1
  6. package/dist/aci.generated.js +14 -4
  7. package/dist/aci.js +28 -7
  8. package/dist/amadeus-checkout.generated.js +2 -2
  9. package/dist/builtin-registry.generated.js +1 -1
  10. package/dist/card-fields.generated.js +165 -10
  11. package/dist/cdp.js +86 -34
  12. package/dist/client.d.ts +73 -33
  13. package/dist/client.js +136 -45
  14. package/dist/creditguard.generated.d.ts +1 -1
  15. package/dist/creditguard.generated.js +1 -3
  16. package/dist/cse-body.d.ts +7 -6
  17. package/dist/cse-body.js +8 -7
  18. package/dist/datatrans.generated.js +8 -5
  19. package/dist/elavon-converge.d.ts +5 -2
  20. package/dist/elavon-converge.generated.js +9 -5
  21. package/dist/elavon-converge.js +8 -4
  22. package/dist/fiserv.d.ts +9 -5
  23. package/dist/fiserv.generated.d.ts +3 -3
  24. package/dist/fiserv.generated.js +50 -20
  25. package/dist/fiserv.js +10 -6
  26. package/dist/lifecycle.d.ts +9 -1
  27. package/dist/lifecycle.js +34 -0
  28. package/dist/payrails.d.ts +78 -18
  29. package/dist/payrails.generated.d.ts +12 -2
  30. package/dist/payrails.generated.js +213 -18
  31. package/dist/payrails.js +165 -44
  32. package/dist/pre-claim.d.ts +7 -0
  33. package/dist/pre-claim.js +98 -36
  34. package/dist/preflight-capabilities.generated.d.ts +0 -106
  35. package/dist/preflight-capabilities.generated.js +0 -153
  36. package/dist/preflight-catalog.json +61 -181
  37. package/dist/preflight-schemas.json +6 -6
  38. package/dist/preflight.generated.d.ts +0 -106
  39. package/dist/preflight.generated.js +27 -15
  40. package/dist/preparation.d.ts +2 -0
  41. package/dist/preparation.js +4 -2
  42. package/dist/prepared-processor.d.ts +19 -5
  43. package/dist/prepared-processor.js +21 -5
  44. package/dist/primer.generated.d.ts +5 -1
  45. package/dist/primer.generated.js +202 -34
  46. package/dist/registry.d.ts +18 -12
  47. package/dist/registry.js +11 -8
  48. package/dist/shiji-sts.d.ts +24 -22
  49. package/dist/shiji-sts.generated.d.ts +1 -1
  50. package/dist/shiji-sts.generated.js +12 -8
  51. package/dist/shiji-sts.js +49 -46
  52. package/dist/stripe-checkout.generated.js +157 -49
  53. package/dist/verifone.generated.js +192 -17
  54. package/dist/verifone.js +1 -1
  55. package/dist/worldline-mycheckout.d.ts +27 -0
  56. package/dist/worldline-mycheckout.generated.d.ts +29 -0
  57. package/dist/worldline-mycheckout.generated.js +162 -0
  58. package/dist/worldline-mycheckout.js +93 -0
  59. package/dist/wspay.d.ts +17 -16
  60. package/dist/wspay.generated.d.ts +6 -1
  61. package/dist/wspay.generated.js +13 -3
  62. package/dist/wspay.js +9 -11
  63. package/examples/preflight/kernel-native/README.md +1 -1
  64. package/examples/preflight/kernel-native/inventory.json +1 -19
  65. package/package.json +3 -3
package/dist/client.js CHANGED
@@ -13,7 +13,7 @@ import { merchantSessionCapture } from './merchant-session-probe.js';
13
13
  import { creditguardPageProcessUrl, creditguardPpDomainHost, creditguardPreparationEnvironment, creditguardProcessTxId, sanitizeCreditguardResponse, } from './creditguard.generated.js';
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
- import { AMADEUS_CHECKOUT_CAPABILITY } from './amadeus-checkout.generated.js';
16
+ import { AMADEUS_CHECKOUT_CAPABILITY, AMADEUS_CHECKOUT_PSP } from './amadeus-checkout.generated.js';
17
17
  import { classifyStripeCheckoutRequest, encodeStripeCheckoutContext, hasStripeCheckoutMarker, parseStripeCheckoutContext, parseStripeCheckoutResponse, STRIPE_CHECKOUT_CONTEXT_HEADER, } from './stripe-checkout.generated.js';
18
18
  import { VoiceModule } from './voice.js';
19
19
  import { BuyModule } from './buy.js';
@@ -29,7 +29,7 @@ import { merchantCardEndpointFactsRefusal as sharedFactsRefusal, readMerchantCar
29
29
  import { DATATRANS_CAPABILITY } from './datatrans.js';
30
30
  import { PELECARD_CAPABILITY, PELECARD_PSP } from './pelecard.js';
31
31
  import { PRIMER_CAPABILITY, PRIMER_PSP, primerTokenAnswer } from './primer.js';
32
- import { PAYRAILS_CAPABILITY, payrailsSaveAnswer } from './payrails.js';
32
+ import { PAYRAILS_CAPABILITY, isPayrailsPaymentUrl, payrailsAnswer } from './payrails.js';
33
33
  import { WSPAY_CAPABILITY, WSPAY_PSP } from './wspay.js';
34
34
  import { SECURE_ACCEPTANCE_CAPABILITY, SECURE_ACCEPTANCE_FAST_POLL_MS, SECURE_ACCEPTANCE_PSP, SECURE_ACCEPTANCE_REPLY_WINDOW_MS, secureAcceptanceCheckoutProfile, secureAcceptanceCheckoutRefusal, secureAcceptanceFrameAnswer, } from './cybersource-secure-acceptance.js';
35
35
  /**
@@ -37,7 +37,7 @@ import { SECURE_ACCEPTANCE_CAPABILITY, SECURE_ACCEPTANCE_FAST_POLL_MS, SECURE_AC
37
37
  * only recognizers in these modes, so a request this build cannot complete
38
38
  * is never paused) and sent on every create. `device_encrypt`: the adapters
39
39
  * capture the Payrails session key the page's init payload names (payrails.ts),
40
- * and the cardholder's device encrypts and sends the save.
40
+ * and the cardholder's device encrypts and sends the save or card-form payment.
41
41
  */
42
42
  export const SUPPORTED_MODES = ['token', 'cse', 'hosted_form', 'device_encrypt'];
43
43
  /**
@@ -107,14 +107,17 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
107
107
  * `primer_payment_instruments`: Primer's card tokenization, which this build
108
108
  * judges before it pauses (primer.ts) and hands back to the page only as a
109
109
  * single-use token. A merchant's client session can save the card after the
110
- * payment with no sign in that request, so the API creates a Primer
111
- * authorization only for a test-mode client and refuses a live one (400
112
- * invalid_primer_request, live_keep_on_file) until the cardholder can consent
113
- * to that save; this build then aborts the paused request with nothing sent. A
114
- * gated recognizer never sits in BUILTIN_REGISTRY, so it is paused only after a
115
- * sync.
116
- * `payrails_device_encrypt`: Payrails' card-form save, which this build pauses
117
- * only once it has bound the save to a captured session (payrails.ts). A gated
110
+ * payment with no sign in that request, so the API creates a live Primer
111
+ * authorization only when it names the checkout page's https origin
112
+ * (checkout_origin, which this build sends from the page's origin): the
113
+ * approval says the merchant keeps the card on file, under the merchant name
114
+ * the create sent, which Agentcard cannot confirm, and pays only with a card
115
+ * the person stored. A refused create aborts the paused request with nothing
116
+ * sent. A gated recognizer never sits in BUILTIN_REGISTRY, so it is paused only
117
+ * after a sync.
118
+ * `payrails_device_encrypt`: Payrails' card save and card-form payment, which
119
+ * this build pauses only once it has bound the request to a captured session
120
+ * (payrails.ts). A gated
118
121
  * recognizer never sits in BUILTIN_REGISTRY, so it is paused only after a sync.
119
122
  * `converge_hpp_process`: Elavon Converge's Hosted Payment Page card request
120
123
  * (elavon-converge.ts), paused in token mode and judged by the pre-claim gate.
@@ -166,8 +169,24 @@ export const SUPPORTED_REGISTRY_FEATURES = ['card_fields', 'checkout_sessions'];
166
169
  * finds a card in it (cybersource-secure-acceptance.ts). Served on the same terms,
167
170
  * so every other company keeps the stop every build applies to a card posted to
168
171
  * Secure Acceptance.
172
+ * `gravy_session_fields_put`: Gravy Secure Fields' update of a checkout session,
173
+ * the one card request a recognizer declares as a PUT. This build pauses that PUT
174
+ * from Gravy's controller frame as it pauses a POST (isRecognizerRequest), creates
175
+ * the authorization with the PUT, and answers the frame with Gravy's own answer to
176
+ * the cardholder's device. Served on the same terms; an older build never sees the
177
+ * entry, and every company Agentcard has not turned Gravy on for keeps today's path,
178
+ * where the request continues to Gravy untouched.
179
+ * `moneris_hosted_tokenization`: Moneris Hosted Tokenization's card request, the
180
+ * frame's form post to /HPPtoken/request.php. This build pauses it only when it
181
+ * carries the number, the expiry and the CVD the cardholder's device fills, fails a
182
+ * frame that leaves the expiry or CVD to the merchant's page in the browser with a
183
+ * `blocked` event, `request_without_card`, and answers with Moneris's token as the
184
+ * API recorded it. The device sends the card through the checkout relay. Served on
185
+ * the same terms: only to a build that lists it, for a company Agentcard turned
186
+ * Moneris on for. A gated recognizer never sits in BUILTIN_REGISTRY, so it is paused
187
+ * only after a sync.
169
188
  */
170
- 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];
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'];
171
190
  /**
172
191
  * Processors whose paused request names no amount at all, so the pair the
173
192
  * agent passes is the amount the cardholder approves: authorize() refuses one
@@ -185,12 +204,16 @@ export const SUPPORTED_CAPABILITIES = ['fiserv_card_capture', GMO_CAPABILITY, CY
185
204
  * it creates a token and charges nothing, and the merchant's server charges its
186
205
  * own cart total later, so the agent's pair, which the prepared checkout already
187
206
  * carries, is the total the cardholder approves, and the card that pays can spend
188
- * no more than that (the API's card rule).
207
+ * no more than that (the API's card rule). Moneris Hosted Tokenization's
208
+ * request.php carries the card and no price: the merchant's server charges the
209
+ * token later, so the agent's pair, above zero (MONERIS_PSP below), is the one
210
+ * figure the approval shows.
189
211
  */
190
- const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP];
212
+ const MONERIS_PSP = 'moneris';
213
+ const AGENT_AMOUNT_PROCESSORS = [GMO_PSP, PELECARD_PSP, WSPAY_PSP, SECURE_ACCEPTANCE_PSP, MONERIS_PSP];
191
214
  /** How AmountRequiredError names a processor. */
192
215
  const PROCESSOR_NAMES = { [GMO_PSP]: 'GMO', [PELECARD_PSP]: 'Pelecard', [WSPAY_PSP]: 'WSPay',
193
- [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance' };
216
+ [SECURE_ACCEPTANCE_PSP]: 'CyberSource Secure Acceptance', [MONERIS_PSP]: 'Moneris' };
194
217
  /**
195
218
  * Processors whose request and answer name no amount at all, so the total the
196
219
  * payment page shows (`pageAmount`) is the one figure that ties the agent's pair
@@ -400,6 +423,35 @@ const PLACEHOLDER_NUMBERS = Object.freeze({
400
423
  discover: '6011000990139424',
401
424
  jcb: '3566111111111113',
402
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
+ }
403
455
  /**
404
456
  * The recognizer says this processor is preparation-required, and no preparation
405
457
  * was passed. Its card may be sent only after the cardholder approved a
@@ -1162,8 +1214,9 @@ export class VaultClient {
1162
1214
  }
1163
1215
  /**
1164
1216
  * The recogniser that claims a card request, or null; what a paused request names as
1165
- * `recognizer`. A POST to a recognised endpoint, or for a recognizer whose card request is a
1166
- * GET (Worldpay eProtect) that exact GET, query included.
1217
+ * `recognizer`. A POST to a recognised endpoint, a PUT for a recognizer whose card request is
1218
+ * a PUT (Gravy), or for a recognizer whose card request is a GET (Worldpay eProtect) that
1219
+ * exact GET, query included.
1167
1220
  */
1168
1221
  recognizerOf(url, method = 'POST') {
1169
1222
  const rec = findRecognizer(url, this.mergedRegistry(), { sandbox: this.sandbox });
@@ -1189,7 +1242,8 @@ export class VaultClient {
1189
1242
  // payrails_device_encrypt, converge_hpp_process, worldpay_eprotect_jsonp,
1190
1243
  // wspay_confirm, shiji_sts_pinned_key, amadeus_checkout_actions.v1,
1191
1244
  // worldline_mycheckout_form, verifone_card_capture, ekashu_hosted_payment_page,
1192
- // cybersource_sa_token_create) and
1245
+ // cybersource_sa_token_create, gravy_session_fields_put,
1246
+ // moneris_hosted_tokenization) and
1193
1247
  // the caller's company is one Agentcard turned that processor on for, so this
1194
1248
  // build arms Fiserv's card capture, GMO's getToken, CyberSource Flex's card
1195
1249
  // fields, Juspay's card flow, Datatrans's Secure Fields card post, the
@@ -1197,8 +1251,9 @@ export class VaultClient {
1197
1251
  // CreditGuard's hosted page submit, Primer's card tokenization, Payrails' card
1198
1252
  // save, Converge's payment page, Worldpay eProtect's registration, WSPay's
1199
1253
  // card confirm, Shiji STS's card leg, Amadeus Checkout's card add,
1200
- // Worldline MyCheckout's card form and CyberSource Secure Acceptance's token
1201
- // create only for
1254
+ // Worldline MyCheckout's card form, CyberSource Secure Acceptance's token
1255
+ // create, Gravy's session fields PUT and Moneris Hosted
1256
+ // Tokenization's card request only for
1202
1257
  // such a company, and an older build never receives a recognizer it cannot
1203
1258
  // judge. Every other org's registry never recognizes a Flex, Juspay, Datatrans,
1204
1259
  // Pelecard, ACI, CreditGuard, Primer, Payrails, Converge, eProtect, WSPay, Shiji STS, Amadeus or
@@ -1293,8 +1348,9 @@ export class VaultClient {
1293
1348
  }
1294
1349
  /**
1295
1350
  * True when this request is a card tokenization we can take over: a POST to
1296
- * a recognized endpoint, or for a recognizer whose card request is a GET
1297
- * (Worldpay eProtect) that exact GET, query included.
1351
+ * a recognized endpoint, a PUT for a recognizer whose card request is a PUT
1352
+ * (Gravy), or for a recognizer whose card request is a GET (Worldpay eProtect)
1353
+ * that exact GET, query included.
1298
1354
  */
1299
1355
  isCardRequest(url, method = 'POST') {
1300
1356
  return this.recognizerOf(url, method) !== null;
@@ -1378,12 +1434,17 @@ export class VaultClient {
1378
1434
  * preselects that card: without a pin the page preselects its own choice (its recommended
1379
1435
  * card, or the first card the cardholder's device can open), which can be another card. After
1380
1436
  * prepare(), pass the prepared checkout's `cardId`, the card the cardholder already approved.
1381
- * The facts hold only while the cardholder pays with that card. Throws PlaceholderCardError
1382
- * 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.
1383
1442
  */
1384
1443
  async placeholderCard(input) {
1385
1444
  if (!input || typeof input.user !== 'string' || !input.user)
1386
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.');
1387
1448
  // The card list lives under /api/v2 only: the API aliases /v2/checkout alone without the prefix.
1388
1449
  const list = await this.get(`/api/v2/vault_cards?user_id=${encodeURIComponent(input.user)}`, input.signal);
1389
1450
  const cards = Array.isArray(list?.data) ? list.data : [];
@@ -1391,7 +1452,7 @@ export class VaultClient {
1391
1452
  if (!card || typeof card !== 'object' || typeof card.id !== 'string' || !card.id)
1392
1453
  throw new PlaceholderCardError('card_not_found');
1393
1454
  const brand = typeof card.brand === 'string' ? card.brand.toLowerCase() : '';
1394
- const number = Object.prototype.hasOwnProperty.call(PLACEHOLDER_NUMBERS, brand) ? PLACEHOLDER_NUMBERS[brand] : undefined;
1455
+ const number = placeholderNumber(brand, input.psp);
1395
1456
  if (!number)
1396
1457
  throw new PlaceholderCardError('brand_unsupported');
1397
1458
  const month = card.expiry_month, year = card.expiry_year;
@@ -1474,6 +1535,12 @@ export class VaultClient {
1474
1535
  throw fail('secure_acceptance_profile_id_invalid');
1475
1536
  if (secureAcceptance && !input.cardId)
1476
1537
  throw fail('card_required');
1538
+ // Amadeus Checkout: Air France and KLM keep the expiry in their own form, where the agent types the
1539
+ // paying card's real expiry (placeholderCard), so the preparation names that card. The API refuses
1540
+ // one without it and approves no other card; refusing here asks nobody to approve it.
1541
+ const amadeus = input.psp === AMADEUS_CHECKOUT_PSP;
1542
+ if (amadeus && !input.cardId)
1543
+ throw fail('card_required');
1477
1544
  // CyberSource Flex, tokenized before Pay: the loaded card field's capture context and fields,
1478
1545
  // verified here against the keys payment-core pins before anything is created (the API and the
1479
1546
  // cardholder's device verify them again).
@@ -1596,6 +1663,8 @@ export class VaultClient {
1596
1663
  || (eprotect && (state.paypage_id !== input.paypageId || state.card_id !== input.cardId))
1597
1664
  // Secure Acceptance: the merchant profile and the card the cardholder approved are the ones asked for.
1598
1665
  || (secureAcceptance && (state.secure_acceptance_profile_id !== secureAcceptanceProfileId || state.card_id !== input.cardId))
1666
+ // Amadeus: the card the cardholder approved is the one whose expiry the agent typed.
1667
+ || (amadeus && state.card_id !== input.cardId)
1599
1668
  // The API says a preparation tokenizes before Pay exactly when this one carried a capture context.
1600
1669
  || (state.tokenize_before_pay === true) !== !!flexContext)
1601
1670
  throw fail('ready_unconfirmed', id);
@@ -1863,7 +1932,12 @@ export class VaultClient {
1863
1932
  // payrails_session_key_required.
1864
1933
  const sidecar = input.request.payrails;
1865
1934
  if (mode === 'device_encrypt' && (!sidecar || typeof sidecar.encryption_public_key !== 'string' || !sidecar.encryption_public_key)) {
1866
- throw new Error('A Payrails card save needs request.payrails.encryption_public_key, the session key from the checkout\'s Payrails init payload (the adapters capture it). Nothing was created.');
1935
+ throw new Error('A Payrails card save or card-form payment needs request.payrails.encryption_public_key, the session key from the checkout\'s Payrails init payload (the adapters capture it). Nothing was created.');
1936
+ }
1937
+ // A card-form payment's ciphertext carries the session's holderReference, which its body does not.
1938
+ const payrailsPayment = mode === 'device_encrypt' && isPayrailsPaymentUrl(input.request.url);
1939
+ if (payrailsPayment && (typeof sidecar?.holder_reference !== 'string' || !sidecar.holder_reference)) {
1940
+ throw new Error('A Payrails card-form payment needs request.payrails.holder_reference, the holderReference from the checkout\'s Payrails init payload (the adapters capture it). Nothing was created.');
1867
1941
  }
1868
1942
  // A preparation that tokenized before Pay pays only a field request loaded under the capture
1869
1943
  // context and fields its cardholder approved: the device holds CyberSource's token for that context.
@@ -1871,20 +1945,23 @@ export class VaultClient {
1871
1945
  throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
1872
1946
  }
1873
1947
  // Say no rather than coerce. Each recognizer's card request has exactly one
1874
- // method (recognizerRequestMethod): a POST body, or for Worldpay eProtect the
1875
- // GET whose query carries the card. The service stores the replay template
1876
- // with that method, so a caller handing in another one would get it
1877
- // silently rewritten and the cardholder's device would replay something the
1878
- // caller never asked for. A direct caller deserves to be told, not surprised.
1879
- // A card endpoint has no recognizer: its card request is its profile's own method (a POST, or
1880
- // Target Optical's PUT), which admitCardEndpoint already held the request to.
1948
+ // method (recognizerRequestMethod): a POST body, Gravy's PUT body, or for
1949
+ // Worldpay eProtect the GET whose query carries the card. The service stores
1950
+ // the replay template with that method, so a caller handing in another one
1951
+ // would get it silently rewritten and the cardholder's device would replay
1952
+ // something the caller never asked for. A direct caller deserves to be told,
1953
+ // not surprised. A card endpoint has no recognizer: its card request is its
1954
+ // profile's own method (a POST, or Target Optical's PUT), which
1955
+ // admitCardEndpoint already held the request to.
1881
1956
  const method = (input.request.method ?? cardEndpoint?.method ?? 'POST').toUpperCase();
1882
1957
  const cardMethod = rec ? recognizerRequestMethod(rec) : cardEndpoint ? cardEndpoint.method : 'POST';
1883
1958
  if (method !== cardMethod) {
1884
1959
  throw new Error(cardMethod === 'GET'
1885
1960
  ? `a ${rec?.psp} card registration is a GET; got ${method} for ${redactUrl(input.request.url)}.`
1886
- : `checkout authorization is POST-only; got ${method} for ${redactUrl(input.request.url)}. ` +
1887
- 'A non-POST tokenizer needs recognizer support before it can be intercepted.');
1961
+ : cardMethod === 'PUT' && rec
1962
+ ? `a ${rec.psp} card request is a PUT; got ${method} for ${redactUrl(input.request.url)}.`
1963
+ : `checkout authorization is POST-only; got ${method} for ${redactUrl(input.request.url)}. ` +
1964
+ 'A non-POST tokenizer needs recognizer support before it can be intercepted.');
1888
1965
  }
1889
1966
  // A GET card request is its URL alone. This build completes one: Worldpay
1890
1967
  // eProtect's registration exactly as payment-core admits it, with no body.
@@ -1915,6 +1992,10 @@ export class VaultClient {
1915
1992
  // amount_required.
1916
1993
  if (!hasAmount && rec && AGENT_AMOUNT_PROCESSORS.includes(rec.psp))
1917
1994
  throw new AmountRequiredError(rec.psp);
1995
+ // A Moneris token is charged later for whatever the merchant's server decides, so a zero pair
1996
+ // would show the cardholder no price at all: it is refused like a missing one.
1997
+ if (hasAmount && rec && rec.psp === MONERIS_PSP && Number(input.amount) <= 0)
1998
+ throw new AmountRequiredError(rec.psp);
1918
1999
  // Nor is the agent's pair tied to the page by anything but the total the
1919
2000
  // page shows: without it the cardholder could approve one figure while the
1920
2001
  // page charges another. The API refuses the same create as page_amount_required.
@@ -1924,7 +2005,8 @@ export class VaultClient {
1924
2005
  // later, under another session), so the price the cardholder approves is
1925
2006
  // the one sent here: the agent's pair or else the page total. Refused
1926
2007
  // before any create or prompt, the way the API refuses amount_required.
1927
- if (mode === 'device_encrypt' && !hasAmount && !input.pageAmount) {
2008
+ // A card-form payment names the amount it authorizes, which the API reads.
2009
+ if (mode === 'device_encrypt' && !payrailsPayment && !hasAmount && !input.pageAmount) {
1928
2010
  throw new Error('A Payrails card save names no amount, so pass amount and currency (or let the adapter read the page total): that is the amount the cardholder approves. Nothing was created and nothing was charged.');
1929
2011
  }
1930
2012
  const timeoutMs = input.timeoutMs ?? 15 * 60_000;
@@ -1996,8 +2078,10 @@ export class VaultClient {
1996
2078
  microform: { version: input.request.microform.version, fields: [...input.request.microform.fields],
1997
2079
  ...(input.request.microform.trigger ? { trigger: input.request.microform.trigger } : {}) },
1998
2080
  } : {}),
1999
- // Only the key: the token already rides in the passthrough authorization header.
2000
- ...(mode === 'device_encrypt' && sidecar ? { payrails: { encryption_public_key: sidecar.encryption_public_key } } : {}),
2081
+ // Only the key, and a card-form payment's holderReference: the token already rides in the
2082
+ // passthrough authorization header.
2083
+ ...(mode === 'device_encrypt' && sidecar ? { payrails: { encryption_public_key: sidecar.encryption_public_key,
2084
+ ...(payrailsPayment ? { holder_reference: sidecar.holder_reference } : {}) } } : {}),
2001
2085
  },
2002
2086
  };
2003
2087
  try {
@@ -2063,7 +2147,13 @@ export class VaultClient {
2063
2147
  // then at the caller's interval; at the default 2 seconds the first read alone would miss it.
2064
2148
  // The API's bind names 100 ms as such a row's poll interval, so slow_down admits these reads
2065
2149
  // (as an eProtect row names the 500 ms read above).
2066
- const preparedFlexPollUntil = preparation?.cybersourceFlex ? Date.now() + 10_000 : 0;
2150
+ // A prepared Bambora token create too: Custom Checkout's card frame gives its tokenization
2151
+ // request 5 seconds from Pay, and the cardholder's device answers a prepared token 1.5 to 4
2152
+ // seconds after this bind. Read every 2 seconds, an answer later than the first read waited
2153
+ // for the read 4 seconds after the bind, at or past the frame's timer; read every 100 ms, it
2154
+ // reaches the frame within about 0.2 seconds of the device's answer. The API's bind names
2155
+ // 100 ms on such a row too.
2156
+ const preparedTightPollUntil = preparation?.cybersourceFlex || preparation?.psp === 'bambora' ? Date.now() + 10_000 : 0;
2067
2157
  let execution = {};
2068
2158
  let approvalDelivered = false;
2069
2159
  const deliverApproval = (state) => {
@@ -2112,7 +2202,7 @@ export class VaultClient {
2112
2202
  if (stopSignal?.aborted)
2113
2203
  throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
2114
2204
  const pollInterval = Date.now() < preparedFastPollUntil ? Math.min(this.pollIntervalMs, 500)
2115
- : Date.now() < preparedFlexPollUntil ? Math.min(this.pollIntervalMs, 100) : this.pollIntervalMs;
2205
+ : Date.now() < preparedTightPollUntil ? Math.min(this.pollIntervalMs, 100) : this.pollIntervalMs;
2116
2206
  // A Secure Acceptance read also wakes when its reply window closes (Infinity for every other processor).
2117
2207
  await interruptibleSleep(Math.min(pollInterval, Math.max(0, deadline - Date.now()), Math.max(0, secureAcceptanceReplyBy - Date.now())), stopSignal);
2118
2208
  if (stopSignal?.aborted)
@@ -2247,16 +2337,17 @@ export class VaultClient {
2247
2337
  fields: { ...sub.fields },
2248
2338
  ...(removeRaw ? { remove: [...removeRaw] } : {}),
2249
2339
  },
2250
- ...(shiji ? { shijiStsCompanion: Object.freeze({ keySequences: Object.freeze([...s.shiji_sts_companion.key_sequences]) }) } : {}),
2340
+ ...(shiji ? { shijiStsCompanion: Object.freeze({ keySequenceSha256: Object.freeze([...s.shiji_sts_companion.key_sequence_sha256]) }) } : {}),
2251
2341
  ...amountAuthority,
2252
2342
  ...execution,
2253
2343
  };
2254
2344
  }
2255
2345
  if (approvedMode === 'device_encrypt') {
2256
2346
  // The device sent the card to Payrails itself; this is Payrails' answer to it. It
2257
- // enters the page only as a 201 for a card instrument, rebuilt from payment-core's
2258
- // allow-list under a JSON content type (payrails.ts payrailsSaveAnswer). Anything else
2259
- // is never handed over: the card may be saved, so the outcome is unknown, not declined.
2347
+ // enters the page only as a 201 for a card instrument or, for a card-form payment, a 2xx
2348
+ // naming the authorize action, rebuilt from payment-core's allow-list under a JSON
2349
+ // content type (payrails.ts payrailsAnswer, by the paused URL). Anything else is never
2350
+ // handed over: the card may be saved or paying, so the outcome is unknown, not declined.
2260
2351
  // Autopilot never runs this mode, so an autopilot answer is not one to act on either.
2261
2352
  if (execution.executionMode === 'autopilot')
2262
2353
  throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
@@ -2264,7 +2355,7 @@ export class VaultClient {
2264
2355
  throw new PaymentOutcomeUnknownError(authorizationId, 'approved_mode_unsupported');
2265
2356
  const saved = s.response;
2266
2357
  const answer = saved && typeof saved === 'object' && Number.isInteger(saved.status) && typeof saved.body === 'string'
2267
- ? payrailsSaveAnswer(saved.status, saved.body) : null;
2358
+ ? payrailsAnswer(saved.status, saved.body, input.request.url) : null;
2268
2359
  if (!answer)
2269
2360
  throw new PaymentOutcomeUnknownError(authorizationId, 'payrails_answer_unconfirmed');
2270
2361
  if (s.processor_context !== undefined)
@@ -54,7 +54,7 @@ declare function parseCreditguardTransaction(body: any, txId: any): {
54
54
  personalIdAlwaysEnabled: boolean;
55
55
  inputs: any;
56
56
  } | null;
57
- declare function creditguardTransactionRefusal(txn: any, expectation: any): "otp" | "keep_on_file" | "dcc" | "installments" | "personal_id" | "jwe_unsupported" | "transaction_type" | "amount_mismatch" | "environment_mismatch" | "currency_mismatch" | "transaction_unreadable" | "three_d_secure" | "form_inputs" | "card_field_hidden" | "no_retries_left" | "jwe_public_key_malformed" | null;
57
+ declare function creditguardTransactionRefusal(txn: any, expectation: any): "otp" | "keep_on_file" | "dcc" | "installments" | "personal_id" | "jwe_unsupported" | "transaction_type" | "amount_mismatch" | "environment_mismatch" | "currency_mismatch" | "transaction_unreadable" | "three_d_secure" | "form_inputs" | "card_field_hidden" | "jwe_public_key_malformed" | null;
58
58
  declare function creditguardSendWindowRefusal(timing: any): "timing_unknown" | "page_expired" | "recaptcha_expired" | null;
59
59
  declare var CREDITGUARD_REFUSAL_CODES: readonly string[];
60
60
  declare function creditguardResponseOutcome(status: any, body: any, options: any): {
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 83caafdf10afb4617e5eb3a896d3a1ba4b5c098747495658529c5d13798051cc
3
+ // artifact-sha256: 58593275ccbce7e76a1aa68065eb63063cdf0ed00d533706bec557cb8a21cbc8
4
4
  // src/creditguard.js
5
5
  var CREDITGUARD_PPS_HOSTS = Object.freeze(["pps.creditguard.co.il", "pps2.creditguard.co.il", "ppsuat.creditguard.co.il"]);
6
6
  var CREDITGUARD_SANDBOX_HOST = "ppsuat.creditguard.co.il";
@@ -572,8 +572,6 @@ function creditguardTransactionRefusal(txn, expectation) {
572
572
  const form = formInputsRefusal(txn.inputs);
573
573
  if (form)
574
574
  return form;
575
- if (txn.remainingSubmitRetries === null || txn.remainingSubmitRetries === 0)
576
- return "no_retries_left";
577
575
  const environment = creditguardPreparationEnvironment(expectation.hostname);
578
576
  if (environment === null || environment === "sandbox" !== (txn.envType === "test") || typeof expectation.sandbox !== "boolean" || expectation.sandbox !== (environment === "sandbox"))
579
577
  return "environment_mismatch";
@@ -13,12 +13,13 @@ import type { PreparedCheckout, ReplayResponse } from './client.js';
13
13
  * always a prepared one (authorize() requires it), so its preparation names
14
14
  * the processor and the key pin the cardholder approved. It continues only a
15
15
  * request on Fiserv's capture hosts, and only when that capture's own pin (its
16
- * envelope's key, pinned for its endpoint and its merchant) is the approved
17
- * pin; then Fiserv's own substitution writes only the envelope's four members
18
- * at source.encryptionData of the live paused body, and throws for a body that
19
- * is not the capture Agentcard covers or an envelope under another key than
20
- * the one the paused capture names (fiserv.ts). Any other approval never meets
21
- * a Fiserv capture host. Every refusal throws before anything is sent.
16
+ * envelope's key, or for a credentials session's key its merchant's, pinned for
17
+ * its endpoint and its merchant) is the approved pin; then Fiserv's own
18
+ * substitution writes only the envelope's four members at source.encryptionData
19
+ * of the live paused body, and throws for a body that is not the capture
20
+ * Agentcard covers or an envelope under another key than that pin's
21
+ * (fiserv.ts). Any other approval never meets a Fiserv capture host. Every
22
+ * refusal throws before anything is sent.
22
23
  */
23
24
  export declare function cseBody(body: string, replay: Extract<ReplayResponse, {
24
25
  mode: 'cse';
package/dist/cse-body.js CHANGED
@@ -18,12 +18,13 @@ import { substituteVerifoneBody } from './verifone.js';
18
18
  * always a prepared one (authorize() requires it), so its preparation names
19
19
  * the processor and the key pin the cardholder approved. It continues only a
20
20
  * request on Fiserv's capture hosts, and only when that capture's own pin (its
21
- * envelope's key, pinned for its endpoint and its merchant) is the approved
22
- * pin; then Fiserv's own substitution writes only the envelope's four members
23
- * at source.encryptionData of the live paused body, and throws for a body that
24
- * is not the capture Agentcard covers or an envelope under another key than
25
- * the one the paused capture names (fiserv.ts). Any other approval never meets
26
- * a Fiserv capture host. Every refusal throws before anything is sent.
21
+ * envelope's key, or for a credentials session's key its merchant's, pinned for
22
+ * its endpoint and its merchant) is the approved pin; then Fiserv's own
23
+ * substitution writes only the envelope's four members at source.encryptionData
24
+ * of the live paused body, and throws for a body that is not the capture
25
+ * Agentcard covers or an envelope under another key than that pin's
26
+ * (fiserv.ts). Any other approval never meets a Fiserv capture host. Every
27
+ * refusal throws before anything is sent.
27
28
  */
28
29
  export function cseBody(body, replay, url, preparation) {
29
30
  // A Verifone approval is always a prepared one for the profile it pays, and continues only the
@@ -39,7 +40,7 @@ export function cseBody(body, replay, url, preparation) {
39
40
  if (replay.kind !== undefined || !isFiservCaptureHost(url) || fiservCapturePinId(url, body) !== preparation.merchantProfile) {
40
41
  throw new Error('Refusing to substitute: the paused request is not a Fiserv capture under the key pin the cardholder approved.');
41
42
  }
42
- return substituteFiservEnvelope(body, replay.substitutions);
43
+ return substituteFiservEnvelope(body, replay.substitutions, url);
43
44
  }
44
45
  if (isFiservCaptureHost(url))
45
46
  throw new Error('Refusing to substitute: a Fiserv capture continues only through a prepared Fiserv approval.');
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 0179250ba6f44c33967a08dc4ce3a891d8b73f0f7eb60e9cab34f801dc2a4c57
3
+ // artifact-sha256: fdee611015d42dc0a679e93a64766ac971b190fcc8f17a107d761c414569e5f9
4
4
  // src/datatrans.js
5
5
  var DATATRANS_HOSTS = Object.freeze(["pay.datatrans.com", "pay.sandbox.datatrans.com"]);
6
6
  var DATATRANS_TOKENIZE_PATH = "/upp/payment/SecureFields/paymentField";
@@ -178,15 +178,18 @@ function judgeBody(body) {
178
178
  if (!pairs)
179
179
  return null;
180
180
  const fields = new Map(pairs.map(({ key, value }) => [key, value]));
181
+ const merchantHeldExpiry = !fields.get("expm") && !fields.get("expy") && fields.has("expm") === fields.has("expy") && fields.get("usage") !== "REGISTRATION";
181
182
  for (const [key, value] of fields) {
182
183
  if (!KEYS.has(key))
183
184
  return null;
185
+ if (merchantHeldExpiry && (key === "expm" || key === "expy"))
186
+ continue;
184
187
  if (EMPTY_ONLY.has(key) ? value !== "" : !RULES[key](value))
185
188
  return null;
186
189
  }
187
- if (!REQUIRED.every((key) => fields.has(key)))
190
+ if (!REQUIRED.every((key) => fields.has(key) || merchantHeldExpiry && (key === "expm" || key === "expy")))
188
191
  return null;
189
- return { pairs, fields, usage: fields.get("usage") ?? "" };
192
+ return { pairs, fields, usage: fields.get("usage") ?? "", merchantHeldExpiry };
190
193
  }
191
194
  function datatransRegistrationRefusal(usage, card, policy = DATATRANS_REGISTRATION_POLICY) {
192
195
  if (usage !== "REGISTRATION")
@@ -319,14 +322,14 @@ function injectDatatransCard(body, card, policy = DATATRANS_REGISTRATION_POLICY)
319
322
  if (!fields.get("cvv") || !digits(card.cvc, 3, 4))
320
323
  throw new Error(FILL_REFUSAL);
321
324
  const yy = String(year % 100).padStart(2, "0");
322
- const values = {
325
+ const values = judged.merchantHeldExpiry ? { cardNumber: card.number, cvv: card.cvc } : {
323
326
  cardNumber: card.number,
324
327
  cvv: card.cvc,
325
328
  expm: fields.get("expm").length === 2 ? String(month).padStart(2, "0") : String(month),
326
329
  expy: fields.get("expy").length === 4 ? `20${yy}` : yy
327
330
  };
328
331
  const written = judged.pairs.map((pair) => owns(values, pair.key) ? `${pair.rawKey}=${encodeDatatransFormValue(values[pair.key])}` : `${pair.rawKey}=${pair.raw}`);
329
- return { body: written.join("&"), where: ["cardNumber", "cvv", "expm", "expy"] };
332
+ return { body: written.join("&"), where: Object.keys(values) };
330
333
  }
331
334
  var TRANSACTION_ID = /^[0-9]{18}$/;
332
335
  var CARD_WORD = /^[A-Za-z][A-Za-z _-]{0,31}$/;
@@ -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
@@ -20,7 +23,7 @@ export declare const CONVERGE_CAPABILITY = "converge_hpp_process";
20
23
  */
21
24
  export declare const CONVERGE_REFUSAL_REASONS: readonly ["converge_body_malformed", "converge_payment_type_refused", "converge_debit_card_refused", "converge_push_to_card_refused", "converge_gift_card_refused", "converge_credential_refused", "converge_wallet_refused", "converge_captcha_refused", "converge_service_fee_refused", "converge_3ds_follow_up_refused", "converge_surcharge_refused", "converge_amount_add_on_refused", "converge_card_saving_refused", "converge_recurring_refused", "converge_currency_refused", "converge_amount_missing", "converge_context_missing", "converge_card_missing", "converge_card_malformed", "converge_url_refused", "converge_method_refused", "converge_content_type_refused"];
22
25
  export type ConvergeRefusalReason = typeof CONVERGE_REFUSAL_REASONS[number];
23
- /** True when the URL's host is one of Converge's two payment page hosts. */
26
+ /** True when the URL's host is one of Converge's three payment page hosts: api, www and the demo. */
24
27
  export declare function isConvergeHost(url: string): boolean;
25
28
  /**
26
29
  * The pause decision for a card-carrying POST the registry recognized on a Converge host. Null
@@ -1,13 +1,14 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: dc4269c2533c43ddcc4f7be0f3a4645beea09bce6854c98e38701f29cf037e2b
3
+ // artifact-sha256: 136e1c2b0fd2bdee0cfbf5efbbfaf1a92ace7d1cc52e17a3fd5a4584370e5e53
4
4
  // src/elavon-converge.js
5
- var CONVERGE_HOSTS = /* @__PURE__ */ Object.freeze(["api.convergepay.com", "api.demo.convergepay.com"]);
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";
7
7
  var CONVERGE_PROCESS_PATH = "/hosted-payments/service/payment/hpp/process";
8
8
  var CONVERGE_PROCESS_URLS = /* @__PURE__ */ Object.freeze([
9
9
  "https://api.convergepay.com/hosted-payments/service/payment/hpp/process",
10
- "https://api.demo.convergepay.com/hosted-payments/service/payment/hpp/process"
10
+ "https://api.demo.convergepay.com/hosted-payments/service/payment/hpp/process",
11
+ "https://www.convergepay.com/hosted-payments/service/payment/hpp/process"
11
12
  ]);
12
13
  var CONVERGE_REFUSAL_CODES = /* @__PURE__ */ Object.freeze([
13
14
  "converge_body_malformed",
@@ -72,15 +73,18 @@ var RECURRING_FIELDS = /* @__PURE__ */ Object.freeze([
72
73
  "ssl_recurring_batch_count",
73
74
  "ssl_installment_id"
74
75
  ]);
76
+ var ONE_TIME_RECURRING_FLAG = "false";
75
77
  var CARD_SAVING_FLAGS = /* @__PURE__ */ Object.freeze(["ssl_get_token", "ssl_add_token"]);
76
78
  var CREDENTIAL_FIELDS = /* @__PURE__ */ Object.freeze(["ssl_account_id", "ssl_merchant_id", "ssl_user_id", "ssl_pin", "ssl_vendor_id"]);
77
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}$/;
78
81
  var CARD_NUMBER = /^\d{12,19}$/;
79
82
  var EXPIRY_MMYY = /^(0[1-9]|1[0-2])\d{2}$/;
80
83
  var CVC = /^\d{3,4}$/;
81
84
  var AMOUNT = /^(\d{1,13})(?:\.(\d{1,2}))?$/;
82
85
  var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
83
86
  var owns = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
87
+ var recurringRequested = (data) => RECURRING_FIELDS.some((key) => owns(data, key) && !(key === "recurring" && data[key] === ONE_TIME_RECURRING_FLAG));
84
88
  var refuse = (code) => Object.freeze({ verdict: "refuse", code });
85
89
  var CARD = /* @__PURE__ */ Object.freeze({ verdict: "card" });
86
90
  var CONTINUE = /* @__PURE__ */ Object.freeze({ verdict: "continue" });
@@ -129,7 +133,7 @@ function convergeBodyVerdict(body) {
129
133
  return refuse("converge_credential_refused");
130
134
  if (WALLET_FIELDS.some((key) => owns(data, key)))
131
135
  return refuse("converge_wallet_refused");
132
- if (owns(parsed, "captchaToken"))
136
+ if (owns(parsed, "captchaToken") && !(typeof parsed.captchaToken === "string" && SOLVED_CAPTCHA.test(parsed.captchaToken)))
133
137
  return refuse("converge_captcha_refused");
134
138
  if (SERVICE_FEE_FIELDS.some((key) => owns(data, key)))
135
139
  return refuse("converge_service_fee_refused");
@@ -142,7 +146,7 @@ function convergeBodyVerdict(body) {
142
146
  if (owns(data, "ssl_token") || CARD_SAVING_FLAGS.some((key) => owns(data, key) && !(typeof data[key] === "string" && /^n$/i.test(data[key])))) {
143
147
  return refuse("converge_card_saving_refused");
144
148
  }
145
- if (RECURRING_FIELDS.some((key) => owns(data, key)))
149
+ if (recurringRequested(data))
146
150
  return refuse("converge_recurring_refused");
147
151
  if (owns(data, "ssl_transaction_currency") && !(typeof data.ssl_transaction_currency === "string" && /^usd$/i.test(data.ssl_transaction_currency))) {
148
152
  return refuse("converge_currency_refused");
@@ -1,6 +1,7 @@
1
1
  // Elavon Converge Hosted Payment Page (psp `elavon_converge`): the page NY DMV's payment apps
2
- // redirect to, api.convergepay.com/hosted-payments/?ssl_txn_auth_token=<one-time token> (the
3
- // demo environment serves the same page on api.demo.convergepay.com). Converge's own AngularJS
2
+ // redirect to, api.convergepay.com/hosted-payments/?ssl_txn_auth_token=<one-time token>. Every
3
+ // Converge Buy Button and pay link opens the same page on www.convergepay.com, and the demo
4
+ // environment serves it on api.demo.convergepay.com. Converge's own AngularJS
4
5
  // app posts the card in one same-origin JSON XHR, POST /hosted-payments/service/payment/hpp/process,
5
6
  // with the card at paymentData.ssl_card_number, ssl_exp_date (MMYY) and ssl_cvv2cvc2, the amount
6
7
  // at paymentData.ssl_amount, and the page's rolling session in `context`. The same URL also takes
@@ -24,9 +25,12 @@ export const CONVERGE_CAPABILITY = 'converge_hpp_process';
24
25
  * the device refuses with, so an integrator reads one code whichever of them refused.
25
26
  * - A shape the Vault never pays: `converge_wallet_refused`, `converge_debit_card_refused`,
26
27
  * `converge_push_to_card_refused`, `converge_gift_card_refused`, `converge_payment_type_refused`,
27
- * `converge_captcha_refused`, `converge_service_fee_refused`, `converge_recurring_refused`,
28
+ * `converge_service_fee_refused`, `converge_recurring_refused`,
28
29
  * `converge_card_saving_refused` (ssl_get_token or ssl_add_token), `converge_amount_add_on_refused`
29
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.
30
34
  * - A follow-up after a step the Vault cannot complete: `converge_3ds_follow_up_refused` (3-D
31
35
  * Secure or Pay Defense) and `converge_surcharge_refused`.
32
36
  * - A body the cardholder has nothing to approve on: `converge_amount_missing` and
@@ -59,7 +63,7 @@ export const CONVERGE_REFUSAL_REASONS = [
59
63
  'converge_method_refused',
60
64
  'converge_content_type_refused',
61
65
  ];
62
- /** True when the URL's host is one of Converge's two payment page hosts. */
66
+ /** True when the URL's host is one of Converge's three payment page hosts: api, www and the demo. */
63
67
  export function isConvergeHost(url) {
64
68
  try {
65
69
  return CONVERGE_HOSTS.includes(new URL(url).hostname);