@agent-cards/checkout 0.17.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 (49) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/PREFLIGHT.md +4 -0
  3. package/README.md +106 -14
  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/card-fields.generated.d.ts +3 -0
  8. package/dist/card-fields.generated.js +46 -0
  9. package/dist/cdp.d.ts +6 -1
  10. package/dist/cdp.js +574 -220
  11. package/dist/client.d.ts +285 -5
  12. package/dist/client.js +590 -12
  13. package/dist/cse-body.d.ts +25 -0
  14. package/dist/cse-body.js +41 -0
  15. package/dist/fiserv.d.ts +65 -0
  16. package/dist/fiserv.generated.d.ts +73 -0
  17. package/dist/fiserv.generated.js +830 -0
  18. package/dist/fiserv.js +104 -0
  19. package/dist/index.d.ts +7 -2
  20. package/dist/index.js +5 -1
  21. package/dist/lifecycle.d.ts +38 -1
  22. package/dist/lifecycle.js +77 -5
  23. package/dist/merchant-handoff.d.ts +54 -0
  24. package/dist/merchant-handoff.js +100 -0
  25. package/dist/merchant-hosted.d.ts +140 -0
  26. package/dist/merchant-hosted.js +170 -0
  27. package/dist/merchant-total-watch.d.ts +115 -0
  28. package/dist/merchant-total-watch.js +268 -0
  29. package/dist/merchant-total.d.ts +257 -0
  30. package/dist/merchant-total.js +383 -0
  31. package/dist/pre-claim.d.ts +123 -0
  32. package/dist/pre-claim.js +386 -0
  33. package/dist/preflight-capabilities.generated.d.ts +1 -1
  34. package/dist/preflight-capabilities.generated.js +1 -1
  35. package/dist/preflight-catalog.json +133 -1
  36. package/dist/preflight-schemas.json +14 -2
  37. package/dist/preflight.generated.d.ts +1 -1
  38. package/dist/preflight.generated.js +15 -1
  39. package/dist/preparation.d.ts +13 -0
  40. package/dist/preparation.js +46 -9
  41. package/dist/prepared-processor.d.ts +36 -3
  42. package/dist/prepared-processor.js +53 -3
  43. package/dist/registry.d.ts +60 -0
  44. package/dist/registry.js +14 -0
  45. package/dist/stripe-checkout.generated.js +140 -20
  46. package/dist/substitutions.generated.d.ts +2 -1
  47. package/dist/substitutions.generated.js +758 -6
  48. package/examples/preflight/kernel-native/inventory.json +1 -1
  49. package/package.json +3 -3
@@ -2097,6 +2097,72 @@
2097
2097
  "hostname_pattern": "^www\\.mercadopago\\.com\\.ar$",
2098
2098
  "pathname_pattern": "^\\/checkout\\/v1\\/payment\\/redirect\\/[^/]+\\/card-form\\/?$"
2099
2099
  },
2100
+ {
2101
+ "id": "fiserv-commercehub-payment-fields",
2102
+ "psp": "fiserv",
2103
+ "kinds": [
2104
+ "script"
2105
+ ],
2106
+ "origin": "https://commercehub-checkout.fiservapps.com",
2107
+ "path_pattern": "/sdk/:version/payment-fields.js",
2108
+ "sources": [
2109
+ {
2110
+ "kind": "primary_source",
2111
+ "reference": "https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js",
2112
+ "reviewed_on": "2026-09-23"
2113
+ },
2114
+ {
2115
+ "kind": "primary_source",
2116
+ "reference": "https://orangepay-ecommerce.hdpayments.homedepot.com/env.js",
2117
+ "reviewed_on": "2026-09-23"
2118
+ }
2119
+ ],
2120
+ "hostname_pattern": "^commercehub-checkout\\.fiservapps\\.com$",
2121
+ "pathname_pattern": "^\\/sdk\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/payment-fields\\.js$"
2122
+ },
2123
+ {
2124
+ "id": "fiserv-commercehub-checkout-script",
2125
+ "psp": "fiserv",
2126
+ "kinds": [
2127
+ "script"
2128
+ ],
2129
+ "origin": "https://commercehub-checkout.fiservapps.com",
2130
+ "path_pattern": "/sdk/:version/checkout.js",
2131
+ "sources": [
2132
+ {
2133
+ "kind": "primary_source",
2134
+ "reference": "https://developer.fiserv.com/product/CommerceHub/docs/Online-Commerce/Integration-Options/Hosted-Checkout/Hosted-Checkout-Version-Release",
2135
+ "reviewed_on": "2026-09-23"
2136
+ }
2137
+ ],
2138
+ "hostname_pattern": "^commercehub-checkout\\.fiservapps\\.com$",
2139
+ "pathname_pattern": "^\\/sdk\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/checkout\\.js$"
2140
+ },
2141
+ {
2142
+ "id": "fiserv-secure-data-capture-frame",
2143
+ "psp": "fiserv",
2144
+ "kinds": [
2145
+ "frame",
2146
+ "iframe",
2147
+ "document"
2148
+ ],
2149
+ "origin": "https://commercehub-secure-data-capture.fiservapps.com",
2150
+ "path_pattern": "/sdk-secure/:version/iframe.html",
2151
+ "sources": [
2152
+ {
2153
+ "kind": "primary_source",
2154
+ "reference": "https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js",
2155
+ "reviewed_on": "2026-09-23"
2156
+ },
2157
+ {
2158
+ "kind": "primary_source",
2159
+ "reference": "https://commercehub-secure-data-capture.fiservapps.com/sdk-secure/3.8.14/iframe.html",
2160
+ "reviewed_on": "2026-09-23"
2161
+ }
2162
+ ],
2163
+ "hostname_pattern": "^commercehub-secure-data-capture\\.fiservapps\\.com$",
2164
+ "pathname_pattern": "^\\/sdk-secure\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/iframe\\.html$"
2165
+ },
2100
2166
  {
2101
2167
  "id": "square-web-sdk-sandbox",
2102
2168
  "psp": "square",
@@ -2454,6 +2520,72 @@
2454
2520
  ],
