@agent-cards/checkout 0.8.0 → 0.9.1
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.
- package/CHANGELOG.md +7 -0
- package/PREFLIGHT.md +300 -0
- package/README.md +7 -3
- package/dist/cdp.js +2 -2
- package/dist/playwright.d.ts +3 -0
- package/dist/playwright.js +3 -0
- package/dist/preflight-capabilities.generated.d.ts +1165 -0
- package/dist/preflight-capabilities.generated.js +1799 -0
- package/dist/preflight-catalog.json +3892 -0
- package/dist/preflight-playwright.d.ts +34 -0
- package/dist/preflight-playwright.js +262 -0
- package/dist/preflight-schemas.json +1063 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.generated.d.ts +1867 -0
- package/dist/preflight.generated.js +482 -0
- package/dist/preflight.js +2 -0
- package/examples/preflight/classify-direct.mjs +21 -0
- package/examples/preflight/classify-kernel.mjs +30 -0
- package/examples/preflight/inspect-browser.mjs +44 -0
- package/examples/preflight/kernel-native/README.md +112 -0
- package/examples/preflight/kernel-native/documented-adapters.json +113 -0
- package/examples/preflight/kernel-native/inventory.json +224 -0
- package/examples/preflight/kernel-native/qualification.mjs +182 -0
- package/examples/preflight/kernel-profile.empty.json +11 -0
- package/examples/preflight/mollie-hosted.observations.json +23 -0
- package/examples/preflight/mollie-hosted.result.json +103 -0
- package/examples/preflight/stripe-script.direct.result.json +92 -0
- package/examples/preflight/stripe-script.observations.json +16 -0
- package/examples/preflight/stripe-script.result.json +87 -0
- package/package.json +15 -6
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CheckoutSignalSnapshot, type CheckoutPreflightOptions, type CheckoutSupportResult } from './preflight.js';
|
|
2
|
+
/** The public Playwright methods used by inspection; no Playwright dependency is required. */
|
|
3
|
+
export interface CheckoutInspectionFrame {
|
|
4
|
+
url(): string;
|
|
5
|
+
parentFrame(): CheckoutInspectionFrame | null;
|
|
6
|
+
isDetached(): boolean;
|
|
7
|
+
evaluate<Result, Argument>(fn: (argument: Argument) => Result | Promise<Result>, argument: Argument): Promise<Result>;
|
|
8
|
+
frameElement(): Promise<{
|
|
9
|
+
evaluate<Result>(fn: (element: Element) => Result | Promise<Result>): Promise<Result>;
|
|
10
|
+
dispose(): Promise<void>;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export interface CheckoutInspectionPage {
|
|
14
|
+
frames(): CheckoutInspectionFrame[];
|
|
15
|
+
on(event: 'framenavigated' | 'frameattached' | 'framedetached', listener: (frame: CheckoutInspectionFrame) => void): unknown;
|
|
16
|
+
off(event: 'framenavigated' | 'frameattached' | 'framedetached', listener: (frame: CheckoutInspectionFrame) => void): unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface CheckoutCollectionOptions {
|
|
19
|
+
/** Total inspection deadline, including all frames. Default 1,500 ms; maximum 10,000 ms. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Maximum frame count. Default and maximum 64. */
|
|
22
|
+
maxFrames?: number;
|
|
23
|
+
/** Maximum document and asset observations. Default and maximum 2,048. */
|
|
24
|
+
maxSignals?: number;
|
|
25
|
+
/** Maximum transient observation and returned snapshot size. Default and maximum 262,144 bytes; minimum 512 bytes. */
|
|
26
|
+
maxSnapshotBytes?: number;
|
|
27
|
+
}
|
|
28
|
+
export type InspectCheckoutOptions = CheckoutPreflightOptions & {
|
|
29
|
+
collection?: CheckoutCollectionOptions;
|
|
30
|
+
};
|
|
31
|
+
/** Read only frame, external asset and form locations; return reviewed rule IDs, never raw URLs. */
|
|
32
|
+
export declare function collectCheckoutSignals(page: CheckoutInspectionPage, options?: CheckoutCollectionOptions): Promise<CheckoutSignalSnapshot>;
|
|
33
|
+
/** Inspect once without preparing or submitting a checkout. */
|
|
34
|
+
export declare function inspectCheckout(page: CheckoutInspectionPage, options?: InspectCheckoutOptions): Promise<CheckoutSupportResult>;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { normalizeCheckoutSignals, assessCheckoutSupport, } from './preflight.js';
|
|
2
|
+
const EVENTS = ['framenavigated', 'frameattached', 'framedetached'];
|
|
3
|
+
const DEADLINE = Symbol('checkout inspection deadline');
|
|
4
|
+
const DEFAULTS = { timeoutMs: 1_500, maxFrames: 64, maxSignals: 2_048, maxSnapshotBytes: 262_144 };
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
function bounded(value, fallback, maximum, minimum = 1) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return fallback;
|
|
9
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
10
|
+
throw new TypeError('Checkout inspection limits must be positive integers within the documented bounds.');
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
async function beforeDeadline(operation, deadline) {
|
|
15
|
+
const remaining = deadline - performance.now();
|
|
16
|
+
if (remaining <= 0)
|
|
17
|
+
throw DEADLINE;
|
|
18
|
+
let timer;
|
|
19
|
+
try {
|
|
20
|
+
return await Promise.race([
|
|
21
|
+
Promise.resolve().then(operation),
|
|
22
|
+
new Promise((_resolve, reject) => { timer = setTimeout(() => reject(DEADLINE), remaining); }),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (timer !== undefined)
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Read only frame, external asset and form locations; return reviewed rule IDs, never raw URLs. */
|
|
31
|
+
export async function collectCheckoutSignals(page, options = {}) {
|
|
32
|
+
const limits = {
|
|
33
|
+
timeoutMs: bounded(options.timeoutMs, DEFAULTS.timeoutMs, 10_000),
|
|
34
|
+
maxFrames: bounded(options.maxFrames, DEFAULTS.maxFrames, DEFAULTS.maxFrames),
|
|
35
|
+
maxSignals: bounded(options.maxSignals, DEFAULTS.maxSignals, DEFAULTS.maxSignals),
|
|
36
|
+
maxSnapshotBytes: bounded(options.maxSnapshotBytes, DEFAULTS.maxSnapshotBytes, DEFAULTS.maxSnapshotBytes, 512),
|
|
37
|
+
};
|
|
38
|
+
const deadline = performance.now() + limits.timeoutMs;
|
|
39
|
+
const reasons = new Set();
|
|
40
|
+
const signals = [];
|
|
41
|
+
const registered = [];
|
|
42
|
+
let transientBytes = 0;
|
|
43
|
+
let truncated = false;
|
|
44
|
+
const changed = () => { reasons.add('navigation_changed'); };
|
|
45
|
+
const limit = (reason) => { reasons.add(reason); truncated = true; };
|
|
46
|
+
const append = (signal) => {
|
|
47
|
+
if (signals.length >= limits.maxSignals) {
|
|
48
|
+
limit('signal_limit');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Raw merchant URLs exist only during this call and never enter errors or the return value.
|
|
52
|
+
const bytes = encoder.encode(JSON.stringify(signal)).length;
|
|
53
|
+
if (transientBytes + bytes > limits.maxSnapshotBytes) {
|
|
54
|
+
limit('snapshot_limit');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
transientBytes += bytes;
|
|
58
|
+
signals.push(signal);
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
for (const event of EVENTS) {
|
|
63
|
+
page.on(event, changed);
|
|
64
|
+
registered.push(event);
|
|
65
|
+
}
|
|
66
|
+
const observedFrames = page.frames();
|
|
67
|
+
const roots = observedFrames.filter(frame => frame.parentFrame() === null);
|
|
68
|
+
if (roots.length !== 1)
|
|
69
|
+
reasons.add('frame_unavailable');
|
|
70
|
+
// Frame zero always denotes the main document, regardless of adapter enumeration order.
|
|
71
|
+
const frames = roots.length === 1 ? [roots[0], ...observedFrames.filter(frame => frame !== roots[0])] : observedFrames;
|
|
72
|
+
const initialUrls = new Map(frames.map(frame => [frame, frame.url()]));
|
|
73
|
+
const ids = new Map(frames.map((frame, index) => [frame, index]));
|
|
74
|
+
if (frames.length > limits.maxFrames)
|
|
75
|
+
limit('frame_limit');
|
|
76
|
+
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;
|
|
96
|
+
}
|
|
97
|
+
if (truncated && reasons.has('snapshot_limit'))
|
|
98
|
+
break;
|
|
99
|
+
const parent = frame.parentFrame();
|
|
100
|
+
const frame_id = ids.get(frame);
|
|
101
|
+
const parent_frame_id = parent ? ids.get(parent) ?? null : null;
|
|
102
|
+
let visible = parent ? visibility.get(parent) ?? null : true;
|
|
103
|
+
try {
|
|
104
|
+
if (frame.isDetached()) {
|
|
105
|
+
reasons.add('frame_unavailable');
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (parent) {
|
|
109
|
+
if (!ids.has(parent))
|
|
110
|
+
reasons.add('frame_unavailable');
|
|
111
|
+
let acquisitionTimedOut = false;
|
|
112
|
+
const acquisition = Promise.resolve().then(() => frame.frameElement());
|
|
113
|
+
void acquisition.then(element => {
|
|
114
|
+
if (acquisitionTimedOut)
|
|
115
|
+
void Promise.resolve().then(() => element.dispose()).catch(() => { });
|
|
116
|
+
}, () => { });
|
|
117
|
+
let element;
|
|
118
|
+
try {
|
|
119
|
+
element = await beforeDeadline(() => acquisition, deadline);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
acquisitionTimedOut = true;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const ownVisibility = await beforeDeadline(() => element.evaluate(node => {
|
|
127
|
+
if (!node.isConnected)
|
|
128
|
+
return null;
|
|
129
|
+
for (let current = node; current;) {
|
|
130
|
+
const style = getComputedStyle(current);
|
|
131
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || style.opacity === '0')
|
|
132
|
+
return false;
|
|
133
|
+
const root = current.getRootNode();
|
|
134
|
+
current = current.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
|
|
135
|
+
}
|
|
136
|
+
const rect = node.getBoundingClientRect();
|
|
137
|
+
return rect.width > 0 && rect.height > 0;
|
|
138
|
+
}), deadline);
|
|
139
|
+
visible = visible === false || ownVisibility === false ? false : visible === null || ownVisibility === null ? null : true;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
// Disposal only releases our automation handle. A stalled disposal cannot extend the inspection deadline.
|
|
143
|
+
await beforeDeadline(() => element.dispose(), deadline);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
visibility.set(frame, visible);
|
|
147
|
+
const data = await beforeDeadline(() => frame.evaluate(({ remaining, byteBudget }) => {
|
|
148
|
+
const collected = [];
|
|
149
|
+
let bytes = 0;
|
|
150
|
+
let signalLimit = false;
|
|
151
|
+
let byteLimit = false;
|
|
152
|
+
const elements = document.querySelectorAll('script[src], iframe[src], form[action]');
|
|
153
|
+
for (let index = 0; index < elements.length; index++) {
|
|
154
|
+
if (index >= remaining) {
|
|
155
|
+
signalLimit = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
const element = elements[index];
|
|
159
|
+
const kind = element.tagName === 'SCRIPT' ? 'script' : element.tagName === 'IFRAME' ? 'iframe' : 'form';
|
|
160
|
+
const source = element.getAttribute(kind === 'form' ? 'action' : 'src');
|
|
161
|
+
if (!source)
|
|
162
|
+
continue;
|
|
163
|
+
if (source.length > byteBudget - bytes) {
|
|
164
|
+
byteLimit = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
let url;
|
|
168
|
+
try {
|
|
169
|
+
url = new URL(source, document.baseURI).href;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
bytes += url.length * 3 + 128;
|
|
175
|
+
if (bytes > byteBudget) {
|
|
176
|
+
byteLimit = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
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 });
|
|
193
|
+
}
|
|
194
|
+
const documentUrl = document.URL;
|
|
195
|
+
if (documentUrl.length * 3 > byteBudget)
|
|
196
|
+
byteLimit = true;
|
|
197
|
+
return { documentUrl: byteLimit && documentUrl.length * 3 > byteBudget ? '' : documentUrl, collected, signalLimit, byteLimit };
|
|
198
|
+
}, { remaining: Math.max(0, limits.maxSignals - signals.length - 1), byteBudget: Math.max(0, limits.maxSnapshotBytes - transientBytes) }), deadline);
|
|
199
|
+
if (frame.isDetached())
|
|
200
|
+
reasons.add('frame_unavailable');
|
|
201
|
+
if (data.documentUrl !== initialUrls.get(frame) || frame.url() !== initialUrls.get(frame))
|
|
202
|
+
reasons.add('navigation_changed');
|
|
203
|
+
append({ kind: parent ? 'frame' : 'document', url: data.documentUrl, frame_id, parent_frame_id, visible });
|
|
204
|
+
for (const signal of data.collected) {
|
|
205
|
+
if (!append({ ...signal, frame_id, parent_frame_id, visible: visible === false ? false : signal.kind === 'script' ? visible : visible === null ? null : signal.visible }))
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (data.signalLimit)
|
|
209
|
+
limit('signal_limit');
|
|
210
|
+
if (data.byteLimit)
|
|
211
|
+
limit('snapshot_limit');
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error === DEADLINE) {
|
|
215
|
+
reasons.add('collection_timeout');
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
// Browser errors can include complete merchant URLs. Return a bounded reason instead.
|
|
219
|
+
reasons.add('frame_unavailable');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (performance.now() >= deadline)
|
|
223
|
+
reasons.add('collection_timeout');
|
|
224
|
+
const finalFrames = page.frames();
|
|
225
|
+
if (frames.length !== finalFrames.length || finalFrames.some(frame => !ids.has(frame) || frame.url() !== initialUrls.get(frame))) {
|
|
226
|
+
reasons.add('navigation_changed');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
reasons.add('frame_unavailable');
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
for (const event of registered) {
|
|
234
|
+
try {
|
|
235
|
+
page.off(event, changed);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
reasons.add('collection_incomplete');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let snapshot = normalizeCheckoutSignals({ signals, observation: { complete: reasons.size === 0, truncated, reasons: [...reasons] } });
|
|
243
|
+
if (encoder.encode(JSON.stringify(snapshot)).length > limits.maxSnapshotBytes) {
|
|
244
|
+
limit('snapshot_limit');
|
|
245
|
+
const boundedSnapshot = normalizeCheckoutSignals({ signals: [], observation: { complete: false, truncated, reasons: [...reasons] } });
|
|
246
|
+
let bytes = encoder.encode(JSON.stringify(boundedSnapshot)).length;
|
|
247
|
+
for (const signal of snapshot.signals) {
|
|
248
|
+
const addedBytes = encoder.encode(JSON.stringify(signal)).length + (boundedSnapshot.signals.length ? 1 : 0);
|
|
249
|
+
if (bytes + addedBytes > limits.maxSnapshotBytes)
|
|
250
|
+
break;
|
|
251
|
+
boundedSnapshot.signals.push(signal);
|
|
252
|
+
bytes += addedBytes;
|
|
253
|
+
}
|
|
254
|
+
snapshot = boundedSnapshot;
|
|
255
|
+
}
|
|
256
|
+
return snapshot;
|
|
257
|
+
}
|
|
258
|
+
/** Inspect once without preparing or submitting a checkout. */
|
|
259
|
+
export async function inspectCheckout(page, options = {}) {
|
|
260
|
+
const { collection, ...assessment } = options;
|
|
261
|
+
return assessCheckoutSupport(await collectCheckoutSignals(page, collection), assessment);
|
|
262
|
+
}
|