@agent-cards/sdk 0.20.0 → 0.22.0

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 (111) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/PREFLIGHT.md +34 -0
  3. package/README.md +1217 -15
  4. package/dist/aci.d.ts +173 -0
  5. package/dist/aci.generated.d.ts +87 -0
  6. package/dist/aci.generated.js +306 -0
  7. package/dist/aci.js +371 -0
  8. package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
  9. package/dist/adyen-merchant-hosted.generated.js +283 -49
  10. package/dist/agentcard.d.ts +9 -2
  11. package/dist/agentcard.js +4 -2
  12. package/dist/amadeus-checkout.generated.d.ts +57 -0
  13. package/dist/amadeus-checkout.generated.js +549 -0
  14. package/dist/amadeus.d.ts +41 -0
  15. package/dist/amadeus.js +81 -0
  16. package/dist/builtin-registry.generated.js +1 -1
  17. package/dist/card-endpoint.d.ts +177 -0
  18. package/dist/card-endpoint.js +286 -0
  19. package/dist/card-fields.generated.d.ts +5 -3
  20. package/dist/card-fields.generated.js +284 -7
  21. package/dist/cdp.d.ts +6 -1
  22. package/dist/cdp.js +983 -147
  23. package/dist/client.d.ts +493 -13
  24. package/dist/client.js +1100 -82
  25. package/dist/creditguard.generated.d.ts +79 -0
  26. package/dist/creditguard.generated.js +662 -0
  27. package/dist/cse-body.js +11 -1
  28. package/dist/cybersource-flex.d.ts +183 -0
  29. package/dist/cybersource-flex.generated.d.ts +92 -0
  30. package/dist/cybersource-flex.generated.js +689 -0
  31. package/dist/cybersource-flex.js +275 -0
  32. package/dist/datatrans.d.ts +96 -0
  33. package/dist/datatrans.generated.d.ts +64 -0
  34. package/dist/datatrans.generated.js +426 -0
  35. package/dist/datatrans.js +178 -0
  36. package/dist/elavon-converge.d.ts +37 -0
  37. package/dist/elavon-converge.generated.d.ts +49 -0
  38. package/dist/elavon-converge.generated.js +439 -0
  39. package/dist/elavon-converge.js +84 -0
  40. package/dist/eprotect.d.ts +152 -0
  41. package/dist/eprotect.generated.d.ts +77 -0
  42. package/dist/eprotect.generated.js +498 -0
  43. package/dist/eprotect.js +222 -0
  44. package/dist/gmo.d.ts +63 -0
  45. package/dist/gmo.generated.d.ts +60 -0
  46. package/dist/gmo.generated.js +586 -0
  47. package/dist/gmo.js +119 -0
  48. package/dist/hosted-form.d.ts +3 -2
  49. package/dist/hosted-form.js +3 -2
  50. package/dist/index.d.ts +19 -3
  51. package/dist/index.js +6 -1
  52. package/dist/juspay-gate.d.ts +82 -0
  53. package/dist/juspay-gate.js +126 -0
  54. package/dist/juspay.generated.d.ts +89 -0
  55. package/dist/juspay.generated.js +705 -0
  56. package/dist/lifecycle.d.ts +62 -6
  57. package/dist/lifecycle.js +123 -8
  58. package/dist/merchant-card-endpoints.generated.d.ts +63 -0
  59. package/dist/merchant-card-endpoints.generated.js +1103 -0
  60. package/dist/merchant-hosted.d.ts +4 -1
  61. package/dist/merchant-hosted.js +5 -2
  62. package/dist/merchant-session-probe.d.ts +48 -0
  63. package/dist/merchant-session-probe.js +53 -0
  64. package/dist/merchant-session.generated.d.ts +69 -0
  65. package/dist/merchant-session.generated.js +919 -0
  66. package/dist/payrails.d.ts +133 -0
  67. package/dist/payrails.generated.d.ts +73 -0
  68. package/dist/payrails.generated.js +596 -0
  69. package/dist/payrails.js +305 -0
  70. package/dist/pelecard.d.ts +32 -0
  71. package/dist/pelecard.generated.d.ts +32 -0
  72. package/dist/pelecard.generated.js +283 -0
  73. package/dist/pelecard.js +42 -0
  74. package/dist/pre-claim.d.ts +84 -3
  75. package/dist/pre-claim.js +987 -9
  76. package/dist/preflight-catalog.json +2310 -263
  77. package/dist/preflight-schemas.json +187 -6
  78. package/dist/preflight.generated.d.ts +1 -1
  79. package/dist/preflight.generated.js +247 -4
  80. package/dist/preparation.d.ts +5 -2
  81. package/dist/preparation.js +36 -10
  82. package/dist/prepared-processor.d.ts +86 -5
  83. package/dist/prepared-processor.js +157 -6
  84. package/dist/primer.d.ts +53 -0
  85. package/dist/primer.generated.d.ts +38 -0
  86. package/dist/primer.generated.js +624 -0
  87. package/dist/primer.js +82 -0
  88. package/dist/registry.d.ts +114 -12
  89. package/dist/registry.js +85 -11
  90. package/dist/shiji-sts.d.ts +144 -0
  91. package/dist/shiji-sts.generated.d.ts +72 -0
  92. package/dist/shiji-sts.generated.js +913 -0
  93. package/dist/shiji-sts.js +222 -0
  94. package/dist/stripe-checkout.generated.d.ts +1 -1
  95. package/dist/stripe-checkout.generated.js +735 -10
  96. package/dist/stripe-setup.d.ts +16 -0
  97. package/dist/stripe-setup.js +61 -0
  98. package/dist/substitutions.generated.js +187 -29
  99. package/dist/verifone.d.ts +68 -0
  100. package/dist/verifone.generated.d.ts +51 -0
  101. package/dist/verifone.generated.js +1401 -0
  102. package/dist/verifone.js +119 -0
  103. package/dist/voice-types.d.ts +27 -0
  104. package/dist/voice.d.ts +6 -1
  105. package/dist/voice.js +6 -1
  106. package/dist/wspay.d.ts +41 -0
  107. package/dist/wspay.generated.d.ts +41 -0
  108. package/dist/wspay.generated.js +370 -0
  109. package/dist/wspay.js +65 -0
  110. package/examples/preflight/kernel-native/inventory.json +1 -1
  111. package/package.json +3 -3
package/dist/pre-claim.js CHANGED
@@ -3,9 +3,24 @@
3
3
  // Before a paused request is claimed for the vault, a small set of processor
4
4
  // gates inspect it: Mercado Pago's native Checkout Pro configuration and
5
5
  // association, Braintree's shared /graphql endpoint, native Stripe Checkout,
6
- // and Fiserv's card capture. The first three used to be written inline in
7
- // Fetch.requestPaused and again in the page.route handler, so a decision could
8
- // drift between the two adapters and a new family had to be added in both places.
6
+ // Fiserv's card capture, GMO's CreateToken.json, which also carries requests
7
+ // the vault can never pay, CyberSource Flex Microform's field iframes (whose
8
+ // capture context rides into the pause), Datatrans Secure Fields, whose
9
+ // card URL also carries card posts the device could never fill and card posts
10
+ // it already answered, and Pelecard's card submission URL, which also carries
11
+ // saved-token, wallet and keep-on-file bodies, Primer's
12
+ // /payment-instruments, which also carries requests the vault can never pay,
13
+ // Payrails, whose save needs the session key the page captured and whose
14
+ // hosts also carry card payments the vault can never pay, and Elavon
15
+ // Converge's hpp/process URL, which also carries wallet, debit, follow-up and
16
+ // card-saving bodies, WSPay's confirm URL, which also carries bodies with
17
+ // no new card, and Shiji STS's getTokenId and getCvvId, which also carry
18
+ // requests the vault must never pay.
19
+ // The first three used to be
20
+ // written inline in Fetch.requestPaused and again in the page.route handler, so
21
+ // a decision could drift between the two adapters and a new family had to be
22
+ // added in both places.
23
+ // Juspay's tokenize and /txns joined later as one more entry.
9
24
  //
10
25
  // dispatchPreClaim runs the gates in order and resolves the request to one of:
11
26
  // - continue: let the request through untouched, or with a body the gate
@@ -38,8 +53,24 @@
38
53
  import { PaymentOutcomeUnknownError, redactUrl } from './client.js';
39
54
  import { classifyBraintreeRequest } from './braintree.js';
40
55
  import { StripeCheckoutClaimConflictError, stripeCheckoutReadinessError } from './stripe-checkout.js';
56
+ import { gmoRequestRefusal, isGmoTokenHost, isGmoTokenV1Request, recognizesGmo } from './gmo.js';
57
+ import { JUSPAY_PSP } from './juspay-gate.js';
58
+ import { datatransPauseDecision, isDatatransHost } from './datatrans.js';
59
+ import { DATATRANS_INVALID_REQUEST } from './datatrans.generated.js';
60
+ import { isPelecardHost, pelecardRefusal } from './pelecard.js';
61
+ import { ACI_CHECKOUT_REUSED } from './aci.js';
62
+ import { ACI_EXPIRY_UNSTRIPPABLE } from './aci.generated.js';
63
+ import { CREDITGUARD_PPS_HOSTS, classifyCreditguardRequest, creditguardNuveiRefusal, creditguardProcessRefusal } from './creditguard.generated.js';
64
+ import { isPrimerPciHost, primerRequestVerdict } from './primer.js';
65
+ import { convergeRefusal, isConvergeHost } from './elavon-converge.js';
66
+ import { isWspayHost, wspayRefusal } from './wspay.js';
67
+ import { isShijiStsHost, shijiStsVerdict } from './shiji-sts.js';
41
68
  import { classifyProfileRequest, merchantBodyKeyPathSha256, profileTemplatedUrl } from './merchant-hosted.js';