2455
2521
  "hostname_pattern": "^checkoutshopper-live-nea\\.adyen\\.com$",
2456
2522
  "pathname_pattern": "^\\/checkoutshopper\\/securedfields\\/[^/]+\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/securedFields\\.html$"
2523
+ },
2524
+ {
2525
+ "id": "fiserv-commercehub-payment-fields-cert",
2526
+ "psp": "fiserv",
2527
+ "kinds": [
2528
+ "script"
2529
+ ],
2530
+ "origin": "https://commercehub-checkout-cert.fiservapps.com",
2531
+ "path_pattern": "/sdk/:version/payment-fields.js",
2532
+ "sources": [
2533
+ {
2534
+ "kind": "primary_source",
2535
+ "reference": "https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js",
2536
+ "reviewed_on": "2026-09-23"
2537
+ },
2538
+ {
2539
+ "kind": "primary_source",
2540
+ "reference": "https://orangepay-ecommerce.hdpayments.homedepot.com/env.js",
2541
+ "reviewed_on": "2026-09-23"
2542
+ }
2543
+ ],
2544
+ "hostname_pattern": "^commercehub-checkout-cert\\.fiservapps\\.com$",
2545
+ "pathname_pattern": "^\\/sdk\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/payment-fields\\.js$"
2546
+ },
2547
+ {
2548
+ "id": "fiserv-commercehub-checkout-script-cert",
2549
+ "psp": "fiserv",
2550
+ "kinds": [
2551
+ "script"
2552
+ ],
2553
+ "origin": "https://commercehub-checkout-cert.fiservapps.com",
2554
+ "path_pattern": "/sdk/:version/checkout.js",
2555
+ "sources": [
2556
+ {
2557
+ "kind": "primary_source",
2558
+ "reference": "https://developer.fiserv.com/product/CommerceHub/docs/Online-Commerce/Integration-Options/Hosted-Checkout/Hosted-Checkout-Version-Release",
2559
+ "reviewed_on": "2026-09-23"
2560
+ }
2561
+ ],
2562
+ "hostname_pattern": "^commercehub-checkout-cert\\.fiservapps\\.com$",
2563
+ "pathname_pattern": "^\\/sdk\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/checkout\\.js$"
2564
+ },
2565
+ {
2566
+ "id": "fiserv-secure-data-capture-frame-cert",
2567
+ "psp": "fiserv",
2568
+ "kinds": [
2569
+ "frame",
2570
+ "iframe",
2571
+ "document"
2572
+ ],
2573
+ "origin": "https://commercehub-secure-data-capture-cert.fiservapps.com",
2574
+ "path_pattern": "/sdk-secure/:version/iframe.html",
2575
+ "sources": [
2576
+ {
2577
+ "kind": "primary_source",
2578
+ "reference": "https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js",
2579
+ "reviewed_on": "2026-09-23"
2580
+ },
2581
+ {
2582
+ "kind": "primary_source",
2583
+ "reference": "https://commercehub-secure-data-capture.fiservapps.com/sdk-secure/3.8.14/iframe.html",
2584
+ "reviewed_on": "2026-09-23"
2585
+ }
2586
+ ],
2587
+ "hostname_pattern": "^commercehub-secure-data-capture-cert\\.fiservapps\\.com$",
2588
+ "pathname_pattern": "^\\/sdk-secure\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/iframe\\.html$"
2457
2589
  }
2458
2590
  ],
2459
2591
  "processors": [
@@ -2640,7 +2772,7 @@
2640
2772
  },
