@agent-cards/checkout 0.18.0 → 0.19.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 (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/PREFLIGHT.md +4 -0
  3. package/README.md +91 -7
  4. package/dist/adyen-merchant-hosted.generated.d.ts +277 -0
  5. package/dist/adyen-merchant-hosted.generated.js +1902 -0
  6. package/dist/builtin-registry.generated.js +1 -1
  7. package/dist/cdp.d.ts +4 -1
  8. package/dist/cdp.js +416 -217
  9. package/dist/client.d.ts +236 -5
  10. package/dist/client.js +514 -11
  11. package/dist/cse-body.d.ts +25 -0
  12. package/dist/cse-body.js +41 -0
  13. package/dist/fiserv.d.ts +65 -0
  14. package/dist/fiserv.generated.d.ts +73 -0
  15. package/dist/fiserv.generated.js +830 -0
  16. package/dist/fiserv.js +104 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +5 -1
  19. package/dist/lifecycle.d.ts +15 -1
  20. package/dist/lifecycle.js +28 -3
  21. package/dist/merchant-handoff.d.ts +54 -0
  22. package/dist/merchant-handoff.js +100 -0
  23. package/dist/merchant-hosted.d.ts +140 -0
  24. package/dist/merchant-hosted.js +170 -0
  25. package/dist/merchant-total-watch.d.ts +115 -0
  26. package/dist/merchant-total-watch.js +268 -0
  27. package/dist/merchant-total.d.ts +257 -0
  28. package/dist/merchant-total.js +383 -0
  29. package/dist/pre-claim.d.ts +123 -0
  30. package/dist/pre-claim.js +386 -0
  31. package/dist/preflight-catalog.json +132 -0
  32. package/dist/preflight-schemas.json +14 -2
  33. package/dist/preflight.generated.js +15 -1
  34. package/dist/preparation.d.ts +7 -0
  35. package/dist/preparation.js +33 -6
  36. package/dist/prepared-processor.d.ts +36 -3
  37. package/dist/prepared-processor.js +53 -3
  38. package/dist/registry.d.ts +45 -0
  39. package/dist/registry.js +14 -0
  40. package/dist/stripe-checkout.generated.js +96 -7
  41. package/dist/substitutions.generated.d.ts +2 -1
  42. package/dist/substitutions.generated.js +758 -6
  43. package/examples/preflight/kernel-native/inventory.json +1 -1
  44. package/package.json +3 -3
@@ -463,6 +463,9 @@
463
463
  "dlocal-smart-card-fields",
464
464
  "mercado-pago-secure-fields-frame",
465
465
  "mercado-pago-checkout-pro-card-form",
466
+ "fiserv-commercehub-payment-fields",
467
+ "fiserv-commercehub-checkout-script",
468
+ "fiserv-secure-data-capture-frame",
466
469
  "square-web-sdk-sandbox",
467
470
  "paysafe-js-test",
468
471
  "worldpay-checkout-sdk-try",
@@ -476,7 +479,10 @@
476
479
  "adyen-secured-fields-live-au",
477
480
  "adyen-secured-fields-live-apse",
478
481
  "adyen-secured-fields-live-in",
479
- "adyen-secured-fields-live-nea"
482
+ "adyen-secured-fields-live-nea",
483
+ "fiserv-commercehub-payment-fields-cert",
484
+ "fiserv-commercehub-checkout-script-cert",
485
+ "fiserv-secure-data-capture-frame-cert"
480
486
  ]
481
487
  },
482
488
  "kind": {
@@ -983,6 +989,9 @@
983
989
  "dlocal-smart-card-fields",
984
990
  "mercado-pago-secure-fields-frame",
985
991
  "mercado-pago-checkout-pro-card-form",
992
+ "fiserv-commercehub-payment-fields",
993
+ "fiserv-commercehub-checkout-script",
994
+ "fiserv-secure-data-capture-frame",
986
995
  "square-web-sdk-sandbox",
987
996
  "paysafe-js-test",
988
997
  "worldpay-checkout-sdk-try",
@@ -996,7 +1005,10 @@
996
1005
  "adyen-secured-fields-live-au",
997
1006
  "adyen-secured-fields-live-apse",
998
1007
  "adyen-secured-fields-live-in",
999
- "adyen-secured-fields-live-nea"
1008
+ "adyen-secured-fields-live-nea",
1009
+ "fiserv-commercehub-payment-fields-cert",
1010
+ "fiserv-commercehub-checkout-script-cert",
1011
+ "fiserv-secure-data-capture-frame-cert"
1000
1012
  ]
1001
1013
  },