42
69
  import { isFiservCaptureHost, isFiservPausableBody } from './fiserv.js';
70
+ import { CYBERSOURCE_FLEX_PSP, isCybersourceFlexHost } from './cybersource-flex.js';
71
+ import { cardEndpointDetail, cardEndpointFor, cardEndpointLive } from './card-endpoint.js';
72
+ import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isPayrailsStaticHost, payrailsKeyRefusal, payrailsRequestKind, payrailsSaveRefusal, } from './payrails.js';
73
+ import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
43
74
  /**
44
75
  * The URL an event may name for a request. For an armed merchant profile's
45
76
  * endpoint or a sibling of it: the endpoint origin, the path TEMPLATE and the
@@ -55,8 +86,40 @@ export function eventUrl(vault, url) {
55
86
  }
56
87
  catch { /* not a profile this build reviewed */ }
57
88
  }
89
+ // A Verifone merchant's origin: its origin and, for a leg, the leg's path template (verifone.ts).
90
+ const verifone = typeof vault?.verifoneProfileOf === 'function' ? vault.verifoneProfileOf(url) : null;
91
+ if (verifone) {
92
+ try {
93
+ return verifoneTemplatedUrl(verifone.id, url);
94
+ }
95
+ catch { /* not a profile this build reviewed */ }
96
+ }
58
97
  return redactUrl(url);
59
98
  }
99
+ // A reviewed merchant's own card endpoint (card-endpoint.ts). It runs first: its URLs
100
+ // are merchant origins no processor gate claims, and one never reaches another gate. A
101
+ // POST that is exactly a profile's card request pauses for the claim flow, carrying the
102
+ // profile, when this build's copy of the profile takes payments. In any other status
103
+ // (observe, a fact only a HAR can pin, no reviewed HAR, a recurring charge) it aborts as
104
+ // an unsupported checkout, synchronously and before any create, so the placeholder card
105
+ // never reaches the merchant; the event names the profile and the endpoint origin, never
106
+ // the URL's path or query, which carry the merchant's cart and booking ids. Anything
107
+ // else, a GET or a CORS preflight to the same URL included, defers.
108
+ const cardEndpointPreClaim = (ctx) => {
109
+ const profile = cardEndpointFor(ctx.request.url, ctx.request.method);
110
+ if (!profile)
111
+ return undefined;
112
+ if (cardEndpointLive(profile))
113
+ return { kind: 'pause', stripeStep: null, cardEndpoint: profile };
114
+ return cardEndpointUnsupported(ctx, profile);
115
+ };
116
+ async function cardEndpointUnsupported(ctx, profile) {
117
+ ctx.preparationGate.invalidate('unsupported_checkout');
118
+ ctx.lifecycle.unsupported('merchant_card_endpoint_not_enabled');
119
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { ...cardEndpointDetail(profile), reason: 'merchant_card_endpoint_not_enabled' } });
120
+ await ctx.effects.abort();
121
+ return { kind: 'abort' };
122
+ }
60
123
  // Mercado Pago's native Checkout Pro shares one origin between its BIN-lookup
61
124
  // configuration and its card association. A request that is not for that origin
62
125
  // defers synchronously; a POST to it is handled asynchronously (a document read
@@ -104,10 +167,15 @@ async function mercadoContinuation(ctx) {
104
167
  // a Braintree POST is classified the same as an empty one, so both adapters
105
168
  // reach the same verdict. The configuration continue swallows a transport
106
169
  // failure, as the inline CDP path did (the Playwright path never reaches a
107
- // failing route here).
170
+ // failing route here). The body is read only for a Braintree host:
171
+ // classifyBraintreeRequest answers undefined for any other host before it looks
172
+ // at a body, so another processor's request is never read here, and a body read
173
+ // that throws (Playwright's postData() can) stays with the gate that owns it.
108
174
  const braintreePreClaim = (ctx) => {
109
- const { url, method, body } = ctx.request;
110
- const braintree = method.toUpperCase() === 'POST' ? classifyBraintreeRequest(url, method, body) : undefined;
175
+ const { url, method } = ctx.request;
176
+ if (method.toUpperCase() !== 'POST' || classifyBraintreeRequest(url, method, null) === undefined)
177
+ return undefined;
178
+ const braintree = classifyBraintreeRequest(url, method, ctx.request.body);
111
179
  if (braintree === 'configuration')
112
180
  return braintreeContinue(ctx);
113
181
  if (braintree === 'invalid')
@@ -285,6 +353,71 @@ async function merchantHostedUnsupported(ctx, profile, url, category, reason) {
285
353
  await ctx.effects.abort();
286
354
  return { kind: 'abort' };
287
355
  }
356
+ // Verifone merchants (verifone.ts). A request to an armed Verifone profile's endpoint origin is
357
+ // the profile's to judge before the terminal arbiter, which would otherwise continue it (no
358
+ // processor recognizes a merchant's own server). verifone.js sends the card, encrypted in the
359
+ // page, in the merchant's own requests, so every request to that origin is read:
360
+ // - no verifone.js blob (a basket query, a GET, a preflight, a page load, a request with no
361
+ // body at all): defer, and the request is judged as it was before any profile existed. The
362
+ // profile arms the merchant's whole origin, where a bodiless POST or DELETE (a session
363
+ // refresh, a cart edit, an empty beacon) is routine, so only a body the adapter could not read
364
+ // counts as unread, never a missing one;
365
+ // - a body the adapter knows is there and could not read (CDP: a blob or a file), on anything
366
+ // but a GET, HEAD or OPTIONS: not proven to carry no card, so it is judged as a card body the
367
+ // profile cannot finish, as for a merchant-hosted profile. Playwright reports such a body as
368
+ // no body (PreClaimContext.request), so only attachToCdp refuses it;
369
+ // - a card request to a profile this attachment already handed a card to: the page's retry
370
+ // carries the agent's placeholder and the approval is spent, so it aborts;
371
+ // - a profile in observe: report the request's shape (the SHA-256 of its key paths, never a
372
+ // value) with the verdict, then abort as unsupported, so the placeholder never reaches the
373
+ // merchant and no authorization exists;
374
+ // - a blob the profile cannot pay (a BIN lookup, a gift card, a saved card's CVV, a second
375
+ // card entry, a key nobody reviewed, a body the leg did not review): abort as unsupported,
376
+ // with the reason;
377
+ // - the reviewed leg, on an enabled profile, with no preparation on this attachment: abort as
378
+ // preparation_required before any pause, prompt or authorization;
379
+ // - the same request with a preparation: pause for the claim flow, where only a ready
380
+ // preparation for this profile pays it.
381
+ // A reader that throws on the body is not proof it carries no card either: the request is judged
382
+ // as an unreadable card body and aborted. Synchronous until it must abort, like every gate.
383
+ export const verifonePreClaim = (ctx) => {
384
+ const { url, method } = ctx.request;
385
+ const profile = typeof ctx.vault.verifoneProfileOf === 'function' ? ctx.vault.verifoneProfileOf(url) : null;
386
+ if (!profile)
387
+ return undefined;
388
+ const body = ctx.request.body;
389
+ const unread = body === null;
390
+ let verdict;
391
+ try {
392
+ verdict = !unread ? classifyVerifoneProfileRequest(profile, url, method, body)
393
+ : UNREAD_BODY_METHODS.has(String(method).toUpperCase()) ? { verdict: 'pass' } : { verdict: 'abort', reason: MERCHANT_BODY_UNREADABLE };
394
+ }
395
+ catch {
396
+ verdict = { verdict: 'abort', reason: MERCHANT_BODY_UNREADABLE };
397
+ }
398
+ if (verdict.verdict === 'pass')
399
+ return undefined;
400
+ const templated = verifoneTemplatedUrl(profile.id, url);
401
+ if (ctx.merchantHandoff?.retried(profile.id))
402
+ return merchantHostedAbort(ctx);
403
+ if (profile.status !== 'enabled') {
404
+ ctx.onEvent?.({ type: 'verifone_profile_observed', detail: { profile: profile.id, url: templated, method, verdict: verdict.verdict,
405
+ ...(verdict.verdict === 'abort' ? { reason: verdict.reason } : {}), keyPathSha256: unread ? null : keyPathSha256Of(body) } });
406
+ return verifoneUnsupported(ctx, profile.id, templated, 'merchant_profile_observe', 'Agentcard has not turned on payments to this merchant yet');
407
+ }
408
+ if (verdict.verdict === 'abort')
409
+ return verifoneUnsupported(ctx, profile.id, templated, 'merchant_request_unsupported', verdict.reason);
410
+ if (!ctx.preparationGate.isEngaged())
411
+ return merchantHostedPreparationRequired(ctx);
412
+ return { kind: 'pause', stripeStep: ctx.stripeStep };
413
+ };
414
+ async function verifoneUnsupported(ctx, profile, url, category, reason) {
415
+ ctx.preparationGate.invalidate('unsupported_checkout');
416
+ ctx.lifecycle.unsupported(category);
417
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url, method: ctx.request.method, profile, reason } });
418
+ await ctx.effects.abort();
419
+ return { kind: 'abort' };
420
+ }
288
421
  // Fiserv Commerce Hub card capture (fiserv.ts). A POST the registry recognizes on