2641
2773
  {
2642
2774
  "id": "stripe.tokenization",
2643
- "path_pattern": "^/v1/(?:payment_methods|tokens)$",
2775
+ "path_pattern": "^/v1/(?:payment_methods|tokens|sources|confirmation_tokens)$",
2644
2776
  "effect": "token_or_method_creation",
2645
2777
  "amount_binding": "none",
2646
2778
  "psp": "stripe",
@@ -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": {
@@ -271,7 +271,7 @@ export declare function getCheckoutPreflightCatalog(): {
271
271
  readonly requires: "reviewed_final_payment_and_continuation_evidence";
272
272
  }, {
273
273
  readonly id: "stripe.tokenization";
274
- readonly path_pattern: "^/v1/(?:payment_methods|tokens)$";
274
+ readonly path_pattern: "^/v1/(?:payment_methods|tokens|sources|confirmation_tokens)$";
275
275
  readonly effect: "token_or_method_creation";
276
276
  readonly amount_binding: "none";
277
277
  readonly psp: "stripe";
@@ -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,7 +19,20 @@ 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;
32
+ /**
33
+ * Retire the server handle first, then report the form outcome. Neither
34
+ * call waits on the other: a stalled report cannot delay the cancel, and a
35
+ * stalled cancel cannot lose the report.
36
+ */
37
+ private cancelThenObserve;
25
38
  }
@@ -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,10 +82,12 @@ 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
- void this.opts.vault.cancelPreparation(prepared.id).catch(() => { });
90
+ void this.cancelThenObserve(prepared.id, 'cancelled');
73
91
  throw new CheckoutPreparationError(prepared.id, 'cancelled');
74
92
  }
75
93
  await this.assertDocument();
@@ -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,10 +152,17 @@ 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;
138
- void this.opts.vault.cancelPreparation(this.prepared.id).catch(() => { });
165
+ void this.cancelThenObserve(this.prepared.id, 'request_not_bound');
139
166
  // No bound ID means the adapter must not leave a spent handle appearing ready.
140
167
  if (this.lifecycle.getState().status === 'ready_to_submit')
141
168
  this.lifecycle.preparationFailed(new CheckoutPreparationError(this.prepared.id, 'request_not_bound'));
@@ -148,7 +175,17 @@ export class PreparationGate {
148
175
  clearTimeout(this.expiryTimer);
149
176
  this.stop.abort();
150
177
  if (this.prepared)
151
- void this.opts.vault.cancelPreparation(this.prepared.id).catch(() => { });
178
+ void this.cancelThenObserve(this.prepared.id, reason);
152
179
  this.lifecycle.preparationFailed(new CheckoutPreparationError(this.prepared?.id ?? null, reason));
153
180
  }
181
+ /**
182
+ * Retire the server handle first, then report the form outcome. Neither
183
+ * call waits on the other: a stalled report cannot delay the cancel, and a
184
+ * stalled cancel cannot lose the report.
185
+ */
186
+ async cancelThenObserve(id, reason) {
187
+ const cancellation = this.opts.vault.cancelPreparation(id).catch(() => { });
188
+ const observation = this.opts.vault.observePreparation?.(id, this.observedRequest ? 'presented_not_filled' : 'not_presented', reason);
189
+ await Promise.all([cancellation, observation?.catch(() => { })]);
190
+ }
154
191
  }
@@ -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
+ }
@@ -40,6 +40,21 @@ export interface Recognizer {
40
40
  * the call (signatures, client tokens). Matched case-insensitively.
41
41
  */
42
42
  passthroughHeaders: RegExp[];
43
+ /**
44
+ * Form keys that carry the card number. When present, a request on this
45
+ * recognizer's endpoints is a card request only if its body carries one of
46
+ * them: a Stripe confirmation that pays with a method the page already
47
+ * created (`payment_method=pm_...`) carries none, so there is nothing for
48
+ * the cardholder to approve. Served by the API and carried through
49
+ * syncRegistry() verbatim.
50
+ */
51
+ cardFields?: string[];
52
+ /**
53
+ * With cardFields: the endpoints (regex source over hostname + pathname,
54
+ * anchored) where a request without a card is routine and never ours, so
55
+ * it continues untouched; a request without a card anywhere else is refused.
56
+ */
57
+ passWithoutCard?: string;
43
58
  /**
44
59
  * True when the card is encrypted inside the page before the request leaves.
45
60
  * A digit swap is useless here; the vault must re-run the PSP's client-side
@@ -49,10 +64,55 @@ export interface Recognizer {
49
64
  clientSideEncrypted?: boolean;
50
65
  /** How the card reaches the processor; absent means 'token'. Carried verbatim by syncRegistry. */
51
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;
52
100
  }
53
101
  import { BUILTIN_REGISTRY } from './builtin-registry.generated.js';
54
102
  export { BUILTIN_REGISTRY };
55
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;
56
116
  /**
57
117
  * Glob url patterns covering every host in `registry` — what to hand
58
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
  //