1002
1014
  "kind": {
@@ -1,5 +1,5 @@
1
1
  // Generated from @agent-cards/payment-core. Do not edit.
2
- // source-sha256: 47959768c673f160be4b35fd7cd406aebce431e666963f46a2c9c95125906456
2
+ // source-sha256: 7c76900599ba4ac29a8d1ca541170591ddae58d35c78560e5d37562e0050534a
3
3
  import { CHECKOUT_PREFLIGHT_CAPABILITIES } from './preflight-capabilities.generated.js';
4
4
  /** Advisory page discovery. These rules never admit a payment destination. */
5
5
  export const CHECKOUT_PREFLIGHT_VERSION = '1';
@@ -49,6 +49,16 @@ RULES.push({ id: 'tranzila-terminal-hosted-page', psp: 'tranzila', kinds: ['docu
49
49
  sources: [documented('https://docs.dlocal.com/docs/set-up-smart-fields'), observed('dlocal-smart-fields-6.8.3')] }, { id: 'mercado-pago-secure-fields-frame', psp: 'mercado_pago', kinds: ['frame', 'iframe', 'document'], hostname: /^secure-fields\.mercadopago\.com$/, pathname: /^\/$/, origin: 'https://secure-fields.mercadopago.com', path_pattern: '/',
50
50
  sources: [documented('https://sdk.mercadopago.com/js/v2'), observed('mercado-pago-secure-fields-frames')] }, { id: 'mercado-pago-checkout-pro-card-form', psp: 'mercado_pago', kinds: ['document', 'frame', 'iframe'], hostname: /^www\.mercadopago\.com\.ar$/, pathname: /^\/checkout\/v1\/payment\/redirect\/[^/]+\/card-form\/?$/, origin: 'https://www.mercadopago.com.ar', path_pattern: '/checkout/v1/payment/redirect/:preference/card-form/',
51
51
  sources: [documented('https://www.mercadopago.com.ar/developers/es/docs/checkout-pro/landing'), observed('mercado-pago-checkout-pro-guest-card-form')] });
52
+ // Fiserv Commerce Hub, read 2026-09-23 from Home Depot's OrangePay frame and Fiserv's
53
+ // docs: payment-fields.js renders one Secure Data Capture iframe per card field, and
54
+ // the Checkout JS SDK is Commerce Hub's other browser entry. Fiserv ships dark (its
55
+ // definition names a capability), so the bundled catalog declares no support for it:
56
+ // these assets identify fiserv and the result stays unknown.
57
+ const fiservSource = (reference) => ({ kind: 'primary_source', reference, reviewed_on: '2026-09-23' });
58
+ RULES.push({ id: 'fiserv-commercehub-payment-fields', psp: 'fiserv', kinds: ['script'], hostname: /^commercehub-checkout\.fiservapps\.com$/, pathname: /^\/sdk\/[0-9]+\.[0-9]+\.[0-9]+\/payment-fields\.js$/, origin: 'https://commercehub-checkout.fiservapps.com', path_pattern: '/sdk/:version/payment-fields.js',
59
+ sources: [fiservSource('https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js'), fiservSource('https://orangepay-ecommerce.hdpayments.homedepot.com/env.js')] }, { id: 'fiserv-commercehub-checkout-script', psp: 'fiserv', kinds: ['script'], hostname: /^commercehub-checkout\.fiservapps\.com$/, pathname: /^\/sdk\/[0-9]+\.[0-9]+\.[0-9]+\/checkout\.js$/, origin: 'https://commercehub-checkout.fiservapps.com', path_pattern: '/sdk/:version/checkout.js',
60
+ sources: [fiservSource('https://developer.fiserv.com/product/CommerceHub/docs/Online-Commerce/Integration-Options/Hosted-Checkout/Hosted-Checkout-Version-Release')] }, { id: 'fiserv-secure-data-capture-frame', psp: 'fiserv', kinds: ['frame', 'iframe', 'document'], hostname: /^commercehub-secure-data-capture\.fiservapps\.com$/, pathname: /^\/sdk-secure\/[0-9]+\.[0-9]+\.[0-9]+\/iframe\.html$/, origin: 'https://commercehub-secure-data-capture.fiservapps.com', path_pattern: '/sdk-secure/:version/iframe.html',
61
+ sources: [fiservSource('https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js'), fiservSource('https://commercehub-secure-data-capture.fiservapps.com/sdk-secure/3.8.14/iframe.html')] });
52
62
  // Keep sandbox evidence truthful instead of labelling it with a production origin.
53
63
  for (const [baseId, suffix, hostname, origin] of [
54
64
  ['square-web-sdk', 'sandbox', /^sandbox\.web\.squarecdn\.com$/, 'https://sandbox.web.squarecdn.com'],
@@ -66,6 +76,10 @@ for (const [baseId, suffix, hostname, origin] of [
66
76
  ['adyen-secured-fields-live', 'apse', /^checkoutshopper-live-apse\.adyen\.com$/, 'https://checkoutshopper-live-apse.adyen.com'],
67
77
  ['adyen-secured-fields-live', 'in', /^checkoutshopper-live-in\.adyen\.com$/, 'https://checkoutshopper-live-in.adyen.com'],
68
78
  ['adyen-secured-fields-live', 'nea', /^checkoutshopper-live-nea\.adyen\.com$/, 'https://checkoutshopper-live-nea.adyen.com'],
79
+ // Commerce Hub's "Sandbox and Cert" environment (payment-fields.js 3.8.14 environment table).
80
+ ['fiserv-commercehub-payment-fields', 'cert', /^commercehub-checkout-cert\.fiservapps\.com$/, 'https://commercehub-checkout-cert.fiservapps.com'],
81
+ ['fiserv-commercehub-checkout-script', 'cert', /^commercehub-checkout-cert\.fiservapps\.com$/, 'https://commercehub-checkout-cert.fiservapps.com'],
82
+ ['fiserv-secure-data-capture-frame', 'cert', /^commercehub-secure-data-capture-cert\.fiservapps\.com$/, 'https://commercehub-secure-data-capture-cert.fiservapps.com'],
69
83
  ]) {
70
84
  RULES.push({ ...RULES.find(rule => rule.id === baseId), id: `${baseId}-${suffix}`, hostname, origin });
71
85
  }
@@ -19,6 +19,13 @@ export declare class PreparationGate {
19
19
  assertDocument(): Promise<void>;
20
20
  private readDocument;
21
21
  isEngaged(): boolean;
22
+ /**
23
+ * A card request that only a prepared checkout may pay (an Adyen merchant
24
+ * profile's, or a Fiserv card capture, pre-claim.ts) arrived with no
25
+ * preparation and was refused. Like claim() on an unused gate, a later
26
+ * prepare() is then refused too.
27
+ */
28
+ observeUnprepared(): void;
22
29
  retireUnboundClaim(): void;
23
30
  /** A bound native request has its own cancellation/unknown-outcome machinery. */
24
31
  invalidate(reason: string): void;
@@ -1,6 +1,6 @@
1
1
  import { CheckoutPreparationError } from './client.js';
2
2
  import { validAmountInput } from './client.js';
3
- import { matchesPreparedRequest, validPreparationEnvironment, preparationEndpoint } from './prepared-processor.js';
3
+ import { matchesPreparation, validPreparationEnvironment, preparationEndpoint } from './prepared-processor.js';
4
4
  /** A local, one-use rendezvous. It never starts or retries a merchant request. */
5
5
  export class PreparationGate {
6
6
  opts;
@@ -39,9 +39,25 @@ export class PreparationGate {
39
39
  try {
40
40
  if (!options || !validPreparationEnvironment(options.psp, options.environment))
41
41
  throw new CheckoutPreparationError(null, 'unsupported_processor');
42
- const tokenizer = preparationEndpoint(options.psp, options.environment);
43
- if (!this.opts.vault.isCardRequest(tokenizer, 'POST'))
44
- throw new CheckoutPreparationError(null, 'processor_interception_unavailable');
42
+ // A Fiserv checkout's merchantProfile names a key pin, which prepareCheckout checks: its
43
+ // capture endpoint is checked below like any processor-hosted checkout's.
44
+ if (options.merchantProfile !== undefined && options.psp !== 'fiserv') {
45
+ // An Adyen merchant's own endpoint: a profile this build reviewed, which
46
+ // this attachment pauses, and one Agentcard turned on for this client.
47
+ const profile = typeof this.opts.vault.merchantProfile === 'function' && typeof options.merchantProfile === 'string'
48
+ ? this.opts.vault.merchantProfile(options.merchantProfile) : null;
49
+ if (options.psp !== 'adyen')
50
+ throw new CheckoutPreparationError(null, 'unsupported_processor');
51
+ if (!profile)
52
+ throw new CheckoutPreparationError(null, 'processor_interception_unavailable');
53
+ if (profile.status !== 'enabled')
54
+ throw new CheckoutPreparationError(null, 'unsupported_checkout');
55
+ }
56
+ else {
57
+ const tokenizer = preparationEndpoint(options.psp, options.environment);
58
+ if (!this.opts.vault.isCardRequest(tokenizer, 'POST'))
59
+ throw new CheckoutPreparationError(null, 'processor_interception_unavailable');
60
+ }
45
61
  if (!validAmountInput(this.opts.amount) || this.opts.amount === 0 || !/^[a-z]{3}$/i.test(this.opts.currency ?? ''))
46
62
  throw new CheckoutPreparationError(null, 'amount_required');
47
63
  if (signal.aborted)
@@ -66,7 +82,9 @@ export class PreparationGate {
66
82
  },
67
83
  });
68
84
  this.prepared = prepared;
69
- if (prepared.psp !== options.psp || prepared.environment !== options.environment)
85
+ // A Fiserv preparation also names the key pin the cardholder approves (merchantProfile).
86
+ if (prepared.psp !== options.psp || prepared.environment !== options.environment
87
+ || prepared.merchantProfile !== options.merchantProfile)
70
88
  throw new CheckoutPreparationError(prepared.id, 'checkout_changed');
71
89
  if (signal.aborted || this.state !== 'preparing') {
72
90
  void this.cancelThenObserve(prepared.id, 'cancelled');
@@ -101,7 +119,9 @@ export class PreparationGate {
101
119
  throw new CheckoutPreparationError(this.prepared?.id ?? null, 'already_used_or_unavailable');
102
120
  }
103
121
  const prepared = this.prepared;
104
- if (Date.parse(prepared.expiresAt) <= Date.now() || !matchesPreparedRequest(prepared.psp, prepared.environment, requestUrl, 'POST', requestBody, requestHeaders)) {
122
+ // A merchant-hosted preparation claims only its profile's reviewed card request.
123
+ // A Fiserv preparation claims only a capture under the key pin it names.
124
+ if (Date.parse(prepared.expiresAt) <= Date.now() || !matchesPreparation(prepared, requestUrl, 'POST', requestBody, requestHeaders)) {
105
125
  const reason = Date.parse(prepared.expiresAt) <= Date.now() ? 'expired' : 'checkout_changed';
106
126
  this.invalidate(reason);
107
127
  throw new CheckoutPreparationError(prepared.id, reason);
@@ -132,6 +152,13 @@ export class PreparationGate {
132
152
  }
133
153
  }
134
154
  isEngaged() { return this.state !== 'unused'; }
155
+ /**
156
+ * A card request that only a prepared checkout may pay (an Adyen merchant
157
+ * profile's, or a Fiserv card capture, pre-claim.ts) arrived with no
158
+ * preparation and was refused. Like claim() on an unused gate, a later
159
+ * prepare() is then refused too.
160
+ */
161
+ observeUnprepared() { this.observedRequest = true; }
135
162
  retireUnboundClaim() {
136
163
  if (this.state !== 'consumed' || !this.prepared || this.lifecycle.getState().authorizationId)
137
164
  return;
@@ -1,10 +1,43 @@
1
- export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe';
1
+ export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv';
2
2
  export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
3
- /** How the approved card reaches the processor: the device's own request (token), or ciphertext the device produces for this browser to send (cse, Adyen). */
3
+ /**
4
+ * How the approved card reaches the processor: the device's own request (token), or
5
+ * ciphertext the device produces for this browser to send (cse: Adyen's four encrypted
6
+ * fields, or Fiserv's Secure Data Capture envelope under a key Agentcard pinned).
7
+ */
4
8
  export type PreparationMode = 'token' | 'cse';
5
9
  export declare function preparationMode(psp: string): PreparationMode;
6
- /** Shared endpoint processors cannot attest test/live mode from their URL or key prefix. Adyen can: test and live are separate hosts and key prefixes. */
10
+ /**
11
+ * Shared endpoint processors cannot attest test/live mode from their URL or key prefix.
12
+ * Adyen can: test and live are separate hosts and key prefixes. Fiserv's are separate
13
+ * hosts too: connect (production) and connect-cert (sandbox).
14
+ */
7
15
  export declare function validPreparationEnvironment(psp: string, environment: string): boolean;
8
16
  export declare function preparationEndpoint(psp: PreparationProcessor, environment: PreparationEnvironment): string;
9
17
  /** Processor identity and environment are part of the device's prior consent. */
10
18
  export declare function matchesPreparedRequest(psp: PreparationProcessor, environment: PreparationEnvironment, requestUrl: string, method: string, body?: string | null, headers?: Record<string, string>): boolean;
19
+ /**
20
+ * The one request a merchant-hosted preparation pays: the reviewed profile's
21
+ * card request itself (classifyMerchantRequest pauses it), at the profile's exact
22
+ * endpoint, as its method, with a body the profile reviewed. A sibling endpoint, a
23
+ * stored card, a store-the-card body or any other shape never consumes consent.
24
+ */
25
+ export declare function matchesPreparedMerchantRequest(profileId: string, requestUrl: string, method: string, body?: string | null, sandboxDeclaration?: {
26
+ endpoint: string;
27
+ }): boolean;
28
+ /**
29
+ * matchesPreparedRequest for a preparation, through its merchant profile (at its declared
30
+ * endpoint, when it names one) when it names one. A Fiserv preparation also names the key its
31
+ * cardholder approved a payment under (merchantProfile, a pin id): the capture's own pin,
32
+ * the one its envelope's key is pinned as for its endpoint and merchant, must be that
33
+ * pin, as the API's bind requires. Two pins in one environment (a merchant's old and new
34
+ * keys during a rotation) never spend each other's approval. No other processor names one.
35
+ */
36
+ export declare function matchesPreparation(preparation: {
37
+ psp: PreparationProcessor;
38
+ environment: PreparationEnvironment;
39
+ merchantProfile?: string;
40
+ sandboxDeclaration?: {
41
+ endpoint: string;
42
+ };
43
+ }, requestUrl: string, method: string, body?: string | null, headers?: Record<string, string>): boolean;
@@ -4,20 +4,28 @@ import { braintreeEnvironment, isPreparedBraintreeRequest, readTokenizationJson
4
4
  import { isPreparedRecurlyRequest } from './recurly.generated.js';
5
5
  import { isPreparedSpreedlyRequest, isSpreedlyTokenRequest, SPREEDLY_TOKEN_ENDPOINT } from './spreedly.generated.js';
6
6
  import { checkoutComPreparationUrl, isPreparedCheckoutComRequest } from './checkout-com.generated.js';
7
+ import { classifyDeclaredMerchantRequest, classifyMerchantRequest } from './merchant-hosted.js';
8
+ import { fiservCapturePinId, fiservPreparationUrl, isPreparedFiservRequest } from './fiserv.js';
7
9
  export function preparationMode(psp) {
8
- return psp === 'adyen' ? 'cse' : 'token';
10
+ return psp === 'adyen' || psp === 'fiserv' ? 'cse' : 'token';
9
11
  }
10
- /** Shared endpoint processors cannot attest test/live mode from their URL or key prefix. Adyen can: test and live are separate hosts and key prefixes. */
12
+ /**
13
+ * Shared endpoint processors cannot attest test/live mode from their URL or key prefix.
14
+ * Adyen can: test and live are separate hosts and key prefixes. Fiserv's are separate
15
+ * hosts too: connect (production) and connect-cert (sandbox).
16
+ */
11
17
  export function validPreparationEnvironment(psp, environment) {
12
18
  if (psp === 'bambora' || psp === 'mercado_pago' || psp === 'recurly' || psp === 'spreedly')
13
19
  return environment === 'shared';
14
- return ['square', 'braintree', 'worldpay', 'adyen', 'checkout_com', 'paysafe'].includes(psp) && ['production', 'sandbox'].includes(environment);
20
+ return ['square', 'braintree', 'worldpay', 'adyen', 'checkout_com', 'paysafe', 'fiserv'].includes(psp) && ['production', 'sandbox'].includes(environment);
15
21
  }
16
22
  export function preparationEndpoint(psp, environment) {
17
23
  if (!validPreparationEnvironment(psp, environment))
18
24
  throw new Error('unsupported_preparation_processor');
19
25
  if (psp === 'paysafe')
20
26
  return paysafePreparationUrl(environment);
27
+ if (psp === 'fiserv')
28
+ return fiservPreparationUrl(environment);
21
29
  if (psp === 'checkout_com')
22
30
  return checkoutComPreparationUrl(environment);
23
31
  if (psp === 'adyen')
@@ -84,6 +92,11 @@ export function matchesPreparedRequest(psp, environment, requestUrl, method, bod
84
92
  return false;
85
93
  if (psp === 'paysafe')
86
94
  return isPreparedPaysafeRequest(requestUrl, method, body ?? null, environment, headers);
95
+ // Fiserv: that environment's card-capture endpoint exactly, the capture rules on the whole
96
+ // request (missing headers refuse), and an envelope under a key pinned for that environment
97
+ // and for the merchant the body names (payment-core isPreparedFiservRequest).
98
+ if (psp === 'fiserv')
99
+ return isPreparedFiservRequest(requestUrl, method.toUpperCase(), body ?? null, environment, headers);
87
100
  if (psp === 'checkout_com')
88
101
  return isPreparedCheckoutComRequest(requestUrl, method, body ?? null, environment, headers);
89
102
  if (psp === 'adyen')
@@ -120,3 +133,40 @@ export function matchesPreparedRequest(psp, environment, requestUrl, method, bod
120
133
  return false;
121
134
  }
122
135
  }
136
+ /**
137
+ * The one request a merchant-hosted preparation pays: the reviewed profile's
138
+ * card request itself (classifyMerchantRequest pauses it), at the profile's exact
139
+ * endpoint, as its method, with a body the profile reviewed. A sibling endpoint, a
140
+ * stored card, a store-the-card body or any other shape never consumes consent.
141
+ */
142
+ export function matchesPreparedMerchantRequest(profileId, requestUrl, method, body, sandboxDeclaration) {
143
+ try {
144
+ const verdict = sandboxDeclaration
145
+ ? classifyDeclaredMerchantRequest(profileId, sandboxDeclaration.endpoint, requestUrl, method, body ?? '')
146
+ : classifyMerchantRequest(profileId, requestUrl, method, body ?? '');
147
+ return verdict.verdict === 'pause';
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ /**
154
+ * matchesPreparedRequest for a preparation, through its merchant profile (at its declared
155
+ * endpoint, when it names one) when it names one. A Fiserv preparation also names the key its
156
+ * cardholder approved a payment under (merchantProfile, a pin id): the capture's own pin,
157
+ * the one its envelope's key is pinned as for its endpoint and merchant, must be that
158
+ * pin, as the API's bind requires. Two pins in one environment (a merchant's old and new
159
+ * keys during a rotation) never spend each other's approval. No other processor names one.
160
+ */
161
+ export function matchesPreparation(preparation, requestUrl, method, body, headers) {
162
+ if (preparation.psp === 'fiserv') {
163
+ if (!matchesPreparedRequest(preparation.psp, preparation.environment, requestUrl, method, body, headers))
164
+ return false;
165
+ return typeof preparation.merchantProfile === 'string' && fiservCapturePinId(requestUrl, body ?? null) === preparation.merchantProfile;
166
+ }
167
+ if (preparation.merchantProfile !== undefined) {
168
+ return preparation.psp === 'adyen'
169
+ && matchesPreparedMerchantRequest(preparation.merchantProfile, requestUrl, method, body, preparation.sandboxDeclaration);
170
+ }
171
+ return matchesPreparedRequest(preparation.psp, preparation.environment, requestUrl, method, body, headers);
172
+ }
@@ -64,10 +64,55 @@ export interface Recognizer {
64
64
  clientSideEncrypted?: boolean;
65
65
  /** How the card reaches the processor; absent means 'token'. Carried verbatim by syncRegistry. */
66
66
  mode?: CheckoutMode;
67
+ /**
68
+ * Dark launch: a capability name the API attaches to an entry it serves only
69
+ * to a client that declared it (?capabilities=). Absent on every entry in
70
+ * BUILTIN_REGISTRY, because a gated entry is left out of the built-in
71
+ * fallback. Carried verbatim by syncRegistry for a client that does declare
72
+ * it, so the SDK never has to act on it.
73
+ */
74
+ capability?: string;
75
+ /**
76
+ * True when this processor cannot be taken over on an ordinary authorization:
77
+ * the cardholder must have approved a prepare() first. authorize() refuses
78
+ * such a request locally, before it prompts, when no preparation is active
79
+ * (PreparationRequiredError), and the API answers 409 preparation_required.
80
+ * Absent on every BUILTIN_REGISTRY entry today and carried verbatim by
81
+ * syncRegistry for a build that reaches a processor which sets it.
82
+ */
83
+ preparationRequired?: boolean;
84
+ /**
85
+ * Anchored regex SOURCES (a subset of `hosts`) marking the vendor's
86
+ * SANDBOX/TEST hostnames, so the backend and the device can tell a test host
87
+ * from a live one. Carried verbatim by syncRegistry; the SDK never matches
88
+ * with them (recognition still uses `hosts`), so it acts on neither this nor
89
+ * the pin below. Present in BUILTIN_REGISTRY for the host-separated processors.
90
+ */
91
+ sandboxHosts?: string[];
92
+ /**
93
+ * True when a processor's host environment is enforced: the API refuses a
94
+ * create whose host environment disagrees with the row's test/live nature, and
95
+ * the device sends only documented test cards to a sandbox host. Absent on
96
+ * every entry today; carried verbatim by syncRegistry, and the SDK never acts
97
+ * on it.
98
+ */
99
+ hostEnvironmentPinned?: boolean;
67
100
  }
68
101
  import { BUILTIN_REGISTRY } from './builtin-registry.generated.js';
69
102
  export { BUILTIN_REGISTRY };
70
103
  export declare function findRecognizer(url: string, registry?: Recognizer[]): Recognizer | null;
104
+ /**
105
+ * True when this processor cannot be taken over on an ordinary authorization:
106
+ * the cardholder must have approved a prepare() first (the `preparationRequired`
107
+ * field above). authorize() applies this before it prompts and refuses with
108
+ * PreparationRequiredError when no preparation is active; the API answers 409
109
+ * preparation_required for the same case. This is the SAME rule payment-core's
110
+ * recognizerRequiresPreparation applies on the server, a strict `=== true`, so a
111
+ * value that rode in over the wire (syncRegistry spreads the served JSON) cannot
112
+ * turn a truthy string into a gate. Absent on every BUILTIN_REGISTRY entry, so
113
+ * this is false for all 24 today.
114
+ */
115
+ export declare function recognizerRequiresPreparation(rec: Pick<Recognizer, 'preparationRequired'>): boolean;
71
116
  /**
72
117
  * Glob url patterns covering every host in `registry` — what to hand
73
118
  * `Fetch.enable` so a card request is ever paused. Deliberately a superset of
package/dist/registry.js CHANGED
@@ -46,6 +46,20 @@ export function findRecognizer(url, registry = BUILTIN_REGISTRY) {
46
46
  return new RegExp(`^(?:${r.match.source})$`, 'i').test(hostPath);
47
47
  }) ?? null);
48
48
  }
49
+ /**
50
+ * True when this processor cannot be taken over on an ordinary authorization:
51
+ * the cardholder must have approved a prepare() first (the `preparationRequired`
52
+ * field above). authorize() applies this before it prompts and refuses with
53
+ * PreparationRequiredError when no preparation is active; the API answers 409
54
+ * preparation_required for the same case. This is the SAME rule payment-core's
55
+ * recognizerRequiresPreparation applies on the server, a strict `=== true`, so a
56
+ * value that rode in over the wire (syncRegistry spreads the served JSON) cannot
57
+ * turn a truthy string into a gate. Absent on every BUILTIN_REGISTRY entry, so
58
+ * this is false for all 24 today.
59
+ */
60
+ export function recognizerRequiresPreparation(rec) {
61
+ return rec.preparationRequired === true;
62
+ }
49
63
  // ---------------------------------------------------------------------------
50
64
  // Registry -> CDP url patterns
51
65
  //
@@ -1,7 +1,7 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 128bbf2359c38eda0d29a58c9b6f83123cf2d27d94365a5665e1569d605e9d2c
4
- // src/definitions.js
3
+ // artifact-sha256: 9edf78da59b3bdfe310b4422e06d333cd164497cdb4070db59ffed348492198a
4
+ // src/deep-freeze.js
5
5
  function deepFreeze(value) {
6
6
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
7
7
  for (const child of Object.values(value))
@@ -10,6 +10,7 @@ function deepFreeze(value) {
10
10
  }
11
11
  return value;
12
12
  }
13
+ // src/definitions.js
13
14
  var PROCESSOR_DEFINITIONS = deepFreeze([
14
15
  {
15
16
  psp: "spreedly",
@@ -69,6 +70,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
69
70
  psp: "braintree",
70
71
  match: "payments(\\.sandbox)?\\.braintree-api\\.com/graphql",
71
72
  hosts: ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"],
73
+ sandboxHosts: ["^payments\\.sandbox\\.braintree-api\\.com$"],
72
74
  encoding: "json",
73
75
  passthroughHeaders: ["^authorization$", "^braintree-version$"]
74
76
  },
@@ -86,6 +88,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
86
88
  "^card-acquisition-gateway\\.checkout\\.com$",
87
89
  "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"
88
90
  ],
91
+ sandboxHosts: ["^api\\.sandbox\\.checkout\\.com$", "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$"],
89
92
  encoding: "json",
90
93
  passthroughHeaders: ["^authorization$"]
91
94
  },
@@ -114,6 +117,15 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
114
117
  // merchants on them were silently never paused.
115
118
  match: "(checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com|([a-z0-9-]+\\.)*adyenpayments\\.com)/checkoutshopper/v1/sessions/[A-Za-z0-9_-]+/payments",
116
119
  hosts: ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"],
120
+ // The sandbox marker matches ONLY the TEST hostname, a subset of the first
121
+ // host source (which matches checkoutshopper-test and every checkoutshopper-live
122
+ // regional host). checkoutshopper-live* and *.adyenpayments.com are live.
123
+ // Load-bearing for Adyen although Adyen does not pin its environment: the
124
+ // Adyen TEST platform rule (backend.js adyenLivePlatform) reads this marker
125
+ // on the server's authorization create and cardholder legs
126
+ // (services/checkout-cse-adyen.ts in the backend), on the device and in the
127
+ // enclave. Dropping or widening it moves which payments count as Adyen TEST.
128
+ sandboxHosts: ["^checkoutshopper-test\\.adyen\\.com$"],
117
129
  encoding: "json",
118
130
  passthroughHeaders: [],
119
131
  clientSideEncrypted: true,
@@ -183,6 +195,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
183
195
  // PAN@ card_data.number. POST /v2/card-nonce (URL always carries ?_=&version=).
184
196
  match: "pci-connect\\.squareup(?:sandbox)?\\.com/v2/card-nonce",
185
197
  hosts: ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"],
198
+ sandboxHosts: ["^pci-connect\\.squareupsandbox\\.com$"],
186
199
  encoding: "json",
187
200
  passthroughHeaders: []
188
201
  // auth = client_id in body; no auth header
@@ -203,6 +216,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
203
216
  // request types on the same URL — the body, not the URL, is what distinguishes them.
204
217
  match: "(api2|apitest)\\.authorize\\.net/xml/v1/request\\.api",
205
218
  hosts: ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"],
219
+ sandboxHosts: ["^apitest\\.authorize\\.net$"],
206
220
  encoding: "json",
207
221
  // JSON body; Content-Type is sometimes text/plain (preflight dodge), replayed as-is
208
222
  passthroughHeaders: []
@@ -213,6 +227,8 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
213
227
  // the two paths need DIFFERENT vendor media types; `application/json` → HTTP 406.
214
228
  match: "(try\\.)?access\\.worldpay\\.com/(sessions/card|verifiedTokens/sessions)",
215
229
  hosts: ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"],
230
+ sandboxHosts: ["^try\\.access\\.worldpay\\.com$"],
231
+ // try.* is Worldpay's try/test host
216
232
  encoding: "json",
217
233
  passthroughHeaders: ["^accept$"]
218
234
  },
@@ -229,6 +245,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
229
245
  // with no branch back to the covered path.
230
246
  match: "(secure|ppp-test)\\.safecharge\\.com/ppp/api/v1/(?:cardTokenization|clientPayment|websdk/initPaymentWithCardTokenization)\\.do",
231
247
  hosts: ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"],
248
+ sandboxHosts: ["^ppp-test\\.safecharge\\.com$"],
232
249
  encoding: "json",
233
250
  passthroughHeaders: []
234
251
  },
@@ -263,6 +280,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
263
280
  "^pci-api\\.airwallex\\.com$",
264
281
  "^pci-api\\.sandbox\\.airwallex\\.com$"
265
282
  ],
283
+ sandboxHosts: ["^checkout\\.sandbox\\.airwallex\\.com$", "^pci-api\\.sandbox\\.airwallex\\.com$"],
266
284
  encoding: "json",
267
285
  passthroughHeaders: ["^client-secret$", "^authorization$", "^x-auth-token$", "^x-on-behalf-of$", "^x-api-version$"]
268
286
  },
@@ -276,6 +294,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
276
294
  // Checkout Page (signature-authed, server-to-server, no card) — the POST gate is what splits them.
277
295
  match: "(api|sandboxapi)\\.rapyd\\.net/v1/(?:hosted/collect/card/[^/?#]+/payment_method|checkout/checkout_[A-Za-z0-9]+)(?:[?#]|$)",
278
296
  hosts: ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"],
297
+ sandboxHosts: ["^sandboxapi\\.rapyd\\.net$"],
279
298
  encoding: "json",
280
299
  passthroughHeaders: []
281
300
  // auth rides the opaque hosted-page / checkout token in the URL
@@ -287,6 +306,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
287
306
  // multi-tenant surface: `ppmcc` is dlocal-operated, not merchant-registrable.
288
307
  match: "ppmcc(-sandbox)?\\.dlocal\\.com/cvault/credit-card/temporal",
289
308
  hosts: ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"],
309
+ sandboxHosts: ["^ppmcc-sandbox\\.dlocal\\.com$"],
290
310
  encoding: "json",
291
311
  passthroughHeaders: ["^x-fields-api-key$", "^x-uow$", "^x-dlocal-infrav2$"]
292
312
  },
@@ -318,6 +338,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
318
338
  "^api\\.ebanxpay\\.com$",
319
339
  "^sandbox\\.ebanxpay\\.com$"
320
340
  ],
341
+ sandboxHosts: ["^sandbox\\.ebanx\\.com$", "^sandbox-local-latam\\.ebanx\\.com$", "^sandbox\\.ebanxpay\\.com$"],
321
342
  encoding: "json",
322
343
  passthroughHeaders: []
323
344
  },
@@ -342,6 +363,8 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
342
363
  // GPO Secure Forms. PAN@ card.number.
343
364
  match: "(secure\\.payu\\.com|(merch-prod|secure)\\.snd\\.payu\\.com)/api/front/tokens",
344
365
  hosts: ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"],
366
+ sandboxHosts: ["^(merch-prod|secure)\\.snd\\.payu\\.com$"],
367
+ // .snd. is PayU's sandbox
345
368
  encoding: "json",
346
369
  // Native checkout attaches a bearer to tokenization; preserve it on replay.
347
370
  passthroughHeaders: ["^authorization$"]
@@ -403,6 +426,7 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
403
426
  // card/tokenize uses a saved paymentToken, and 3DS-specific paths are unverified.
404
427
  match: "(?:api(\\.test)?\\.paysafe\\.com/(?:paymenthub/v1/singleusepaymenthandles|js/api/v1/tokenize)|hosted(\\.test)?\\.paysafe\\.com/checkout/api/v1/tokenize)(?![\\w/-])",
405
428
  hosts: ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"],
429
+ sandboxHosts: ["^api\\.test\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"],
406
430
  encoding: "json",
407
431
  passthroughHeaders: ["^authorization$", "^x-paysafe-credentials$", "^correlationid$"]
408
432
  },
@@ -446,6 +470,8 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
446
470
  "^gatewayt\\.moneris\\.com$",
447
471
  "^gatewaydev\\.moneris\\.com$"
448
472
  ],
473
+ // Live: www3, gateway. Test/QA/dev: esqa, gatewayqa, gatewayt, gatewaydev.
474
+ sandboxHosts: ["^esqa\\.moneris\\.com$", "^gatewayqa\\.moneris\\.com$", "^gatewayt\\.moneris\\.com$", "^gatewaydev\\.moneris\\.com$"],
449
475
  encoding: "form",
450
476
  passthroughHeaders: []
451
477
  },
@@ -486,8 +512,66 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
486
512
  "^apis\\.eu\\.globalpay\\.com$",
487
513
  "^apis\\.sandbox\\.eu\\.globalpay\\.com$"
488
514
  ],
515
+ // Heartland/Portico cert.* is the certification (sandbox) host; GP-API sandbox is apis.sandbox[.eu].
516
+ sandboxHosts: ["^cert\\.api2\\.heartlandportico\\.com$", "^apis\\.sandbox\\.globalpay\\.com$", "^apis\\.sandbox\\.eu\\.globalpay\\.com$"],
489
517
  encoding: "json",
490
518
  passthroughHeaders: ["^authorization$", "^x-gp-version$"]
519
+ },
520
+ // ── Encrypted on the device under a pinned key ────────────────────────────
521
+ {
522
+ psp: "fiserv",
523
+ // Fiserv Commerce Hub card capture from the Secure Data Capture (SDC) frame,
524
+ // the request Home Depot's OrangePay checkout sends (payment-fields.js and
525
+ // sdk-secure 3.8.14). The frame always encrypts: it seals the card with
526
+ // AES-256-GCM under a fresh key, wraps that key with RSA-OAEP-SHA256 under the
527
+ // public key the page passed it, and posts the envelope as
528
+ // source.encryptionData. It picks its endpoint by the page's auth type
529
+ // (iframe.js @85480): an AccessToken (Security Credentials) posts here, to
530
+ // card-capture; a Checkout Sessions JWT posts to /ch/checkouts/v1/payment-link
531
+ // instead. payment-link also carries the card, and it is left out on purpose:
532
+ // `match` names card-capture only, so a Sessions merchant's capture is never
533
+ // paused and fails as it does today. Fastlane (PaymentToken), ACH
534
+ // (PaymentCheck), gift and EBT captures share this URL, carry no Vault card,
535
+ // and the SDK continues them unpaused (fiserv.js isFiservPausableBody).
536
+ // The Wave 0 sandbox gate (2026-09-24, connect-cert, Fiserv simulator cards)
537
+ // settled the mode: card-capture refuses a plaintext source.card (400
538
+ // GATEWAY 302 "Missing or Invalid Encryption Data") and captures the frame's
539
+ // own envelope with no Origin, so the device builds that envelope under a
540
+ // key Agentcard pinned (fiserv-keys.js), the runtime writes its four members
541
+ // over the paused ones, and the capture continues from the agent's browser.
542
+ // The AccessToken the agent can read was refused on charges and tokens at
543
+ // Fiserv's edge in that sandbox. Every other Commerce Hub path is refused
544
+ // here: charges, tokens, security/keys, payment-link, and the non-production
545
+ // hosts (connect-dev, -qa, -uat, -test); resolveRecognizerForUrl also takes
546
+ // only the two endpoints written exactly (fiserv-environment.js). The
547
+ // envelope carries no timestamp or session binding, so it is approved by the
548
+ // cardholder every time: dark until an org is allowlisted, and autopilot
549
+ // refuses fiserv at all three gates.
550
+ capability: "fiserv_card_capture",
551
+ // A Fiserv card is sealed only for a capture the cardholder already approved
552
+ // through a prepare(): the backend refuses an unprepared create (409
553
+ // preparation_required), the internal buy loop never mints a Fiserv round,
554
+ // and the SDK refuses locally before it prompts.
555
+ preparationRequired: true,
556
+ match: "connect(?:-cert)?\\.fiservapis\\.com/ch/payments-vas/v1/card-capture(?![\\w/.-])",
557
+ hosts: ["^connect\\.fiservapis\\.com$", "^connect-cert\\.fiservapis\\.com$"],
558
+ sandboxHosts: ["^connect-cert\\.fiservapis\\.com$"],
559
+ // A live row reaches connect only and a test row connect-cert only, and
560
+ // connect-cert receives only documented test cards (device-replay.js).
561
+ hostEnvironmentPinned: true,
562
+ encoding: "json",
563
+ // The browser continues the capture with its own headers, so only the ones
564
+ // the backend checks against the body and the merchant are kept:
565
+ // Auth-Token-Type, the merchant and terminal ids, and the embedding frame's
566
+ // origin the SDC names (x-integration-origin, self-asserted). The
567
+ // AccessToken, the API key, Client-Request-Id and Timestamp are never stored.
568
+ passthroughHeaders: ["^auth-token-type$", "^x-integration-merchant-id$", "^x-integration-terminal-id$", "^x-integration-origin$"],
569
+ clientSideEncrypted: true,
570
+ mode: "cse",
571
+ cse: {
572
+ at: "source.encryptionData",
573
+ fields: ["keyId", "encryptedKey", "encryptionBlock", "aesIv"]
574
+ }
491
575
  }
492
576
  ]);