289
422
  // one of Fiserv's two card-capture hosts is this gate's to judge before any claim:
290
423
  // - only a capture that carries no Vault card (Fastlane's PaymentToken, ACH's
@@ -311,10 +444,13 @@ async function merchantHostedUnsupported(ctx, profile, url, category, reason) {
311
444
  // Synchronous until it must continue or abort, like every gate. Exported for the
312
445
  // gate registry's tests.
313
446
  export const fiservPreClaim = (ctx) => {
314
- const { url, method, body } = ctx.request;
447
+ const { url, method } = ctx.request;
315
448
  if (String(method).toUpperCase() !== 'POST' || !isFiservCaptureHost(url) || !ctx.vault.isCardRequest(url, method))
316
449
  return undefined;
317
- if (!isFiservPausableBody(body))
450
+ // The body is read only here, on a capture host, since reading it may throw: another processor's
451
+ // request whose body the adapter cannot read then reaches the gate that owns it (GMO's aborts
452
+ // that request alone) or the arbiter, as it did before this gate joined the list.
453
+ if (!isFiservPausableBody(ctx.request.body))
318
454
  return fiservContinue(ctx);
319
455
  const idle = ctx.attachmentReady() && !ctx.stripeAttachmentBusy() && !ctx.interceptionAborted() && !ctx.terminal()
320
456
  && !ctx.lifecycle.isBlocked() && !ctx.awaitingApproval();
@@ -333,6 +469,848 @@ async function fiservPreparationRequired(ctx) {
333
469
  await ctx.effects.abort();
334
470
  return { kind: 'abort' };
335
471
  }
472
+ // GMO token v2 and v1 (gmo.ts). GMO's CreateToken.json also carries requests the vault can never
473
+ // pay, so a POST the registry recognizes on a GMO token host is judged here, before the terminal
474
+ // arbiter would pause it, by the whole-request guard the API runs at create:
475
+ // - mp-token.js's SHARED_PK getToken for one token defers, and the arbiter pauses it;
476
+ // - anything else (UNIQUE_PK, GMO Elements, several tokens, a shop id of the other
477
+ // environment's shape, another serialization) aborts as an unsupported checkout,
478
+ // `unsupported_checkout` with reason `unsupported_gmo_request`, with no claim, create or
479
+ // prompt. The attachment then reads `unsupported` and holds: the merchant's configuration
480
+ // decides that shape, so the page's next request would be the same.
481
+ // GMO token v1's JSONP GET to /ext/api/getToken aborts the same way (`unsupported_gmo_token_v1`),
482
+ // so the agent's placeholder is never tokenized silently. Nothing is decided without the registry:
483
+ // an SDK the API did not serve GMO (a build without the capability, or an org Agentcard has not
484
+ // enabled) recognizes neither, so the gate defers and every GMO request continues as before. The
485
+ // preflight OPTIONS and every other method on those URLs continue through the arbiter.
486
+ // A body the adapter could not read says nothing about the page's configuration, so that request
487
+ // alone is aborted with a `blocked` event, `unreadable_gmo_request`, and the attachment is not held.
488
+ // A body or headers whose read throws (Playwright's postData() can) is that same unread request:
489
+ // it is aborted here rather than rejecting the route handler with the request left undecided.
490
+ // Synchronous until the abort, so a card request pauses with no await. A guard that throws
491
+ // refuses the request rather than letting it reach a prompt.
492
+ const gmoPreClaim = (ctx) => {
493
+ const { url, method } = ctx.request;
494
+ if (method.toUpperCase() === 'POST' && isGmoTokenHost(url) && ctx.vault.isCardRequest(url, method)) {
495
+ let body;
496
+ let headers;
497
+ try {
498
+ body = ctx.request.body;
499
+ headers = ctx.request.headers;
500
+ }
501
+ catch {
502
+ return gmoUnreadable(ctx);
503
+ }
504
+ if (body === null)
505
+ return gmoUnreadable(ctx);
506
+ const reason = gmoRequestRefusal({ url, method, headers, body });
507
+ return reason === null ? undefined : gmoUnsupported(ctx, reason);
508
+ }
509
+ if (isGmoTokenV1Request(url, method) && recognizesGmo(ctx.vault))
510
+ return gmoUnsupported(ctx, 'unsupported_gmo_token_v1');
511
+ return undefined;
512
+ };
513
+ async function gmoUnreadable(ctx) {
514
+ ctx.onEvent?.({ type: 'blocked', detail: 'unreadable_gmo_request' });
515
+ await ctx.effects.abort();
516
+ return { kind: 'abort' };
517
+ }
518
+ async function gmoUnsupported(ctx, reason) {
519
+ const { url, method } = ctx.request;
520
+ ctx.preparationGate.invalidate('unsupported_checkout');
521
+ ctx.lifecycle.unsupported(reason);
522
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason } });
523
+ await ctx.effects.abort();
524
+ return { kind: 'abort' };
525
+ }
526
+ // CyberSource Flex Microform (cybersource-flex.ts). A POST to a Flex host that the registry
527
+ // recognizes is this gate's to judge before the terminal arbiter, which would pause every one of
528
+ // them: the same tokens URL carries Microform v0.x and Flex API direct traffic, and only the
529
+ // field iframe's URL fragment says which card field sent it and under which capture context.
530
+ // - a check (eCheck) Microform: continue untouched, it carries no card;
531
+ // - not a field iframe's sealed card, a frame read and found to be no Microform field, an
532
+ // unreviewed version, or fields the device cannot rebuild: abort as unsupported, before any
533
+ // authorization, and hold the attachment (the page's next request would be judged the same way);
534
+ // - a capture context that does not verify, has expired, names another URL, or did not seal
535
+ // this body, a body the adapter could not read, or a frame it could not read: abort this
536
+ // request only, since the page's next request can pass (a freshly minted context, a frame
537
+ // read on the next try);
538
+ // - otherwise pause, carrying the sidecar the create sends and the request's retry shape.
539
+ // Nothing is decided without the registry: an SDK that did not sync with the capability, or whose
540
+ // org Agentcard has not enabled for CyberSource Flex (the API withholds the recognizer), never
541
+ // recognizes a Flex host, so the gate defers and the request continues as before. Synchronous
542
+ // once the context's verification settled (the adapters start it when the field iframe loads).
543
+ const FLEX_UNREADABLE = { kind: 'refuse', reason: 'microform_frame_unread' };
544
+ const cybersourceFlexPreClaim = (ctx) => {
545
+ const { url, method } = ctx.request;
546
+ if (method.toUpperCase() !== 'POST' || !isCybersourceFlexHost(url) || !ctx.vault.isCardRequest(url, method))
547
+ return undefined;
548
+ let decision;
549
+ try {
550
+ decision = ctx.cybersourceFlex ? ctx.cybersourceFlex.judge(ctx.request) : FLEX_UNREADABLE;
551
+ }
552
+ catch {
553
+ decision = FLEX_UNREADABLE;
554
+ }
555
+ return decision instanceof Promise
556
+ ? decision.then((settled) => cybersourceFlexResolve(ctx, settled), () => cybersourceFlexResolve(ctx, FLEX_UNREADABLE))
557
+ : cybersourceFlexResolve(ctx, decision);
558
+ };
559
+ function cybersourceFlexResolve(ctx, decision) {
560
+ if (decision.kind === 'pause')
561
+ return { kind: 'pause', stripeStep: ctx.stripeStep, cybersourceFlex: decision.flex };
562
+ if (decision.kind === 'continue')
563
+ return cybersourceFlexContinue(ctx);
564
+ if (decision.kind === 'unsupported')
565
+ return cybersourceFlexUnsupported(ctx, decision.reason);
566
+ return cybersourceFlexRefuse(ctx, decision.reason);
567
+ }
568
+ async function cybersourceFlexContinue(ctx) {
569
+ await ctx.effects.continueRequest().catch(() => { });
570
+ return { kind: 'continue' };
571
+ }
572
+ async function cybersourceFlexUnsupported(ctx, reason) {
573
+ const { url, method } = ctx.request;
574
+ ctx.preparationGate.invalidate('unsupported_checkout');
575
+ ctx.lifecycle.unsupported('microform_unsupported');
576
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, processor: CYBERSOURCE_FLEX_PSP, reason } });
577
+ await ctx.effects.abort();
578
+ return { kind: 'abort' };
579
+ }
580
+ async function cybersourceFlexRefuse(ctx, reason) {
581
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
582
+ await ctx.effects.abort();
583
+ return { kind: 'abort' };
584
+ }
585
+ // Juspay Express Checkout (juspay-gate.ts; payment-core juspay.js holds the
586
+ // rules). While the attachment serves the Juspay recognizer, every request to a
587
+ // Juspay host is this gate's: the terminal arbiter would continue the page's
588
+ // placeholder tokenize (no recognizer names it) and pause any /txns.
589
+ // - answer_tokenize: the placeholder tokenize is answered here with a
590
+ // synthetic token, remembered for the /txns that follows. Nothing reaches
591
+ // Juspay. The answer runs inside the gate's own try, so a failed fulfill
592
+ // forgets the token and aborts the request; an attachment that is not
593
+ // ready, was cancelled or has latched a terminal failure answers nothing.
594
+ // - pause: the /txns on a token this attachment issued, in the reviewed
595
+ // shape. Synchronous, so the claim flow still sets activeRequest in the
596
+ // paused request's own microtask.
597
+ // - continue: a request that carries no card of the vault's (net banking,
598
+ // UPI, a saved card, the bank's pay/start redirect, a preflight, a read).
599
+ // - abort, this request only: the placeholder's fingerprint lookup (the page
600
+ // carries on without it), a /txns on a synthetic token this attachment
601
+ // never issued, and a body the adapter could not read.
602
+ // - abort as unsupported: every other card-bearing request (save card, EMI,
603
+ // a direct card, a sibling host), before any authorization, holding the
604
+ // attachment the way an unrecognized payment endpoint does.
605
+ const JUSPAY_REFUSED_ALONE = new Set(['placeholder_fingerprint', 'unknown_synthetic_token', 'request_body_unreadable']);
606
+ const juspayPreClaim = (ctx) => {
607
+ const gate = ctx.juspay;
608
+ const decision = gate?.classify(ctx.request);
609
+ if (!gate || !decision)
610
+ return undefined;
611
+ if (decision.decision === 'pause')
612
+ return { kind: 'pause', stripeStep: ctx.stripeStep };
613
+ if (decision.decision === 'answer_tokenize')
614
+ return juspayAnswer(ctx, gate, decision.host, decision.merchantId);
615
+ if (decision.decision === 'continue')
616
+ return juspayContinue(ctx);
617
+ return JUSPAY_REFUSED_ALONE.has(decision.reason) ? juspayRefuse(ctx, decision.reason) : juspayUnsupported(ctx, decision.reason);
618
+ };
619
+ async function juspayAnswer(ctx, gate, host, merchantId) {
620
+ if (!ctx.attachmentReady() || ctx.interceptionAborted() || ctx.lifecycleAborted() || ctx.terminal()) {
621
+ ctx.onEvent?.({ type: 'blocked', detail: 'checkout_interception_not_ready' });
622
+ await ctx.effects.abort();
623
+ return { kind: 'abort' };
624
+ }
625
+ const { token, response } = gate.issue(host, merchantId);
626
+ try {
627
+ await ctx.effects.answer(response);
628
+ }
629
+ catch {
630
+ gate.forget(token);
631
+ ctx.onEvent?.({ type: 'blocked', detail: 'juspay_tokenize_answer_failed' });
632
+ await ctx.effects.abort();
633
+ return { kind: 'abort' };
634
+ }
635
+ ctx.onEvent?.({ type: 'checkout_prepared', detail: { processor: JUSPAY_PSP } });
636
+ return { kind: 'answer' };
637
+ }
638
+ async function juspayContinue(ctx) {
639
+ await ctx.effects.continueRequest().catch(() => { });
640
+ return { kind: 'continue' };
641
+ }
642
+ async function juspayRefuse(ctx, reason) {
643
+ ctx.onEvent?.({ type: 'blocked', detail: `juspay_${reason}` });
644
+ await ctx.effects.abort();
645
+ return { kind: 'abort' };
646
+ }
647
+ async function juspayUnsupported(ctx, reason) {
648
+ const { url, method } = ctx.request;
649
+ ctx.preparationGate.invalidate('unsupported_checkout');
650
+ ctx.lifecycle.unsupported('juspay_request_unsupported');
651
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, processor: JUSPAY_PSP, reason } });
652
+ await ctx.effects.abort();
653
+ return { kind: 'abort' };
654
+ }
655
+ // Datatrans Secure Fields (datatrans.ts). The card-number iframe's card URL also takes posts the
656
+ // device could never fill, and British Airways's page submits the same form again on every blur
657
+ // and expiry change, so a POST the registry recognizes on a Datatrans pay host is judged here,
658
+ // before the terminal arbiter would pause it, by payment-core's own pause decision under the
659
+ // card-on-file policy the API served with the recognizer (VaultClient.cardOnFilePolicy):
660
+ // - a sound TOKENIZE card post defers, and the arbiter pauses it for approval, unless the same
661
+ // frame's same form was already answered with an approved token (the repeat rule) and that
662
+ // approval is still the one being handed off (datatransRepeatApproval): then that stored answer
663
+ // is fulfilled here, with no claim, create or prompt. Once the answer reached the page, the
664
+ // application's merchant resolver must first report the order still pending
665
+ // (datatransRepeatAfterHandoff);
666
+ // - a card post the API would refuse at create aborts with a `blocked` event that names the
667
+ // API's own code, `invalid_datatrans_request` or `datatrans_card_on_file_refused` (the latter
668
+ // only while the served policy refuses card on file), before anything is claimed, created or
669
+ // prompted. Like Braintree's unsupported operation it neither holds the attachment nor changes
670
+ // its state, so the page's next post is judged afresh;
671
+ // - a post with no card number (a wallet, bank-account or CVV-only post) defers, and continues
672
+ // untouched through the recognizer's passWithoutCard.
673
+ // Nothing is decided without the registry: an SDK the API did not serve the datatrans recognizer
674
+ // never recognizes a pay host, so the gate defers and every Datatrans request continues as before.
675
+ // paymentField/cardInfo (the BIN lookup) and the iframe documents are not card requests and never
676
+ // reach it. Synchronous until an effect, so a card post pauses with no await. A check that throws
677
+ // blocks the request as invalid rather than letting it reach a prompt.
678
+ const datatransPreClaim = (ctx) => {
679
+ const { url, method } = ctx.request;
680
+ if (method.toUpperCase() !== 'POST' || !isDatatransHost(url) || !ctx.vault.isCardRequest(url, method))
681
+ return undefined;
682
+ let decision;
683
+ try {
684
+ decision = datatransPauseDecision({ url, headers: ctx.request.headers, body: ctx.request.body }, ctx.vault.cardOnFilePolicy?.(url, method));
685
+ }
686
+ catch {
687
+ decision = DATATRANS_INVALID_REQUEST;
688
+ }
689
+ if (decision === null)
690
+ return undefined;
691
+ if (decision !== 'card')
692
+ return datatransBlock(ctx, decision);
693
+ const approval = datatransRepeatApproval(ctx);
694
+ const repeat = approval === null ? null : ctx.datatrans?.answerFor(ctx.frame, url, ctx.request.body, approval) ?? null;
695
+ if (!repeat)
696
+ return undefined;
697
+ return ctx.lifecycle.getState().status === 'awaiting_merchant' ? datatransRepeatAfterHandoff(ctx, repeat) : datatransRepeat(ctx, repeat);
698
+ };
699
+ /**
700
+ * The approval whose stored Datatrans answer a repeat may still receive, or null. A stored token
701
+ * answers a re-submit only while the approval it came from is still being handed off: the lifecycle
702
+ * names it and reads awaiting_approval (its answer is being delivered) or awaiting_merchant (the
703
+ * merchant has not confirmed yet), and nothing latched, tore down or cancelled the attachment. The
704
+ * order completing or failing, an unknown outcome, a decline, a hold for the cardholder, a retry
705
+ * after a merchant failure and a new authorization all end it, and the re-submit is then a new
706
+ * question for the terminal arbiter, which holds or pauses it like any card post: TOKENIZE binds no
707
+ * amount, so one approval never answers for a later purchase. Once the answer reached the page
708
+ * (awaiting_merchant), the gate also reconciles before it answers (datatransRepeatAfterHandoff), and
709
+ * the stored answer itself ends once its form goes quiet (DatatransRepeats, DATATRANS_REPEAT_IDLE_MS).
710
+ */
711
+ function datatransRepeatApproval(ctx) {
712
+ if (ctx.terminal() || ctx.interceptionAborted() || ctx.lifecycleAborted())
713
+ return null;
714
+ const { status, authorizationId } = ctx.lifecycle.getState();
715
+ return status === 'awaiting_approval' || status === 'awaiting_merchant' ? authorizationId : null;
716
+ }
717
+ /**
718
+ * A re-submit after the approved answer reached the page. The merchant can already hold an order paid
719
+ * with that token, and the post names no amount and no order, so nothing in the browser tells the same
720
+ * checkout's re-submit from a later purchase in the same form. The gate asks the application's merchant
721
+ * resolver first (lifecycle.merchantOrderPending()), and the stored answer goes out only while the
722
+ * resolver reports this approval's order still pending and the checkout still awaits the merchant for
723
+ * it. A completed, failed or unknown order, a resolver that throws or has not answered within
724
+ * MERCHANT_ORDER_READ_MS, and a request in flight make the re-submit a new question for the terminal
725
+ * arbiter, and so does every re-submit on an attachment with no resolver: a later purchase never
726
+ * receives an earlier token without the cardholder, and the paused post never waits on a stalled read.
727
+ */
728
+ function datatransRepeatAfterHandoff(ctx, repeat) {
729
+ if (!ctx.lifecycle.hasMerchantResolver())
730
+ return undefined;
731
+ return datatransPendingRepeat(ctx, repeat);
732
+ }
733
+ async function datatransPendingRepeat(ctx, repeat) {
734
+ if (!await ctx.lifecycle.merchantOrderPending())
735
+ return undefined;
736
+ // The resolver answered asynchronously: the checkout must still be handing off this approval.
737
+ if (ctx.lifecycle.getState().status !== 'awaiting_merchant' || datatransRepeatApproval(ctx) !== repeat.authorizationId)
738
+ return undefined;
739
+ return datatransRepeat(ctx, repeat);
740
+ }
741
+ async function datatransBlock(ctx, reason) {
742
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
743
+ await ctx.effects.abort();
744
+ return { kind: 'abort' };
745
+ }
746
+ async function datatransRepeat(ctx, repeat) {
747
+ try {
748
+ await ctx.effects.answer(repeat.response);
749
+ }
750
+ catch {
751
+ ctx.onEvent?.({ type: 'blocked', detail: 'datatrans_repeat_unanswered' });
752
+ await ctx.effects.abort();
753
+ return { kind: 'abort' };
754
+ }
755
+ ctx.onEvent?.({ type: 'repeat_answered', detail: { psp: 'datatrans', authorizationId: repeat.authorizationId } });
756
+ return { kind: 'answer' };
757
+ }
758
+ // Pelecard Iframe/Redirect 2.0 (pelecard.ts). The payment page's card URL, POST
759
+ // /PaymentGW/Transaction on gateway21 or gateway20.pelecard.biz, also carries the wallet, Bit,
760
+ // PIN pad and bank tabs with no card, and card-shaped bodies the device could never fill: a saved
761
+ // token, a card track, a phone authorization, installments, a buyer-typed total. So a POST the
762
+ // registry recognizes on a Pelecard host that carries a card is judged here, before the terminal
763
+ // arbiter would pause it, by the whole-request check the API runs at create:
764
+ // - the page's own one-time new-card submission defers, and the arbiter pauses it for approval;
765
+ // - anything else aborts with a `blocked` event that names why, with no claim, create or
766
+ // prompt: `invalid_pelecard_request`, or `pelecard_keep_on_file_unsupported` for a
767
+ // submission that asks Pelecard to keep the card on file. Like Braintree's unsupported
768
+ // operation it neither holds the attachment nor changes its state, so the page's next
769
+ // submission (the buyer unticks "save my card") is judged afresh.
770
+ // A body with no card defers too, and the adapter continues it untouched in every state
771
+ // (passWithoutCard); so does a body the adapter could not read, which the adapter fails on its
772
+ // own as unreadable, that request only. Nothing is decided without the registry: an SDK that did
773
+ // not sync with the pelecard_transaction capability, or whose org Agentcard has not enabled for
774
+ // Pelecard (the API then serves no pelecard recognizer), never recognizes a Pelecard host, so the
775
+ // gate defers and the request continues as before. Synchronous until the abort, so a card
776
+ // submission pauses with no await. A check that throws blocks the request as invalid rather than
777
+ // letting it reach a prompt.
778
+ const pelecardPreClaim = (ctx) => {
779
+ const { url, method } = ctx.request;
780
+ if (method.toUpperCase() !== 'POST' || !isPelecardHost(url) || !ctx.vault.isCardRequest(url, method))
781
+ return undefined;
782
+ if (ctx.request.body == null || ctx.vault.withoutCard?.(url, ctx.request.body) === 'continue')
783
+ return undefined;
784
+ let reason;
785
+ try {
786
+ reason = pelecardRefusal({ url, headers: ctx.request.headers, body: ctx.request.body });
787
+ }
788
+ catch {
789
+ reason = 'invalid_pelecard_request';
790
+ }
791
+ return reason === null ? undefined : pelecardBlock(ctx, reason);
792
+ };
793
+ async function pelecardBlock(ctx, reason) {
794
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
795
+ await ctx.effects.abort();
796
+ return { kind: 'abort' };
797
+ }
798
+ // ACI COPYandPAY, once the registry serves it (a synced build that declares its
799
+ // capability); for every other client the gate defers on everything, so nothing
800
+ // changes. On a POST to one of ACI's checkouts (aci.ts, payment-core aci.js):
801
+ // - the card-number request defers to the arbiter, which pauses it for the
802
+ // vault claim flow (a prepared checkout only: an unprepared one is refused
803
+ // there with PreparationRequiredError before any create), unless its
804
+ // checkout's card-number request is running or was answered here: that one
805
+ // is refused as aci_checkout_reused before it claims anything;
806
+ // - the page's CVC and expiry requests are held until that request's
807
+ // approval ends, then answered with its answer, never forwarded; they fail
808
+ // when it failed or never began;
809
+ // - the holder request continues untouched;
810
+ // - the /payment submission continues once per answered card-number request,
811
+ // as a frame navigation, with the placeholder expiry a classic-mode page
812
+ // puts there removed (only where the browser sends a replaced navigation
813
+ // body, else aci_expiry_unstrippable), or is refused with the judge's code;
814
+ // - anything else is refused with payment-core's code: a request that asks ACI
815
+ // to keep the card or pay with a stored one (aci_card_on_file_refused, which
816
+ // also retires a ready preparation) or one the widget never sends.
817
+ // Everything else on ACI's hosts (the widget script, /bins, /v1/collect, GETs)
818
+ // is not a POST to a checkout and defers. Synchronous for the card-number
819
+ // request, so it pauses in the microtask it paused in.
820
+ const aciPreClaim = (ctx) => {
821
+ const leg = ctx.aci?.classify(ctx.request.url, ctx.request.method, ctx.request.body);
822
+ if (!leg)
823
+ return undefined;
824
+ if (leg.leg === 'refuse') {
825
+ ctx.aci.refused(leg);
826
+ return aciRefuse(ctx, leg.code);
827
+ }
828
+ if (leg.leg === 'pass')
829
+ return aciContinue(ctx);
830
+ if (leg.leg === 'absorb')
831
+ return aciHold(ctx, leg);
832
+ if (leg.leg === 'continuation')
833
+ return aciPayment(ctx);
834
+ // The card-number request: the arbiter pauses it for the vault claim flow,
835
+ // unless this checkout already has one running or answered (AciGate.begin).
836
+ if (ctx.aci.reused(leg))
837
+ return aciRefuse(ctx, ACI_CHECKOUT_REUSED);
838
+ return undefined;
839
+ };
840
+ async function aciContinue(ctx) {
841
+ await ctx.effects.continueRequest().catch(() => { });
842
+ return { kind: 'continue' };
843
+ }
844
+ async function aciHold(ctx, leg) {
845
+ const answer = await ctx.aci.hold(leg);
846
+ if (answer && !ctx.interceptionAborted() && !ctx.lifecycleAborted()) {
847
+ try {
848
+ await ctx.effects.answer(answer);
849
+ ctx.onEvent?.({ type: 'aci_leg_absorbed', detail: { url: redactUrl(ctx.request.url) } });
850
+ return { kind: 'answer' };
851
+ }
852
+ catch { /* the page's request is gone; nothing was forwarded */ }
853
+ }
854
+ ctx.onEvent?.({ type: 'blocked', detail: 'aci_leg_unanswered' });
855
+ await ctx.effects.abort();
856
+ return { kind: 'abort' };
857
+ }
858
+ async function aciPayment(ctx) {
859
+ const { url, method, body } = ctx.request;
860
+ const stopped = () => ctx.interceptionAborted() || ctx.lifecycleAborted() || ctx.terminal();
861
+ let verdict = stopped() ? { outcome: 'refuse', code: 'checkout_interception_not_ready' }
862
+ : ctx.aci.payment(url, method, body, { navigation: ctx.request.navigation === true });
863
+ // A classic-mode body continues only where the replaced body is known to
864
+ // reach ACI on a navigation; elsewhere the placeholder expiry would, so it is
865
+ // refused and the reservation stays taken. That check can wait on the
866
+ // browser, so the stop conditions are read again after it.
867
+ if (verdict.outcome === 'continue' && verdict.stripped && !(await replacesNavigationBody(ctx))) {
868
+ verdict = { outcome: 'refuse', code: ACI_EXPIRY_UNSTRIPPABLE };
869
+ }
870
+ if (verdict.outcome === 'continue' && stopped())
871
+ verdict = { outcome: 'refuse', code: 'checkout_interception_not_ready' };
872
+ if (verdict.outcome === 'refuse') {
873
+ ctx.onEvent?.({ type: 'blocked', detail: verdict.code });
874
+ await ctx.effects.abort();
875
+ return { kind: 'abort' };
876
+ }
877
+ // A classic-mode body continues with the placeholder expiry taken out; an
878
+ // untouched one continues exactly as the page sent it. The reservation stays
879
+ // taken either way, so a failed continue is never followed by a second post.
880
+ try {
881
+ await ctx.effects.continueRequest(verdict.stripped ? verdict.body : undefined);
882
+ }
883
+ catch {
884
+ ctx.onEvent?.({ type: 'blocked', detail: 'aci_payment_unsent' });
885
+ return { kind: 'continue' };
886
+ }
887
+ ctx.onEvent?.({ type: 'aci_payment_continued', detail: { authorizationId: verdict.authorizationId, stripped: verdict.stripped } });
888
+ return { kind: 'continue' };
889
+ }
890
+ /** Whether this browser sends the body the SDK replaced on a navigation; an absent or failing answer is no. */
891
+ async function replacesNavigationBody(ctx) {
892
+ try {
893
+ return (await ctx.replacesNavigationBody?.()) === true;
894
+ }
895
+ catch {
896
+ return false;
897
+ }
898
+ }
899
+ async function aciRefuse(ctx, code) {
900
+ // A card that may not be kept on file can never pay this checkout: a ready
901
+ // preparation is retired with that reason rather than left to expire.
902
+ if (code === 'aci_card_on_file_refused')
903
+ ctx.preparationGate.invalidate(code);
904
+ ctx.onEvent?.({ type: 'blocked', detail: code });
905
+ await ctx.effects.abort();
906
+ return { kind: 'abort' };
907
+ }
908
+ // CreditGuard's hosted payment page (Hyp PPS), in two parts the generic card
909
+ // path cannot judge. Every other request on the PPS hosts (the page, its
910
+ // transaction read, /card/details, /cg3ds, assets) and the stock new-card
911
+ // submit defer, synchronously, and the terminal arbiter continues or pauses
912
+ // them; the pause then needs a ready preparation for this page's transaction.
913
+ // - A /process submit the cardholder's device would not fill (a saved card,
914
+ // a wallet, "save my card", installments, DCC, an Israeli ID, a gift card,
915
+ // another method, an unreadable body or other headers) is aborted with the device's own
916
+ // reason, before any claim, create or prompt. A ready preparation stays
917
+ // ready, so the page's corrected submit can still pay.
918
+ // - PPS runs 3-D Secure itself, sending the placeholder card to Nuvei from
919
+ // inside its frame. The nuvei recognizer would pause that as a Nuvei
920
+ // payment. A request to a Nuvei host that a PPS document sent, or that any
921
+ // frame inside a PPS document sent, is aborted instead, the checkout
922
+ // becomes unsupported and any preparation is retired: the page's flow is
923
+ // one the Vault refuses. See nuveiPreClaim for which requests wait on a
924
+ // frame lookup; no request to another host ever does.
925
+ const PPS_DOCUMENT = 'https://pps.creditguard.co.il/';
926
+ const sendsToNuvei = (url) => creditguardNuveiRefusal(url, PPS_DOCUMENT) !== null;
927
+ const headerValue = (headers, name) => Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
928
+ const onPpsHost = (url) => {
929
+ try {
930
+ return CREDITGUARD_PPS_HOSTS.includes(new URL(url).hostname.toLowerCase());
931
+ }
932
+ catch {
933
+ return false;
934
+ }
935
+ };
936
+ // ctx.request.headers is read only once a request is CreditGuard's or Nuvei's: the adapters read
937
+ // headers lazily, and a request mock without them must still pass every other gate.
938
+ const creditguardPreClaim = (ctx) => {
939
+ const { url, method } = ctx.request;
940
+ if (sendsToNuvei(url))
941
+ return nuveiPreClaim(ctx);
942
+ // The body is read only for a request to a PPS host, as every gate reads one only on its own hosts:
943
+ // the adapters read it lazily, and a body they cannot read still reaches the rest of the list.
944
+ if (!onPpsHost(url))
945
+ return undefined;
946
+ const { body } = ctx.request;
947
+ // Any method reaches the /process check: payment-core blocks a non-POST, a query or an unreadable
948
+ // body there as well, so nothing but the stock POST ever continues to CreditGuard.
949
+ const verb = method.toUpperCase();
950
+ const verdict = classifyCreditguardRequest(url, verb, body ?? '', undefined);
951
+ if (verdict === null)
952
+ return undefined;
953
+ const refusal = creditguardProcessRefusal({ url, method: verb, headers: ctx.request.headers, body: body ?? '' }, undefined);
954
+ if (verdict === 'card' && refusal === null)
955
+ return undefined;
956
+ return creditguardBlock(ctx, refusal ?? 'malformed_body');
957
+ };
958
+ /**
959
+ * Who sent a request to a Nuvei host, as far as it decides the request:
960
+ * 1. The browser sets Origin and Referer, and no page can make either name another page's
961
+ * document, so one that names PPS settles it at once, for any request.
962
+ * 2. Only a card request the nuvei recognizer would pause can be approved as a Nuvei payment.
963
+ * Anything else (a GET, another Nuvei path) continues as it always has, with no lookup.
964
+ * 3. Otherwise the frames that sent it are read, up to the top, and a PPS document anywhere among
965
+ * them refuses the request, whichever page its Origin names: the sender may be the merchant's
966
+ * own page running Nuvei's Web SDK, one of Nuvei's own frames, which sends for whichever page
967
+ * frames it, or any other frame, and only the frames above it say whether PPS framed it. With
968
+ * an adapter that reads the chain at once (Playwright) the decision still comes in the
969
+ * request's own microtask. A chain the adapter cannot read leaves the request to the nuvei
970
+ * recognizer, as before; the prepare-time refusal of a 3-D Secure transaction still keeps the
971
+ * CreditGuard payment from starting.
972
+ */
973
+ function nuveiPreClaim(ctx) {
974
+ const { url, method, headers } = ctx.request;
975
+ if ([headerValue(headers, 'origin'), headerValue(headers, 'referer')].some(sender => creditguardNuveiRefusal(url, sender) !== null))
976
+ return creditguardThreeDSecure(ctx);
977
+ if (!ctx.vault.isCardRequest(url, method))
978
+ return undefined;
979
+ const decide = (frames) => frames?.some(frame => creditguardNuveiRefusal(url, frame) !== null) ? creditguardThreeDSecure(ctx) : undefined;
980
+ let frames;
981
+ try {
982
+ frames = ctx.initiatorFrames?.();
983
+ }
984
+ catch {
985
+ frames = undefined;
986
+ }
987
+ return frames instanceof Promise ? frames.then(decide, () => undefined) : decide(frames);
988
+ }
989
+ async function creditguardBlock(ctx, refusal) {
990
+ ctx.onEvent?.({ type: 'blocked', detail: { reason: 'creditguard_request_unsupported', refusal } });
991
+ await ctx.effects.abort();
992
+ return { kind: 'abort' };
993
+ }
994
+ async function creditguardThreeDSecure(ctx) {
995
+ const { url, method } = ctx.request;
996
+ ctx.preparationGate.invalidate('creditguard_three_d_secure');
997
+ ctx.lifecycle.unsupported('creditguard_three_d_secure');
998
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, processor: 'creditguard', reason: 'creditguard_three_d_secure' } });
999
+ await ctx.effects.abort();
1000
+ return { kind: 'abort' };
1001
+ }
1002
+ // Primer (primer.ts). Primer's /payment-instruments also carries requests the vault can never pay,
1003
+ // so a POST the registry recognizes on a Primer pci host is judged here, before the terminal
1004
+ // arbiter would pause it, by payment-core's own pause decision (classifyPrimerRequest), the rule
1005
+ // the API runs at create:
1006
+ // - a one-time new card from the legacy v2 controller defers, and the arbiter pauses it; a body
1007
+ // with no card (PayPal, Apple Pay, Google Pay, Klarna) defers too, and the card-field rule
1008
+ // after the pause continues it untouched;
1009
+ // - a body that asks Primer to keep the card is aborted alone with a `blocked` event,
1010
+ // `primer_keep_on_file`: a save-card choice on the page decides it, so the attachment is not
1011
+ // held and the next request is judged again;
1012
+ // - Primer Checkout (primer-js) and any other shape the legacy controller would not send abort as
1013
+ // an unsupported checkout, `unsupported_checkout` with reason `unsupported_primer_sdk` or
1014
+ // `unsupported_primer_request`, with no claim, create or prompt. The attachment then reads
1015
+ // `unsupported` and holds: the merchant's integration decides that shape.
1016
+ // Nothing is decided without the registry: an SDK the API did not serve Primer (a build without the
1017
+ // capability, or an org Agentcard has not enabled) recognizes no Primer URL, so the gate defers and
1018
+ // every Primer request continues as before. The preflight OPTIONS and every other method continue
1019
+ // through the arbiter. A body the adapter could not read says nothing about the page, so that
1020
+ // request alone is aborted (`unreadable_primer_request`). Synchronous until the abort, so a card
1021
+ // request pauses with no await.
1022
+ const primerPreClaim = (ctx) => {
1023
+ const { url, method } = ctx.request;
1024
+ if (method.toUpperCase() !== 'POST' || !isPrimerPciHost(url) || !ctx.vault.isCardRequest(url, method))
1025
+ return undefined;
1026
+ const verdict = primerRequestVerdict(ctx.request);
1027
+ if (verdict.kind === 'block')
1028
+ return primerBlocked(ctx, verdict.reason);
1029
+ if (verdict.kind === 'unsupported')
1030
+ return primerUnsupported(ctx, verdict.reason);
1031
+ return undefined;
1032
+ };
1033
+ async function primerBlocked(ctx, reason) {
1034
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
1035
+ await ctx.effects.abort();
1036
+ return { kind: 'abort' };
1037
+ }
1038
+ async function primerUnsupported(ctx, reason) {
1039
+ const { url, method } = ctx.request;
1040
+ ctx.preparationGate.invalidate('unsupported_checkout');
1041
+ ctx.lifecycle.unsupported(reason);
1042
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason } });
1043
+ await ctx.effects.abort();
1044
+ return { kind: 'abort' };
1045
+ }
1046
+ // Payrails web SDK v6 (payrails.ts). The adapter captures Payrails sessions only while the synced
1047
+ // registry pauses Payrails' card save (an org the API served the dark entry to); otherwise
1048
+ // ctx.payrails is null, this gate defers, and every Payrails request continues as before. For a
1049
+ // POST to a Payrails host:
1050
+ // - a POST that carries no body the gate can read aborts that request alone
1051
+ // (`unreadable_payrails_request`): no body, an empty one, or one the adapter could not read.
1052
+ // CDP reports '' for the first two and null for the third (a streamed upload, for one);
1053
+ // Playwright reports null for all three, and no Playwright call tells them apart. So the gate
1054
+ // refuses '' and null alike: both adapters then agree, and a streamed card-form payment cannot
1055
+ // pass unread on Playwright. payrails.js 6.5.0 never posts without a body (its request helper
1056
+ // sends JSON.stringify(body || {}) on every POST), and the secure frame v6 posts JSON. Payrails'
1057
+ // static host (assets.payrails.io) is the one exception: it serves the SDK's and the secure
1058
+ // frame's files and takes no payment, so another script's bodyless POST there continues
1059
+ // untouched on both adapters;
1060
+ // - a payment that carries an encrypted card inside paymentComposition (the card form paying,
1061
+ // a stored card with a CVC field) aborts as an unsupported checkout,
1062
+ // `unsupported_payrails_card_payment`, and the attachment holds;
1063
+ // - a card save the registry recognizes is judged first by payment-core's guard and its
1064
+ // card-on-file policy: a refusal aborts as an unsupported checkout with that code, and the
1065
+ // attachment holds. Then it is bound to the one captured session its token, URL,
1066
+ // holderReference and providerConfigId name, and the session's key is checked against the key
1067
+ // the page's frame wrapped. A miss aborts that request alone (`payrails_session_unavailable`,
1068
+ // or the key's code) with a message to reload the checkout, and the attachment does not hold,
1069
+ // so the save after a reload is judged afresh. A bound save pauses carrying the key;
1070
+ // - anything else defers to the arbiter: a save with no card pauses there and the claim flow
1071
+ // continues it (passWithoutCard), and every other request continues.
1072
+ // Synchronous unless answer bodies are still being read (the save then waits for them, briefly)
1073
+ // or the request aborts.
1074
+ const payrailsPreClaim = (ctx) => {
1075
+ const capture = ctx.payrails;
1076
+ const { url, method } = ctx.request;
1077
+ if (!capture || method.toUpperCase() !== 'POST' || !isPayrailsHost(url))
1078
+ return undefined;
1079
+ const body = ctx.request.body;
1080
+ if (body === null || body === '')
1081
+ return isPayrailsStaticHost(url) ? undefined : payrailsUnreadable(ctx);
1082
+ const kind = payrailsRequestKind(url, method, body);
1083
+ if (kind === 'card_payment')
1084
+ return payrailsUnsupported(ctx, 'unsupported_payrails_card_payment');
1085
+ if (kind !== 'card' || !ctx.vault.isCardRequest(url, method))
1086
+ return undefined;
1087
+ const refusal = payrailsSaveRefusal(body);
1088
+ if (refusal)
1089
+ return payrailsUnsupported(ctx, refusal);
1090
+ return capture.pending ? capture.settle().then(() => payrailsBind(ctx, capture, body)) : payrailsBind(ctx, capture, body);
1091
+ };
1092
+ function payrailsBind(ctx, capture, body) {
1093
+ const key = capture.keyFor({ url: ctx.request.url, headers: ctx.request.headers, body });
1094
+ if (key === null)
1095
+ return payrailsUnsupported(ctx, 'payrails_session_unavailable');
1096
+ const refusal = payrailsKeyRefusal(key, body);
1097
+ if (refusal)
1098
+ return payrailsUnsupported(ctx, refusal);
1099
+ return { kind: 'pause', stripeStep: ctx.stripeStep, payrails: { encryption_public_key: key } };
1100
+ }
1101
+ async function payrailsUnreadable(ctx) {
1102
+ ctx.onEvent?.({ type: 'blocked', detail: 'unreadable_payrails_request' });
1103
+ await ctx.effects.abort();
1104
+ return { kind: 'abort' };
1105
+ }
1106
+ async function payrailsUnsupported(ctx, reason) {
1107
+ const { url, method } = ctx.request;
1108
+ const holds = PAYRAILS_HOLDING_REASONS.includes(reason);
1109
+ if (holds) {
1110
+ ctx.preparationGate.invalidate('unsupported_checkout');
1111
+ ctx.lifecycle.unsupported(reason);
1112
+ }
1113
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason,
1114
+ ...(holds ? {} : { message: PAYRAILS_SESSION_MESSAGE }) } });
1115
+ await ctx.effects.abort();
1116
+ return { kind: 'abort' };
1117
+ }
1118
+ // Elavon Converge's Hosted Payment Page (elavon-converge.ts). Its card URL, POST
1119
+ // /hosted-payments/service/payment/hpp/process on api.convergepay.com or
1120
+ // api.demo.convergepay.com, also carries electronic check and financing payments
1121
+ // with no card, and card-shaped bodies the Vault never pays: a wallet with a card
1122
+ // number the server supplied, the debit, gift card and push-to-card tabs, the
1123
+ // follow-ups after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA, a
1124
+ // service fee, recurring or installment fields, card saving, a tip, another
1125
+ // currency. So a POST the registry recognizes on a Converge host that carries a
1126
+ // card is judged here, before the terminal arbiter would pause it, by the
1127
+ // whole-request check the API runs at create:
1128
+ // - the page's own first credit card sale defers, and the arbiter pauses it
1129
+ // for approval;
1130
+ // - anything else aborts with a `blocked` event naming payment-core's code
1131
+ // (converge_wallet_refused, converge_3ds_follow_up_refused, and so on), with
1132
+ // no claim, create or prompt. Like Braintree's unsupported operation it
1133
+ // neither holds the attachment nor changes its state, so the page's next
1134
+ // payment (the buyer leaves Google Pay for the card form) is judged afresh.
1135
+ // A body with no card defers too, and the adapter continues it untouched in every
1136
+ // state (passWithoutCard); so does a body the adapter could not read, which the
1137
+ // adapter fails on its own as unreadable, that request only. Nothing is decided
1138
+ // without the registry: an SDK that did not sync with the converge_hpp_process
1139
+ // capability, or whose org Agentcard has not enabled for Converge (the API then
1140
+ // serves no elavon_converge recognizer), never recognizes a Converge host, so the
1141
+ // gate defers and the request continues as before. Synchronous until the abort,
1142
+ // so a card request pauses with no await. A check that throws blocks the request
1143
+ // as not the page's own rather than letting it reach a prompt.
1144
+ const convergePreClaim = (ctx) => {
1145
+ const { url, method } = ctx.request;
1146
+ if (method.toUpperCase() !== 'POST' || !isConvergeHost(url) || !ctx.vault.isCardRequest(url, method))
1147
+ return undefined;
1148
+ if (ctx.request.body == null || ctx.vault.withoutCard?.(url, ctx.request.body) === 'continue')
1149
+ return undefined;
1150
+ let reason;
1151
+ try {
1152
+ reason = convergeRefusal({ url, headers: ctx.request.headers, body: ctx.request.body });
1153
+ }
1154
+ catch {
1155
+ reason = 'converge_body_malformed';
1156
+ }
1157
+ return reason === null ? undefined : convergeBlock(ctx, reason);
1158
+ };
1159
+ async function convergeBlock(ctx, reason) {
1160
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
1161
+ await ctx.effects.abort();
1162
+ return { kind: 'abort' };
1163
+ }
1164
+ // Worldpay eProtect, once the registry serves it (a synced build that declares
1165
+ // its capability, for an org the API enabled); for every other client the gate
1166
+ // defers on everything, so nothing changes. On eProtect's hosts:
1167
+ // - the registration GET the recognizer admits defers to the arbiter, which
1168
+ // pauses it for the vault claim flow; one the registry does not cover aborts
1169
+ // as unsupported instead of reaching Worldpay;
1170
+ // - any other request on /eProtect/paypage (a CVV-only, PIN, wallet or
1171
+ // private-label variant, the native-app Mobile API form post) aborts as an
1172
+ // unsupported checkout, so the placeholder's ciphertext never leaves;
1173
+ // - the vendor's failover re-send of a registration this attachment saw
1174
+ // aborts quietly: it repeats the placeholder's ciphertext and never prompts
1175
+ // or claims a preparation. A re-send of nothing seen means the registration
1176
+ // went somewhere this SDK does not cover, and aborts as unsupported;
1177
+ // - the vendor's give-up report (/eProtect/ppstats 900 or 901) ends the
1178
+ // approval running for its registration, then continues to Worldpay as the
1179
+ // page's own telemetry;
1180
+ // - everything else there (the scripts, the payframe) continues.
1181
+ // Synchronous for a registration, so it pauses in the microtask it paused in.
1182
+ const eprotectPreClaim = (ctx) => {
1183
+ const request = ctx.eprotect?.classify(ctx.request.url, ctx.request.method);
1184
+ if (!request || request.kind === 'sibling')
1185
+ return undefined;
1186
+ if (request.kind === 'registration') {
1187
+ ctx.eprotect.saw(request);
1188
+ return ctx.vault.isCardRequest(ctx.request.url, ctx.request.method) ? undefined : unsupportedAbort(ctx);
1189
+ }
1190
+ if (request.kind === 'give_up')
1191
+ return eprotectGiveUp(ctx, request, request.code);
1192
+ if (request.kind === 'failover' && ctx.eprotect.repeats(request))
1193
+ return eprotectFailoverAbort(ctx);
1194
+ return unsupportedAbort(ctx);
1195
+ };
1196
+ async function eprotectFailoverAbort(ctx) {
1197
+ ctx.onEvent?.({ type: 'blocked', detail: 'eprotect_failover_aborted' });
1198
+ await ctx.effects.abort();
1199
+ return { kind: 'abort' };
1200
+ }
1201
+ async function eprotectGiveUp(ctx, attempt, code) {
1202
+ const ended = ctx.eprotect.gaveUp(attempt);
1203
+ if (ended)
1204
+ ctx.onEvent?.({ type: 'merchant_request_abandoned', detail: { processor: 'worldpay_eprotect', code, answered: ended === 'answered' } });
1205
+ await ctx.effects.continueRequest().catch(() => { });
1206
+ return { kind: 'continue' };
1207
+ }
1208
+ // WSPay Components (wspay.ts). The card frame's confirm URL also carries saved_card and
1209
+ // temp_card_id bodies with no new card, and the page builds the transaction object itself, so a
1210
+ // POST the registry recognizes on a WSPay host is judged here, before the terminal arbiter would
1211
+ // pause it, by the whole-request check the API runs at create:
1212
+ // - a new card for a one-time payment defers, and the arbiter pauses it for approval;
1213
+ // - anything else aborts with a `blocked` event that names why, with no claim, create or
1214
+ // prompt: `unsupported_wspay_payment_method`, or `wspay_keep_on_file_unsupported` for a
1215
+ // confirm that asks WSPay to keep the card on file. Like Braintree's unsupported operation
1216
+ // it neither holds the attachment nor changes its state, so the page's next confirm (a new
1217
+ // card instead of a saved one) is judged afresh.
1218
+ // Nothing is decided without the registry: an SDK that did not sync with the wspay_confirm
1219
+ // capability, or whose org Agentcard has not enabled for WSPay (the API then serves no WSPay
1220
+ // recognizer), never recognizes a WSPay host, so the gate defers and the request continues as
1221
+ // before. GET /show and /status and POST /pan-info are not card requests and continue through
1222
+ // the arbiter. Synchronous until the abort, so a card confirm pauses with no await. A check that
1223
+ // throws blocks the request as unsupported rather than letting it reach a prompt.
1224
+ const wspayPreClaim = (ctx) => {
1225
+ const { url, method } = ctx.request;
1226
+ if (method.toUpperCase() !== 'POST' || !isWspayHost(url) || !ctx.vault.isCardRequest(url, method))
1227
+ return undefined;
1228
+ let reason;
1229
+ try {
1230
+ reason = wspayRefusal({ url, headers: ctx.request.headers, body: ctx.request.body });
1231
+ }
1232
+ catch {
1233
+ reason = 'unsupported_wspay_payment_method';
1234
+ }
1235
+ return reason === null ? undefined : wspayBlock(ctx, reason);
1236
+ };
1237
+ async function wspayBlock(ctx, reason) {
1238
+ ctx.onEvent?.({ type: 'blocked', detail: reason });
1239
+ await ctx.effects.abort();
1240
+ return { kind: 'abort' };
1241
+ }
1242
+ // Shiji STS (shiji-sts.ts). Its two endpoints also carry requests the vault must never pay, so a
1243
+ // POST the registry recognizes on an STS host is judged here, before the terminal arbiter would
1244
+ // pause it, by payment-core's classifier:
1245
+ // - a getTokenId in the proxy's shape defers, and the arbiter pauses it: the one prompt. While the
1246
+ // card-on-file decision is open the classifier never answers that, and every getTokenId aborts
1247
+ // as an unsupported checkout, `unsupported_checkout` with reason `shiji_sts_keep_on_file`, with
1248
+ // no claim, create or prompt; the attachment then reads `unsupported` and holds;
1249
+ // - a getCvvId never pauses. It continues with the device's CVV ciphertext only when the
1250
+ // attachment's companion binding answers for it (the getTokenId it continued, same page, same
1251
+ // accessToken, within 120 s, once), and aborts as an unsupported checkout otherwise, so the
1252
+ // agent's placeholder CVV never rides beside a real card;
1253
+ // - anything else on those URLs (another shape, a query, a card field under another key) aborts
1254
+ // the same way.
1255
+ // Nothing is decided without the registry: an SDK the API did not serve Shiji (a build without the
1256
+ // capability, or an org Agentcard has not enabled) recognizes neither URL, so the gate defers and
1257
+ // every Shiji request continues as before. The preflight OPTIONS and every other method continue
1258
+ // through the arbiter. A body the adapter could not read aborts that request alone with a `blocked`
1259
+ // event, `unreadable_shiji_sts_request`, and the attachment is not held. Synchronous until the
1260
+ // abort or the companion's continue, so a card request pauses with no await.
1261
+ const shijiStsPreClaim = (ctx) => {
1262
+ const { url, method } = ctx.request;
1263
+ if (method.toUpperCase() !== 'POST' || !isShijiStsHost(url) || !ctx.vault.isCardRequest(url, method))
1264
+ return undefined;
1265
+ const body = ctx.request.body;
1266
+ if (body === null)
1267
+ return shijiStsUnreadable(ctx);
1268
+ const verdict = shijiStsVerdict({ url, method, body });
1269
+ if (verdict.kind === 'pause')
1270
+ return undefined;
1271
+ if (verdict.kind === 'refuse')
1272
+ return shijiStsUnsupported(ctx, verdict.reason);
1273
+ return shijiStsCompanion(ctx, body);
1274
+ };
1275
+ async function shijiStsUnreadable(ctx) {
1276
+ ctx.onEvent?.({ type: 'blocked', detail: 'unreadable_shiji_sts_request' });
1277
+ await ctx.effects.abort();
1278
+ return { kind: 'abort' };
1279
+ }
1280
+ async function shijiStsUnsupported(ctx, reason) {
1281
+ const { url, method } = ctx.request;
1282
+ ctx.preparationGate.invalidate('unsupported_checkout');
1283
+ ctx.lifecycle.unsupported(reason);
1284
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: redactUrl(url), method, reason } });
1285
+ await ctx.effects.abort();
1286
+ return { kind: 'abort' };
1287
+ }
1288
+ async function shijiStsCompanion(ctx, body) {
1289
+ const { url, method } = ctx.request;
1290
+ const shiji = ctx.shijiSts;
1291
+ if (!shiji)
1292
+ return shijiStsUnsupported(ctx, 'shiji_sts_companion_unbound');
1293
+ if (ctx.interceptionAborted() || ctx.lifecycleAborted()) {
1294
+ // The checkout was cancelled or the attachment is tearing down: the CVV stays on the device's side.
1295
+ shiji.companions.release();
1296
+ ctx.onEvent?.({ type: 'blocked', detail: 'checkout_cancelled' });
1297
+ await ctx.effects.abort();
1298
+ return { kind: 'abort' };
1299
+ }
1300
+ const claimed = shiji.companions.claim({ url, method, body }, await shiji.page());
1301
+ if ('reason' in claimed)
1302
+ return shijiStsUnsupported(ctx, claimed.reason);
1303
+ try {
1304
+ await ctx.effects.continueRequest(claimed.body);
1305
+ }
1306
+ catch (error) {
1307
+ ctx.onEvent?.({ type: 'failed', detail: ctx.failureSummary(error) });
1308
+ await ctx.effects.abort().catch(() => { });
1309
+ return { kind: 'abort' };
1310
+ }
1311
+ ctx.onEvent?.({ type: 'companion_continued', detail: { url: redactUrl(url), psp: 'shiji_sts', fields: ['cvv'] } });
1312
+ return { kind: 'continue' };
1313
+ }
336
1314
  // The terminal arbiter, always last. A request that no gate claimed and that is
337
1315
  // neither a native Stripe step nor a recognized card request continues, unless
338
1316
  // it matches a declared payment endpoint we do not support (then it aborts as an
@@ -366,7 +1344,7 @@ async function unsupportedContinue(ctx) {
366
1344
  * arbiter runs after all of them and is not part of this list. A gate appended
367
1345
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
368
1346
  * pauses each one itself. */
369
- export const PSP_PRE_CLAIM_GATES = [mercadoPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, fiservPreClaim];
1347
+ export const PSP_PRE_CLAIM_GATES = [cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim];
370
1348
  /** Run each gate in order; the first to return a decision wins. If none does,
371
1349
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
372
1350
  * Returns synchronously while every gate defers synchronously (an ordinary card