@agent-cards/checkout 0.12.0 → 0.14.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.
@@ -1,7 +1,9 @@
1
- import { normalizeCheckoutSignals, assessCheckoutSupport, } from './preflight.js';
1
+ import { normalizeCheckoutSignals, assessCheckoutSupport, cardEntryAttributable, } from './preflight.js';
2
2
  const EVENTS = ['framenavigated', 'frameattached', 'framedetached'];
3
3
  const DEADLINE = Symbol('checkout inspection deadline');
4
4
  const DEFAULTS = { timeoutMs: 1_500, maxFrames: 64, maxSignals: 2_048, maxSnapshotBytes: 262_144 };
5
+ /** Frames at one nesting depth are read together. A remote CDP connection pays per round trip, not per frame. */
6
+ const CONCURRENCY = 8;
5
7
  const encoder = new TextEncoder();
6
8
  function bounded(value, fallback, maximum, minimum = 1) {
7
9
  if (value === undefined)
@@ -27,6 +29,28 @@ async function beforeDeadline(operation, deadline) {
27
29
  clearTimeout(timer);
28
30
  }
29
31
  }
32
+ function httpOrigin(url) {
33
+ if (!url || !/^https?:/i.test(url))
34
+ return null;
35
+ try {
36
+ return new URL(url).origin;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** Run `worker` over `items` with bounded concurrency; results keep the input order. */
43
+ async function inBatches(items, size, worker) {
44
+ const results = new Array(items.length);
45
+ let next = 0;
46
+ await Promise.all(Array.from({ length: Math.min(size, items.length) }, async () => {
47
+ while (next < items.length) {
48
+ const index = next++;
49
+ results[index] = await worker(items[index]);
50
+ }
51
+ }));
52
+ return results;
53
+ }
30
54
  /** Read only frame, external asset and form locations; return reviewed rule IDs, never raw URLs. */
31
55
  export async function collectCheckoutSignals(page, options = {}) {
32
56
  const limits = {
@@ -41,6 +65,7 @@ export async function collectCheckoutSignals(page, options = {}) {
41
65
  const registered = [];
42
66
  let transientBytes = 0;
43
67
  let truncated = false;
68
+ let stopped = false;
44
69
  const changed = () => { reasons.add('navigation_changed'); };
45
70
  const limit = (reason) => { reasons.add(reason); truncated = true; };
46
71
  const append = (signal) => {
@@ -58,6 +83,24 @@ export async function collectCheckoutSignals(page, options = {}) {
58
83
  signals.push(signal);
59
84
  return true;
60
85
  };
86
+ const stop = () => {
87
+ if (stopped)
88
+ return true;
89
+ if (performance.now() >= deadline) {
90
+ reasons.add('collection_timeout');
91
+ stopped = true;
92
+ }
93
+ else if (signals.length >= limits.maxSignals) {
94
+ limit('signal_limit');
95
+ stopped = true;
96
+ }
97
+ else if (truncated && reasons.has('snapshot_limit'))
98
+ stopped = true;
99
+ return stopped;
100
+ };
101
+ // Hostnames of visible nested card-entry frames outside the top document's origin. They stay
102
+ // local to this call; only the attribution verdict reaches the observation.
103
+ const cardEntryHosts = new Set();
61
104
  try {
62
105
  for (const event of EVENTS) {
63
106
  page.on(event, changed);
@@ -73,29 +116,25 @@ export async function collectCheckoutSignals(page, options = {}) {
73
116
  const ids = new Map(frames.map((frame, index) => [frame, index]));
74
117
  if (frames.length > limits.maxFrames)
75
118
  limit('frame_limit');
119
+ const topOrigin = frames.length ? httpOrigin(initialUrls.get(frames[0])) : null;
76
120
  const visibility = new Map();
77
- const ordered = frames.slice(0, limits.maxFrames).sort((a, b) => {
78
- const depth = (frame) => {
79
- const visited = new Set();
80
- while (frame.parentFrame() && !visited.has(frame)) {
81
- visited.add(frame);
82
- frame = frame.parentFrame();
83
- }
84
- return visited.size;
85
- };
86
- return depth(a) - depth(b);
87
- });
88
- for (const frame of ordered) {
89
- if (performance.now() >= deadline) {
90
- reasons.add('collection_timeout');
91
- break;
92
- }
93
- if (signals.length >= limits.maxSignals) {
94
- limit('signal_limit');
95
- break;
121
+ const depth = (frame) => {
122
+ const visited = new Set();
123
+ while (frame.parentFrame() && !visited.has(frame)) {
124
+ visited.add(frame);
125
+ frame = frame.parentFrame();
96
126
  }
97
- if (truncated && reasons.has('snapshot_limit'))
98
- break;
127
+ return visited.size;
128
+ };
129
+ // Parents settle before their children so visibility can propagate; siblings share round trips.
130
+ const levels = new Map();
131
+ for (const frame of frames.slice(0, limits.maxFrames)) {
132
+ const level = depth(frame);
133
+ levels.set(level, [...(levels.get(level) ?? []), frame]);
134
+ }
135
+ const readFrame = async (frame) => {
136
+ if (stop())
137
+ return null;
99
138
  const parent = frame.parentFrame();
100
139
  const frame_id = ids.get(frame);
101
140
  const parent_frame_id = parent ? ids.get(parent) ?? null : null;
@@ -103,8 +142,9 @@ export async function collectCheckoutSignals(page, options = {}) {
103
142
  try {
104
143
  if (frame.isDetached()) {
105
144
  reasons.add('frame_unavailable');
106
- continue;
145
+ return null;
107
146
  }
147
+ let released;
108
148
  if (parent) {
109
149
  if (!ids.has(parent))
110
150
  reasons.add('frame_unavailable');
@@ -139,8 +179,9 @@ export async function collectCheckoutSignals(page, options = {}) {
139
179
  visible = visible === false || ownVisibility === false ? false : visible === null || ownVisibility === null ? null : true;
140
180
  }
141
181
  finally {
142
- // Disposal only releases our automation handle. A stalled disposal cannot extend the inspection deadline.
143
- await beforeDeadline(() => element.dispose(), deadline);
182
+ // Disposal only releases our automation handle and overlaps the document read below.
183
+ // A stalled disposal still cannot extend the inspection deadline.
184
+ released = beforeDeadline(() => element.dispose(), deadline).then(() => undefined, error => error);
144
185
  }
145
186
  }
146
187
  visibility.set(frame, visible);
@@ -149,6 +190,19 @@ export async function collectCheckoutSignals(page, options = {}) {
149
190
  let bytes = 0;
150
191
  let signalLimit = false;
151
192
  let byteLimit = false;
193
+ const shown = (element) => {
194
+ if (!element.isConnected)
195
+ return false;
196
+ for (let current = element; current;) {
197
+ const style = getComputedStyle(current);
198
+ if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || style.opacity === '0')
199
+ return false;
200
+ const root = current.getRootNode();
201
+ current = current.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
202
+ }
203
+ const rect = element.getBoundingClientRect();
204
+ return rect.width > 0 && rect.height > 0;
205
+ };
152
206
  const elements = document.querySelectorAll('script[src], iframe[src], form[action]');
153
207
  for (let index = 0; index < elements.length; index++) {
154
208
  if (index >= remaining) {
@@ -176,31 +230,63 @@ export async function collectCheckoutSignals(page, options = {}) {
176
230
  byteLimit = true;
177
231
  break;
178
232
  }
179
- let visible = null;
180
- if (kind !== 'script') {
181
- visible = element.isConnected;
182
- for (let current = element; current && visible;) {
183
- const style = getComputedStyle(current);
184
- if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || style.opacity === '0')
185
- visible = false;
186
- const root = current.getRootNode();
187
- current = current.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
188
- }
189
- const rect = element.getBoundingClientRect();
190
- visible = visible && rect.width > 0 && rect.height > 0;
191
- }
192
- collected.push({ kind, url, visible });
233
+ collected.push({ kind, url, visible: kind === 'script' ? null : shown(element) });
234
+ }
235
+ // Card-entry detection reads field metadata only: the autocomplete token or the
236
+ // control's name and id. Values, labels and page text are never read.
237
+ let cardEntry = false;
238
+ const inputs = document.querySelectorAll('input');
239
+ for (let index = 0; index < inputs.length && index < 256 && !cardEntry; index++) {
240
+ const input = inputs[index];
241
+ const type = (input.getAttribute('type') || 'text').toLowerCase();
242
+ if (['hidden', 'submit', 'button', 'checkbox', 'radio', 'image', 'reset', 'file', 'range', 'color'].includes(type))
243
+ continue;
244
+ const autocomplete = (input.getAttribute('autocomplete') || '').trim().toLowerCase();
245
+ const label = `${input.getAttribute('name') || ''} ${input.getAttribute('id') || ''}`.toLowerCase().replace(/[^a-z0-9]+/g, '');
246
+ const cardField = /^cc-(?:number|csc|exp|exp-month|exp-year)$/.test(autocomplete)
247
+ || !label.includes('gift') && /cardnumber|cardnum|cardno|ccnumber|ccnum|ccno|cvv|cvc|csc|securitycode|cardcode/.test(label);
248
+ if (cardField && shown(input))
249
+ cardEntry = true;
193
250
  }
194
251
  const documentUrl = document.URL;
195
252
  if (documentUrl.length * 3 > byteBudget)
196
253
  byteLimit = true;
197
- return { documentUrl: byteLimit && documentUrl.length * 3 > byteBudget ? '' : documentUrl, collected, signalLimit, byteLimit };
254
+ return { documentUrl: byteLimit && documentUrl.length * 3 > byteBudget ? '' : documentUrl, collected, signalLimit, byteLimit, cardEntry };
198
255
  }, { remaining: Math.max(0, limits.maxSignals - signals.length - 1), byteBudget: Math.max(0, limits.maxSnapshotBytes - transientBytes) }), deadline);
256
+ const releaseError = released ? await released : undefined;
257
+ if (releaseError !== undefined)
258
+ throw releaseError;
199
259
  if (frame.isDetached())
200
260
  reasons.add('frame_unavailable');
201
- if (data.documentUrl !== initialUrls.get(frame) || frame.url() !== initialUrls.get(frame))
261
+ // Committed URLs are compared with committed URLs. The document URL is compared only for a
262
+ // frame that committed an http(s) document: a script-created frame written through
263
+ // document.open() inherits the merchant URL without navigating anywhere.
264
+ const initial = initialUrls.get(frame) ?? '';
265
+ if (frame.url() !== initial || (/^https?:/i.test(initial) && data.documentUrl !== '' && data.documentUrl !== initial))
202
266
  reasons.add('navigation_changed');
203
- append({ kind: parent ? 'frame' : 'document', url: data.documentUrl, frame_id, parent_frame_id, visible });
267
+ return { frame_id, parent_frame_id, nested: parent !== null, visible, data };
268
+ }
269
+ catch (error) {
270
+ if (error === DEADLINE) {
271
+ reasons.add('collection_timeout');
272
+ stopped = true;
273
+ return null;
274
+ }
275
+ // Browser errors can include complete merchant URLs. Return a bounded reason instead.
276
+ reasons.add('frame_unavailable');
277
+ return null;
278
+ }
279
+ };
280
+ for (const level of [...levels.keys()].sort((a, b) => a - b)) {
281
+ if (stop())
282
+ break;
283
+ const outcomes = await inBatches(levels.get(level), CONCURRENCY, readFrame);
284
+ // Append in frame order so identical pages produce identical observations.
285
+ for (const outcome of outcomes) {
286
+ if (!outcome)
287
+ continue;
288
+ const { data, frame_id, parent_frame_id, nested, visible } = outcome;
289
+ append({ kind: nested ? 'frame' : 'document', url: data.documentUrl, frame_id, parent_frame_id, visible });
204
290
  for (const signal of data.collected) {
205
291
  if (!append({ ...signal, frame_id, parent_frame_id, visible: visible === false ? false : signal.kind === 'script' ? visible : visible === null ? null : signal.visible }))
206
292
  break;
@@ -209,14 +295,11 @@ export async function collectCheckoutSignals(page, options = {}) {
209
295
  limit('signal_limit');
210
296
  if (data.byteLimit)
211
297
  limit('snapshot_limit');
212
- }
213
- catch (error) {
214
- if (error === DEADLINE) {
215
- reasons.add('collection_timeout');
216
- break;
298
+ if (nested && visible === true && data.cardEntry === true) {
299
+ const origin = httpOrigin(data.documentUrl);
300
+ if (origin && origin !== topOrigin)
301
+ cardEntryHosts.add(new URL(origin).hostname);
217
302
  }
218
- // Browser errors can include complete merchant URLs. Return a bounded reason instead.
219
- reasons.add('frame_unavailable');
220
303
  }
221
304
  }
222
305
  if (performance.now() >= deadline)
@@ -239,7 +322,17 @@ export async function collectCheckoutSignals(page, options = {}) {
239
322
  }
240
323
  }
241
324
  }
242
- let snapshot = normalizeCheckoutSignals({ signals, observation: { complete: reasons.size === 0, truncated, reasons: [...reasons] } });
325
+ const observation = () => ({ complete: reasons.size === 0, truncated, reasons: [...reasons] });
326
+ let snapshot = normalizeCheckoutSignals({ signals, observation: observation() });
327
+ // Visible card controls on a site no evidenced processor owns mean the card form belongs to
328
+ // something the catalog has not reviewed. The observation is then incomplete, so a co-loaded
329
+ // SDK script stays a candidate instead of becoming the selected processor. A snapshot past the
330
+ // byte budget is already incomplete and is bounded below instead.
331
+ if (encoder.encode(JSON.stringify(snapshot)).length <= limits.maxSnapshotBytes
332
+ && [...cardEntryHosts].some(host => !cardEntryAttributable(snapshot, host))) {
333
+ reasons.add('card_entry_unrecognized');
334
+ snapshot = normalizeCheckoutSignals({ signals, observation: observation() });
335
+ }
243
336
  if (encoder.encode(JSON.stringify(snapshot)).length > limits.maxSnapshotBytes) {
244
337
  limit('snapshot_limit');
245
338
  const boundedSnapshot = normalizeCheckoutSignals({ signals: [], observation: { complete: false, truncated, reasons: [...reasons] } });
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "url": {
58
58
  "type": "string",
59
- "maxLength": 4096
59
+ "maxLength": 65536
60
60
  }
61
61
  }
62
62
  }
@@ -90,6 +90,7 @@
90
90
  "navigation_changed",
91
91
  "invalid_observation",
92
92
  "invalid_url",
93
+ "card_entry_unrecognized",
93
94
  "collection_incomplete"
94
95
  ]
95
96
  }
@@ -162,7 +163,7 @@
162
163
  "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]{0,119}$"
163
164
  },
164
165
  "catalog_version": {
165
- "const": "2026-09-14.1"
166
+ "const": "2026-09-15.1"
166
167
  },
167
168
  "integration": {
168
169
  "type": "object",
@@ -208,6 +209,7 @@
208
209
  "properties": {
209
210
  "psp": {
210
211
  "enum": [
212
+ "spreedly",
211
213
  "shopify",
212
214
  "stripe",
213
215
  "braintree",
@@ -358,7 +360,7 @@
358
360
  "const": 1
359
361
  },
360
362
  "catalog_version": {
361
- "const": "2026-09-14.1"
363
+ "const": "2026-09-15.1"
362
364
  },
363
365
  "signals": {
364
366
  "type": "array",
@@ -440,6 +442,8 @@
440
442
  "paysafe-js-v1-latest",
441
443
  "paysafe-js-v1-fields",
442
444
  "paysafe-js-v1-fields-host-2",
445
+ "spreedly-iframe-v1-script",
446
+ "spreedly-native-card-frame",
443
447
  "recurly-js-v4",
444
448
  "recurly-hosted-field-v1",
445
449
  "recurly-hosted-field-v1-host-2",
@@ -450,10 +454,29 @@
450
454
  "bambora-na-fields-1-0-5-39",
451
455
  "global-payments-js-4-1-3",
452
456
  "global-payments-js-v1-alias",
457
+ "tranzila-terminal-hosted-page",
458
+ "square-card-element-frame",
459
+ "authorize-net-sim-hosted-form",
460
+ "worldpay-hosted-payment-page",
461
+ "rapyd-hosted-checkout-page",
462
+ "dlocal-smart-fields-parent",
463
+ "dlocal-smart-card-fields",
464
+ "mercado-pago-secure-fields-frame",
465
+ "mercado-pago-checkout-pro-card-form",
453
466
  "square-web-sdk-sandbox",
454
467
  "paysafe-js-test",
455
468
  "worldpay-checkout-sdk-try",
456
- "authorize-net-accept-test"
469
+ "authorize-net-accept-test",
470
+ "tranzila-terminal-hosted-page-directng",
471
+ "square-card-element-frame-sandbox",
472
+ "authorize-net-sim-hosted-form-secure2",
473
+ "worldpay-hosted-payment-page-test",
474
+ "rapyd-hosted-checkout-page-sandbox",
475
+ "adyen-secured-fields-live-us",
476
+ "adyen-secured-fields-live-au",
477
+ "adyen-secured-fields-live-apse",
478
+ "adyen-secured-fields-live-in",
479
+ "adyen-secured-fields-live-nea"
457
480
  ]
458
481
  },
459
482
  "kind": {
@@ -520,6 +543,7 @@
520
543
  "navigation_changed",
521
544
  "invalid_observation",
522
545
  "invalid_url",
546
+ "card_entry_unrecognized",
523
547
  "collection_incomplete"
524
548
  ]
525
549
  }
@@ -550,7 +574,7 @@
550
574
  "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]{0,119}$"
551
575
  },
552
576
  "catalog_version": {
553
- "const": "2026-09-14.1"
577
+ "const": "2026-09-15.1"
554
578
  },
555
579
  "integration": {
556
580
  "type": "object",
@@ -596,6 +620,7 @@
596
620
  "properties": {
597
621
  "psp": {
598
622
  "enum": [
623
+ "spreedly",
599
624
  "shopify",
600
625
  "stripe",
601
626
  "braintree",
@@ -753,7 +778,7 @@
753
778
  "const": 1
754
779
  },
755
780
  "catalog_version": {
756
- "const": "2026-09-14.1"
781
+ "const": "2026-09-15.1"
757
782
  },
758
783
  "integration": {
759
784
  "type": "object",
@@ -937,6 +962,8 @@
937
962
  "paysafe-js-v1-latest",
938
963
  "paysafe-js-v1-fields",
939
964
  "paysafe-js-v1-fields-host-2",
965
+ "spreedly-iframe-v1-script",
966
+ "spreedly-native-card-frame",
940
967
  "recurly-js-v4",
941
968
  "recurly-hosted-field-v1",
942
969
  "recurly-hosted-field-v1-host-2",
@@ -947,10 +974,29 @@
947
974
  "bambora-na-fields-1-0-5-39",
948
975
  "global-payments-js-4-1-3",
949
976
  "global-payments-js-v1-alias",
977
+ "tranzila-terminal-hosted-page",
978
+ "square-card-element-frame",
979
+ "authorize-net-sim-hosted-form",
980
+ "worldpay-hosted-payment-page",
981
+ "rapyd-hosted-checkout-page",
982
+ "dlocal-smart-fields-parent",
983
+ "dlocal-smart-card-fields",
984
+ "mercado-pago-secure-fields-frame",
985
+ "mercado-pago-checkout-pro-card-form",
950
986
  "square-web-sdk-sandbox",
951
987
  "paysafe-js-test",
952
988
  "worldpay-checkout-sdk-try",
953
- "authorize-net-accept-test"
989
+ "authorize-net-accept-test",
990
+ "tranzila-terminal-hosted-page-directng",
991
+ "square-card-element-frame-sandbox",
992
+ "authorize-net-sim-hosted-form-secure2",
993
+ "worldpay-hosted-payment-page-test",
994
+ "rapyd-hosted-checkout-page-sandbox",
995
+ "adyen-secured-fields-live-us",
996
+ "adyen-secured-fields-live-au",
997
+ "adyen-secured-fields-live-apse",
998
+ "adyen-secured-fields-live-in",
999
+ "adyen-secured-fields-live-nea"
954
1000
  ]
955
1001
  },
956
1002
  "kind": {
@@ -1046,6 +1092,7 @@
1046
1092
  "navigation_changed",
1047
1093
  "invalid_observation",
1048
1094
  "invalid_url",
1095
+ "card_entry_unrecognized",
1049
1096
  "collection_incomplete"
1050
1097
  ]
1051
1098
  }
@@ -1,11 +1,12 @@
1
1
  /** Advisory page discovery. These rules never admit a payment destination. */
2
2
  export declare const CHECKOUT_PREFLIGHT_VERSION = "1";
3
- export declare const CHECKOUT_PREFLIGHT_CATALOG_VERSION = "2026-09-14.1";
3
+ export declare const CHECKOUT_PREFLIGHT_CATALOG_VERSION = "2026-09-15.1";
4
4
  export declare const CHECKOUT_PREFLIGHT_LIMITS: Readonly<{
5
5
  frames: 64;
6
6
  signals: 2048;
7
7
  snapshot_bytes: 262144;
8
8
  url_length: 4096;
9
+ raw_url_length: 65536;
9
10
  }>;
10
11
  export type CheckoutSignalKind = 'document' | 'frame' | 'script' | 'iframe' | 'form';
11
12
  export type CheckoutScenario = 'one_time' | 'save_card' | 'subscription_initial' | 'subscription_renewal';
@@ -126,6 +127,7 @@ export declare function getCheckoutPreflightCatalog(): {
126
127
  signals: 2048;
127
128
  snapshot_bytes: 262144;
128
129
  url_length: 4096;
130
+ raw_url_length: 65536;
129
131
  }>;
130
132
  known_psps: {
131
133
  psp: string;
@@ -148,6 +150,69 @@ export declare function getCheckoutPreflightCatalog(): {
148
150
  variant_unverified?: string;
149
151
  }[];
150
152
  processors: readonly [{
153
+ readonly psp: "spreedly";
154
+ readonly status: "supported";
155
+ readonly mode: "token";
156
+ readonly operations: readonly [{
157
+ readonly id: "spreedly.card_request";
158
+ readonly effect: "processor_specific_card_request";
159
+ readonly amount_binding: "unestablished";
160
+ readonly psp: "spreedly";
161
+ readonly version: 1;
162
+ readonly mode: "token";
163
+ readonly autopilot_eligible: false;
164
+ readonly requires: "reviewed_final_payment_and_continuation_evidence";
165
+ }];
166
+ readonly scenarios: {
167
+ readonly one_time: {
168
+ readonly status: "supported";
169
+ readonly reason: "card_request_adapter_implemented";
170
+ readonly requirements: readonly ["actual_payment_request_validation", "merchant_confirmation"];
171
+ };
172
+ readonly save_card: {
173
+ readonly status: "conditional";
174
+ readonly reason: "merchant_card_storage_and_consent_required";
175
+ readonly requirements: readonly ["actual_payment_request_validation", "merchant_confirmation", "stored_card_consent", "merchant_card_saved"];
176
+ };
177
+ readonly subscription_initial: {
178
+ readonly status: "conditional";
179
+ readonly reason: "merchant_card_storage_and_consent_required";
180
+ readonly requirements: readonly ["actual_payment_request_validation", "merchant_confirmation", "stored_card_consent", "merchant_subscription_confirmation"];
181
+ };
182
+ readonly subscription_renewal: {
183
+ readonly status: "outside_vault";
184
+ readonly reason: "renewal_managed_by_merchant";
185
+ readonly requirements: readonly ["verify_merchant_billing"];
186
+ };
187
+ };
188
+ readonly limitations: readonly ["Only the listed request format is recognized; a recognized processor is not verified merchant coverage.", "The adapter handles the Spreedly iframe v1 restricted endpoint for new credit cards through the encrypted relay.", "The merchant environment and optional signed iframe session must remain valid. Expired signatures require a fresh merchant checkout.", "Spreedly creates a payment-method token; the merchant still completes any gateway payment and authentication."];
189
+ readonly unsupported_variants: readonly [{
190
+ readonly variant: "general_javascript_api";
191
+ readonly status: "unsupported";
192
+ readonly constraint: "direct_sdk";
193
+ readonly reason: "The general payment_methods.json endpoint is outside the reviewed native iframe path.";
194
+ }, {
195
+ readonly variant: "saved_card_or_recache";
196
+ readonly status: "unsupported";
197
+ readonly constraint: "direct_sdk";
198
+ readonly reason: "Stored tokens and CVV-only recache requests are outside new-card tokenization.";
199
+ }, {
200
+ readonly variant: "click_to_pay_or_bank_account";
201
+ readonly status: "unsupported";
202
+ readonly constraint: "direct_sdk";
203
+ readonly reason: "Click to Pay and bank-account request shapes require separate validation.";
204
+ }, {
205
+ readonly variant: "gateway_purchase_or_distribution";
206
+ readonly status: "unsupported";
207
+ readonly constraint: "direct_sdk";
208
+ readonly reason: "Gateway transactions and payment-method distribution are outside the card tokenization adapter.";
209
+ }];
210
+ readonly processor_amount: "none";
211
+ readonly challenge_surface: "merchant_browser";
212
+ readonly merchant_confirmation_required: true;
213
+ readonly live_validation: "not_established_by_catalog";
214
+ readonly sources: readonly ["src/definitions.js", "src/operations.js", "src/backend.js", "src/spreedly.js", "../checkout/src/prepared-processor.ts"];
215
+ }, {
151
216
  readonly psp: "shopify";
152
217
  readonly status: "supported";
153
218
  readonly mode: "token";
@@ -1347,6 +1412,14 @@ export declare function getCheckoutPreflightCatalog(): {
1347
1412
  };
1348
1413
  observation_reason_codes: string[];
1349
1414
  };
1415
+ /**
1416
+ * Whether a visible card-entry frame on `hostname` can belong to a processor already evidenced in the
1417
+ * snapshot. A collector reports `card_entry_unrecognized` when this is false, so co-loaded SDK scripts
1418
+ * cannot be presented as the processor behind card controls that no reviewed fingerprint explains, and
1419
+ * a card form with no processor evidence at all is reported rather than passing as an empty page.
1420
+ * The hostname stays local to the caller; nothing here enters evidence.
1421
+ */
1422
+ export declare function cardEntryAttributable(input: CheckoutSignalSnapshot | unknown, hostname: unknown): boolean;
1350
1423
  /** Classify a sanitized snapshot. No browser, HTTP, or payment operations occur here. */
1351
1424
  export declare function assessCheckoutSupport(input: CheckoutSignalSnapshot | unknown, options?: CheckoutPreflightOptions): CheckoutSupportResult;
1352
1425
  /** Machine-readable transport shapes; semantic compatibility is checked by the classifier. */