493
577
  var PSP_RECOGNIZERS = deepFreeze(PROCESSOR_DEFINITIONS.map(({ compose, encryptedCardFields, ...definition }) => definition));
@@ -497,17 +581,22 @@ var PSP_CLIENT_TABLE = deepFreeze(PROCESSOR_DEFINITIONS.map((definition) => ({
497
581
  hosts: definition.hosts.map((source) => new RegExp(source, "i")),
498
582
  encoding: definition.encoding,
499
583
  headers: definition.passthroughHeaders.map((source) => new RegExp(source, "i")),
584
+ ...definition.sandboxHosts ? { sandboxHosts: definition.sandboxHosts.map((source) => new RegExp(source, "i")) } : {},
585
+ ...definition.hostEnvironmentPinned ? { hostEnvironmentPinned: true } : {},
500
586
  ...definition.mode ? { mode: definition.mode } : {},
501
587
  ...definition.clientSideEncrypted ? { clientSideEncrypted: true } : {},
502
588
  ...definition.compose ? { compose: definition.compose } : {},
503
589
  ...definition.encryptedCardFields ? { cse: { fields: definition.encryptedCardFields } } : {},
504
590
  ...definition.hostedForm ? { form: definition.hostedForm.card } : {}
505
591
  })));
506
- var BUILTIN_REGISTRY = deepFreeze(PSP_RECOGNIZERS.map(({ cse, hostedForm, ...definition }) => ({
507
- ...definition,
508
- match: new RegExp(definition.match, "i"),
509
- passthroughHeaders: definition.passthroughHeaders.map((source) => new RegExp(source, "i"))
510
- })));
592
+ function buildBuiltinRegistry(recognizers) {
593
+ return recognizers.filter((definition) => !definition.capability).map(({ cse, hostedForm, ...definition }) => ({
594
+ ...definition,
595
+ match: new RegExp(definition.match, "i"),
596
+ passthroughHeaders: definition.passthroughHeaders.map((source) => new RegExp(source, "i"))
597
+ }));
598
+ }
599
+ var BUILTIN_REGISTRY = deepFreeze(buildBuiltinRegistry(PSP_RECOGNIZERS));
511
600
  var CHECKOUT_MODES = deepFreeze(["token", "cse", "hosted_form"]);
512
601
  function recognizerByPsp(psp) {
513
602
  return typeof psp === "string" ? PSP_CLIENT_TABLE.find((entry) => entry.psp === psp) ?? null : null;
@@ -7,4 +7,5 @@ declare var SubstitutionError: {
7
7
  };
8
8
  };
9
9
  declare function substituteEncryptedFields(body: any, sub: any): string;
10
- export { SubstitutionError, substituteEncryptedFields };
10
+ declare function substituteMerchantHosted(body: any, sub: any, opts: any): string;
11
+ export { SubstitutionError, substituteEncryptedFields, substituteMerchantHosted };