@conto_finance/sdk 0.3.1 → 0.5.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.
@@ -0,0 +1,1394 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/playwright.ts
21
+ var playwright_exports = {};
22
+ __export(playwright_exports, {
23
+ createEncryptedPlaywrightCheckoutTargetStore: () => createEncryptedPlaywrightCheckoutTargetStore,
24
+ createGuardedPlaywrightCheckoutHttpHandler: () => createGuardedPlaywrightCheckoutHttpHandler,
25
+ createGuardedPlaywrightCheckoutRuntime: () => createGuardedPlaywrightCheckoutRuntime,
26
+ createGuardedPlaywrightCheckoutRuntimeHost: () => createGuardedPlaywrightCheckoutRuntimeHost,
27
+ createInMemoryPlaywrightCheckoutTargetStore: () => createInMemoryPlaywrightCheckoutTargetStore,
28
+ createPlaywrightCdpSessionRegistry: () => createPlaywrightCdpSessionRegistry,
29
+ createPlaywrightCheckoutAdapter: () => createPlaywrightCheckoutAdapter,
30
+ createPlaywrightSessionObserver: () => createPlaywrightSessionObserver
31
+ });
32
+ module.exports = __toCommonJS(playwright_exports);
33
+ var import_node_crypto2 = require("crypto");
34
+
35
+ // src/browser-checkout.ts
36
+ var import_node_crypto = require("crypto");
37
+ var MAX_URL_LENGTH = 2048;
38
+ var MAX_LOCATOR_LENGTH = 1024;
39
+ var MAX_TEXT_LENGTH = 500;
40
+ var BrowserCheckoutTargetVerificationError = class extends Error {
41
+ constructor(code, message) {
42
+ super(message);
43
+ this.code = code;
44
+ this.name = "BrowserCheckoutTargetVerificationError";
45
+ }
46
+ };
47
+ function invalid(message) {
48
+ throw new BrowserCheckoutTargetVerificationError("BROWSER_CHECKOUT_TARGET_INVALID", message);
49
+ }
50
+ function canonicalHttpsUrl(value, field) {
51
+ if (typeof value !== "string" || !value || value.length > MAX_URL_LENGTH) {
52
+ invalid(`${field} is invalid.`);
53
+ }
54
+ let url;
55
+ try {
56
+ url = new URL(value);
57
+ } catch {
58
+ invalid(`${field} is invalid.`);
59
+ }
60
+ if (url.protocol !== "https:" || url.username || url.password) {
61
+ invalid(`${field} must use HTTPS without credentials.`);
62
+ }
63
+ url.hostname = url.hostname.toLowerCase();
64
+ url.hash = "";
65
+ return url.toString();
66
+ }
67
+ function canonicalText(value, field, options = {}) {
68
+ if (value === void 0) {
69
+ if (options.required) invalid(`${field} is required.`);
70
+ return null;
71
+ }
72
+ if (typeof value !== "string") invalid(`${field} is invalid.`);
73
+ const trimmed = value.trim();
74
+ const normalized = options.collapseWhitespace === false ? trimmed : trimmed.replace(/\s+/g, " ");
75
+ if (options.required && !normalized || normalized.length > (options.maxLength ?? MAX_TEXT_LENGTH)) {
76
+ invalid(`${field} is invalid.`);
77
+ }
78
+ return normalized || null;
79
+ }
80
+ function canonicalizeBrowserCheckoutTarget(snapshot) {
81
+ if (!snapshot || typeof snapshot !== "object") {
82
+ invalid("The browser checkout target is invalid.");
83
+ }
84
+ if (typeof snapshot.disabled !== "boolean") {
85
+ invalid("target.disabled is invalid.");
86
+ }
87
+ const tagName = canonicalText(snapshot.tagName, "target.tagName", {
88
+ required: true,
89
+ maxLength: 80
90
+ });
91
+ return Object.freeze({
92
+ pageUrl: canonicalHttpsUrl(snapshot.pageUrl, "target.pageUrl"),
93
+ locator: canonicalText(snapshot.locator, "target.locator", {
94
+ collapseWhitespace: false,
95
+ required: true,
96
+ maxLength: MAX_LOCATOR_LENGTH
97
+ }) ?? "",
98
+ tagName: (tagName ?? "").toLowerCase(),
99
+ role: canonicalText(snapshot.role, "target.role", { maxLength: 80 })?.toLowerCase() ?? null,
100
+ accessibleName: canonicalText(snapshot.accessibleName, "target.accessibleName"),
101
+ targetUrl: snapshot.targetUrl ? canonicalHttpsUrl(snapshot.targetUrl, "target.targetUrl") : null,
102
+ formAction: snapshot.formAction ? canonicalHttpsUrl(snapshot.formAction, "target.formAction") : null,
103
+ opensNewPage: snapshot.opensNewPage === true,
104
+ disabled: snapshot.disabled
105
+ });
106
+ }
107
+ function fingerprintBrowserCheckoutTarget(snapshot) {
108
+ const canonical = canonicalizeBrowserCheckoutTarget(snapshot);
109
+ const digest = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
110
+ return `browser_target_sha256_${digest}`;
111
+ }
112
+ function createVerifiedBrowserCheckoutDriver(resolver) {
113
+ if (!resolver || typeof resolver.resolve !== "function") {
114
+ invalid("A browser checkout target resolver is required.");
115
+ }
116
+ return Object.freeze({
117
+ async dispatch(input) {
118
+ const target = await resolver.resolve(input);
119
+ if (!target) {
120
+ throw new BrowserCheckoutTargetVerificationError(
121
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
122
+ "The approved browser checkout target was not found."
123
+ );
124
+ }
125
+ if (typeof target.dispatch !== "function") {
126
+ invalid("The resolved browser checkout target cannot be dispatched.");
127
+ }
128
+ const canonical = canonicalizeBrowserCheckoutTarget(target.snapshot);
129
+ const approvedPageUrl = canonicalHttpsUrl(input.action.pageUrl, "action.pageUrl");
130
+ const approvedTargetUrl = input.action.targetUrl ? canonicalHttpsUrl(input.action.targetUrl, "action.targetUrl") : null;
131
+ const observedDestinations = [canonical.targetUrl, canonical.formAction].filter(
132
+ (value) => value !== null
133
+ );
134
+ const destinationsAreBound = approvedTargetUrl === null ? observedDestinations.length === 0 : observedDestinations.length > 0 && observedDestinations.every((value) => value === approvedTargetUrl);
135
+ if (canonical.pageUrl !== approvedPageUrl || !destinationsAreBound || fingerprintBrowserCheckoutTarget(target.snapshot) !== input.action.targetFingerprint.trim()) {
136
+ throw new BrowserCheckoutTargetVerificationError(
137
+ "BROWSER_CHECKOUT_TARGET_CHANGED",
138
+ "The browser checkout target changed after Conto authorization."
139
+ );
140
+ }
141
+ if (canonical.disabled) {
142
+ throw new BrowserCheckoutTargetVerificationError(
143
+ "BROWSER_CHECKOUT_TARGET_DISABLED",
144
+ "The approved browser checkout target is disabled."
145
+ );
146
+ }
147
+ return target.dispatch();
148
+ }
149
+ });
150
+ }
151
+
152
+ // src/playwright.ts
153
+ var DEFAULT_CLICK_TIMEOUT_MS = 3e4;
154
+ var MAX_CLICK_TIMEOUT_MS = 12e4;
155
+ var DEFAULT_POPUP_TIMEOUT_MS = 5e3;
156
+ var MAX_POPUP_TIMEOUT_MS = 3e4;
157
+ var DEFAULT_TARGET_TTL_MS = 10 * 6e4;
158
+ var MAX_TARGET_TTL_MS = 60 * 6e4;
159
+ var TARGET_STORE_ENVELOPE_VERSION = 1;
160
+ var DEFAULT_HTTP_BODY_LIMIT_BYTES = 64 * 1024;
161
+ var MAX_HTTP_BODY_LIMIT_BYTES = 1024 * 1024;
162
+ var DEFAULT_CDP_SESSION_LIMIT = 100;
163
+ var MAX_CDP_SESSION_LIMIT = 1e3;
164
+ var SESSION_OBSERVATION_TARGET_LIMIT = 50;
165
+ var SESSION_OBSERVATION_LABEL_LIMIT = 160;
166
+ var SESSION_OBSERVATION_TITLE_LIMIT = 240;
167
+ var SESSION_OBSERVATION_SELECTOR = 'a[href], button, input[type="submit"], input[type="button"], [role="button"], [role="link"]';
168
+ function verificationError(code, message) {
169
+ return new BrowserCheckoutTargetVerificationError(code, message);
170
+ }
171
+ function requiredText(value, field, maxLength) {
172
+ if (typeof value !== "string") {
173
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", `${field} is required.`);
174
+ }
175
+ const normalized = value.trim();
176
+ if (!normalized || normalized.length > maxLength) {
177
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", `${field} is invalid.`);
178
+ }
179
+ return normalized;
180
+ }
181
+ function clickTimeout(value) {
182
+ const resolved = value ?? DEFAULT_CLICK_TIMEOUT_MS;
183
+ if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_CLICK_TIMEOUT_MS) {
184
+ throw verificationError(
185
+ "BROWSER_CHECKOUT_TARGET_INVALID",
186
+ "clickTimeoutMs must be a positive integer no greater than 120000."
187
+ );
188
+ }
189
+ return resolved;
190
+ }
191
+ function targetTtl(value) {
192
+ const resolved = value ?? DEFAULT_TARGET_TTL_MS;
193
+ if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_TARGET_TTL_MS) {
194
+ throw verificationError(
195
+ "BROWSER_CHECKOUT_TARGET_INVALID",
196
+ "targetTtlMs must be a positive integer no greater than 3600000."
197
+ );
198
+ }
199
+ return resolved;
200
+ }
201
+ function popupTimeout(value) {
202
+ const resolved = value ?? DEFAULT_POPUP_TIMEOUT_MS;
203
+ if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_POPUP_TIMEOUT_MS) {
204
+ throw verificationError(
205
+ "BROWSER_CHECKOUT_TARGET_INVALID",
206
+ "popupTimeoutMs must be a positive integer no greater than 30000."
207
+ );
208
+ }
209
+ return resolved;
210
+ }
211
+ function currentDate(now) {
212
+ const value = now();
213
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
214
+ throw verificationError(
215
+ "BROWSER_CHECKOUT_TARGET_INVALID",
216
+ "The Playwright checkout clock returned an invalid date."
217
+ );
218
+ }
219
+ return value;
220
+ }
221
+ function observedHttpsPageUrl(value) {
222
+ if (typeof value !== "string" || !value || value.length > 2048) {
223
+ throw verificationError(
224
+ "BROWSER_CHECKOUT_TARGET_INVALID",
225
+ "The observed checkout page URL is invalid."
226
+ );
227
+ }
228
+ try {
229
+ const url = new URL(value);
230
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("unsafe URL");
231
+ return value;
232
+ } catch {
233
+ throw verificationError(
234
+ "BROWSER_CHECKOUT_TARGET_INVALID",
235
+ "The observed checkout page URL must use HTTPS without credentials."
236
+ );
237
+ }
238
+ }
239
+ function targetStoreKey(input) {
240
+ return `${input.browserSessionId}\0${input.targetFingerprint}`;
241
+ }
242
+ function createInMemoryPlaywrightCheckoutTargetStore() {
243
+ const registrations = /* @__PURE__ */ new Map();
244
+ return Object.freeze({
245
+ async put(registration) {
246
+ registrations.set(targetStoreKey(registration), Object.freeze({ ...registration }));
247
+ },
248
+ async get(input) {
249
+ return registrations.get(targetStoreKey(input)) ?? null;
250
+ },
251
+ async delete(input) {
252
+ registrations.delete(targetStoreKey(input));
253
+ }
254
+ });
255
+ }
256
+ function encryptedStoreKey(namespace, input) {
257
+ const digest = (0, import_node_crypto2.createHash)("sha256").update(namespace).update("\0").update(input.browserSessionId).update("\0").update(input.targetFingerprint).digest("hex");
258
+ return `${namespace}:${digest}`;
259
+ }
260
+ function validRegistration(value, expected) {
261
+ if (!value || typeof value !== "object") return false;
262
+ const registration = value;
263
+ return typeof registration.browserSessionId === "string" && registration.browserSessionId.length > 0 && registration.browserSessionId.length <= 255 && typeof registration.targetFingerprint === "string" && /^browser_target_sha256_[0-9a-f]{64}$/.test(registration.targetFingerprint) && typeof registration.locator === "string" && registration.locator.trim().length > 0 && registration.locator.length <= 1024 && typeof registration.expiresAt === "string" && !Number.isNaN(Date.parse(registration.expiresAt)) && (!expected || registration.browserSessionId === expected.browserSessionId && registration.targetFingerprint === expected.targetFingerprint);
264
+ }
265
+ function createEncryptedPlaywrightCheckoutTargetStore(config) {
266
+ if (!config || !config.blobStore || typeof config.blobStore.put !== "function" || typeof config.blobStore.get !== "function" || typeof config.blobStore.delete !== "function") {
267
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "blobStore is required.");
268
+ }
269
+ const key = Buffer.from(config.encryptionKey ?? []);
270
+ if (key.length !== 32) {
271
+ throw verificationError(
272
+ "BROWSER_CHECKOUT_TARGET_INVALID",
273
+ "encryptionKey must contain exactly 32 bytes."
274
+ );
275
+ }
276
+ const namespace = requiredText(
277
+ config.namespace ?? "conto:playwright-target:v1",
278
+ "namespace",
279
+ 120
280
+ );
281
+ const now = config.now ?? (() => /* @__PURE__ */ new Date());
282
+ return Object.freeze({
283
+ async put(registration) {
284
+ if (!validRegistration(registration)) {
285
+ throw verificationError(
286
+ "BROWSER_CHECKOUT_TARGET_INVALID",
287
+ "The Playwright checkout target registration is invalid."
288
+ );
289
+ }
290
+ if (Date.parse(registration.expiresAt) <= currentDate(now).getTime()) {
291
+ throw verificationError(
292
+ "BROWSER_CHECKOUT_TARGET_INVALID",
293
+ "The Playwright checkout target registration is already expired."
294
+ );
295
+ }
296
+ const storageKey = encryptedStoreKey(namespace, registration);
297
+ const iv = (0, import_node_crypto2.randomBytes)(12);
298
+ const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", key, iv);
299
+ cipher.setAAD(Buffer.from(storageKey));
300
+ const ciphertext = Buffer.concat([
301
+ cipher.update(JSON.stringify(registration), "utf8"),
302
+ cipher.final()
303
+ ]);
304
+ const envelope = JSON.stringify({
305
+ version: TARGET_STORE_ENVELOPE_VERSION,
306
+ iv: iv.toString("base64url"),
307
+ ciphertext: ciphertext.toString("base64url"),
308
+ tag: cipher.getAuthTag().toString("base64url")
309
+ });
310
+ await config.blobStore.put({
311
+ key: storageKey,
312
+ value: envelope,
313
+ expiresAt: registration.expiresAt
314
+ });
315
+ },
316
+ async get(input) {
317
+ const storageKey = encryptedStoreKey(namespace, input);
318
+ const envelopeText = await config.blobStore.get(storageKey);
319
+ if (envelopeText === null) return null;
320
+ try {
321
+ const envelope = JSON.parse(envelopeText);
322
+ if (envelope.version !== TARGET_STORE_ENVELOPE_VERSION || typeof envelope.iv !== "string" || typeof envelope.ciphertext !== "string" || typeof envelope.tag !== "string") {
323
+ throw new Error("invalid envelope");
324
+ }
325
+ const decipher = (0, import_node_crypto2.createDecipheriv)(
326
+ "aes-256-gcm",
327
+ key,
328
+ Buffer.from(envelope.iv, "base64url")
329
+ );
330
+ decipher.setAAD(Buffer.from(storageKey));
331
+ decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
332
+ const plaintext = Buffer.concat([
333
+ decipher.update(Buffer.from(envelope.ciphertext, "base64url")),
334
+ decipher.final()
335
+ ]).toString("utf8");
336
+ const registration = JSON.parse(plaintext);
337
+ if (!validRegistration(registration, input)) throw new Error("invalid registration");
338
+ if (Date.parse(registration.expiresAt) <= currentDate(now).getTime()) {
339
+ await config.blobStore.delete(storageKey);
340
+ return null;
341
+ }
342
+ return Object.freeze({ ...registration });
343
+ } catch (error) {
344
+ if (error instanceof BrowserCheckoutTargetVerificationError) throw error;
345
+ throw verificationError(
346
+ "BROWSER_CHECKOUT_TARGET_INVALID",
347
+ "The encrypted Playwright checkout target registration cannot be verified."
348
+ );
349
+ }
350
+ },
351
+ async delete(input) {
352
+ await config.blobStore.delete(encryptedStoreKey(namespace, input));
353
+ }
354
+ });
355
+ }
356
+ async function exactElementHandle(page, locatorExpression) {
357
+ const locator = page.locator(locatorExpression);
358
+ const count = await locator.count();
359
+ if (count === 0) {
360
+ throw verificationError(
361
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
362
+ "The Playwright checkout target was not found."
363
+ );
364
+ }
365
+ if (count !== 1) {
366
+ throw verificationError(
367
+ "BROWSER_CHECKOUT_TARGET_INVALID",
368
+ "The Playwright checkout locator must resolve to exactly one element."
369
+ );
370
+ }
371
+ const handle = await locator.elementHandle();
372
+ if (!handle) {
373
+ throw verificationError(
374
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
375
+ "The Playwright checkout target became unavailable."
376
+ );
377
+ }
378
+ return handle;
379
+ }
380
+ async function snapshotPlaywrightTarget(input) {
381
+ const handle = input.handle;
382
+ if (!handle) {
383
+ throw verificationError(
384
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
385
+ "The Playwright checkout target became unavailable."
386
+ );
387
+ }
388
+ const element = await handle.evaluate((node) => {
389
+ const html = node;
390
+ const tagName = html.tagName.toLowerCase();
391
+ const explicitRole = html.getAttribute("role")?.trim();
392
+ const inputType = html.getAttribute("type")?.trim().toLowerCase();
393
+ const implicitRole = tagName === "a" && html.href ? "link" : tagName === "button" || tagName === "input" && ["button", "image", "reset", "submit"].includes(inputType ?? "") ? "button" : void 0;
394
+ const accessibleName = html.getAttribute("aria-label")?.trim() || html.textContent?.trim() || html.value?.trim() || html.getAttribute("title")?.trim() || void 0;
395
+ const targetUrl = tagName === "a" && html.href ? html.href : void 0;
396
+ const explicitFormAction = html.getAttribute("formaction")?.trim();
397
+ const formAction = tagName === "button" || tagName === "input" && inputType === "submit" ? explicitFormAction ? new URL(explicitFormAction, document.baseURI).toString() : html.form?.action || void 0 : void 0;
398
+ const target = html.getAttribute("target")?.trim().toLowerCase() || html.getAttribute("formtarget")?.trim().toLowerCase() || html.form?.getAttribute("target")?.trim().toLowerCase();
399
+ return {
400
+ tagName,
401
+ role: explicitRole || implicitRole,
402
+ accessibleName,
403
+ targetUrl,
404
+ formAction,
405
+ opensNewPage: target === "_blank",
406
+ disabled: Boolean(html.disabled) || html.getAttribute("aria-disabled") === "true"
407
+ };
408
+ });
409
+ return {
410
+ pageUrl: input.page.url(),
411
+ locator: input.locator,
412
+ ...element
413
+ };
414
+ }
415
+ function boundDestination(snapshot) {
416
+ const destinations = [snapshot.targetUrl, snapshot.formAction].filter(
417
+ (value) => Boolean(value)
418
+ );
419
+ const unique = new Set(destinations);
420
+ if (unique.size > 1) {
421
+ throw verificationError(
422
+ "BROWSER_CHECKOUT_TARGET_INVALID",
423
+ "The checkout target exposes conflicting navigation and form destinations."
424
+ );
425
+ }
426
+ return destinations[0];
427
+ }
428
+ function createPlaywrightCheckoutAdapter(config) {
429
+ if (!config || typeof config.pageForSession !== "function") {
430
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "pageForSession is required.");
431
+ }
432
+ const store = config.targetStore ?? createInMemoryPlaywrightCheckoutTargetStore();
433
+ const timeout = clickTimeout(config.clickTimeoutMs);
434
+ const popupWait = popupTimeout(config.popupTimeoutMs);
435
+ const ttl = targetTtl(config.targetTtlMs);
436
+ const now = config.now ?? (() => /* @__PURE__ */ new Date());
437
+ async function pageForSession(browserSessionId) {
438
+ const page = await config.pageForSession(browserSessionId);
439
+ if (!page) {
440
+ throw verificationError(
441
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
442
+ "The privileged Playwright page was not found for this browser session."
443
+ );
444
+ }
445
+ return page;
446
+ }
447
+ const driver = createVerifiedBrowserCheckoutDriver({
448
+ resolve: async ({ authorization, browserSessionId, action }) => {
449
+ const registration = await store.get({
450
+ browserSessionId,
451
+ targetFingerprint: action.targetFingerprint
452
+ });
453
+ if (!registration || !validRegistration(registration) || registration.browserSessionId !== browserSessionId || registration.browserSessionId !== authorization.browserSessionId || registration.targetFingerprint !== action.targetFingerprint) {
454
+ return null;
455
+ }
456
+ if (Date.parse(registration.expiresAt) <= currentDate(now).getTime()) {
457
+ await store.delete({ browserSessionId, targetFingerprint: action.targetFingerprint });
458
+ return null;
459
+ }
460
+ await store.delete({ browserSessionId, targetFingerprint: action.targetFingerprint });
461
+ const page = await pageForSession(browserSessionId);
462
+ const pageUrlBeforeClick = page.url();
463
+ const handle = await exactElementHandle(page, registration.locator);
464
+ const snapshot = await snapshotPlaywrightTarget({
465
+ page,
466
+ locator: registration.locator,
467
+ handle
468
+ });
469
+ return {
470
+ snapshot,
471
+ dispatch: async () => {
472
+ let popupPage = null;
473
+ let resolvePopup;
474
+ const popupEvent = new Promise((resolve) => {
475
+ resolvePopup = resolve;
476
+ });
477
+ const capturePopup = (popup) => {
478
+ popupPage = popup;
479
+ resolvePopup?.(popup);
480
+ };
481
+ page.on("popup", capturePopup);
482
+ try {
483
+ await handle.click({ timeout });
484
+ if (snapshot.opensNewPage && !popupPage) {
485
+ let timer;
486
+ try {
487
+ popupPage = await Promise.race([
488
+ popupEvent,
489
+ new Promise((_resolve, reject) => {
490
+ timer = setTimeout(
491
+ () => reject(
492
+ verificationError(
493
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
494
+ "The approved checkout target did not open its declared page."
495
+ )
496
+ ),
497
+ popupWait
498
+ );
499
+ })
500
+ ]);
501
+ } finally {
502
+ if (timer) clearTimeout(timer);
503
+ }
504
+ }
505
+ } finally {
506
+ page.off("popup", capturePopup);
507
+ }
508
+ const observedAt = currentDate(now);
509
+ const observedPage = popupPage ?? page;
510
+ const pageUrl = observedHttpsPageUrl(observedPage.url());
511
+ const kind = popupPage ? "NEW_PAGE" : pageUrl === pageUrlBeforeClick ? "SAME_PAGE" : "PAGE_NAVIGATION";
512
+ return {
513
+ result: {
514
+ clicked: true,
515
+ pageUrl,
516
+ targetFingerprint: action.targetFingerprint,
517
+ browserOutcome: {
518
+ kind,
519
+ observedAt: observedAt.toISOString(),
520
+ settlement: "UNVERIFIED"
521
+ }
522
+ },
523
+ observation: {
524
+ reference: `browser_dispatch_${authorization.authorizationId}`,
525
+ observedAt: observedAt.toISOString(),
526
+ pageUrl
527
+ }
528
+ };
529
+ }
530
+ };
531
+ }
532
+ });
533
+ return Object.freeze({
534
+ async discover(input) {
535
+ const browserSessionId = requiredText(input.browserSessionId, "browserSessionId", 255);
536
+ const locator = requiredText(input.locator, "locator", 1024);
537
+ const page = await pageForSession(browserSessionId);
538
+ const handle = await exactElementHandle(page, locator);
539
+ const snapshot = await snapshotPlaywrightTarget({ page, locator, handle });
540
+ if (!snapshot.accessibleName) {
541
+ throw verificationError(
542
+ "BROWSER_CHECKOUT_TARGET_INVALID",
543
+ "The Playwright checkout target must have an accessible name."
544
+ );
545
+ }
546
+ const targetFingerprint = fingerprintBrowserCheckoutTarget(snapshot);
547
+ const targetUrl = boundDestination(snapshot);
548
+ const action = Object.freeze({
549
+ kind: input.kind,
550
+ checkoutMethod: input.checkoutMethod,
551
+ pageUrl: snapshot.pageUrl,
552
+ targetFingerprint,
553
+ ...targetUrl ? { targetUrl } : {},
554
+ targetLabel: snapshot.accessibleName
555
+ });
556
+ const discoveredAt = currentDate(now);
557
+ await store.put({
558
+ browserSessionId,
559
+ targetFingerprint,
560
+ locator,
561
+ expiresAt: new Date(discoveredAt.getTime() + ttl).toISOString()
562
+ });
563
+ return Object.freeze({ action, targetFingerprint });
564
+ },
565
+ driver
566
+ });
567
+ }
568
+ function createGuardedPlaywrightCheckoutRuntime(config) {
569
+ if (!config || !config.conto || typeof config.conto.runGuardedCheckoutWithDriver !== "function") {
570
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "conto client is required.");
571
+ }
572
+ if (typeof config.dispatchAttemptFor !== "function") {
573
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "dispatchAttemptFor is required.");
574
+ }
575
+ const adapter = createPlaywrightCheckoutAdapter(config);
576
+ return Object.freeze({
577
+ discover: adapter.discover,
578
+ async run(input) {
579
+ const dispatchAttemptId = requiredText(
580
+ await config.dispatchAttemptFor({
581
+ idempotencyKey: input.idempotencyKey,
582
+ browserSessionId: input.browserSessionId,
583
+ targetFingerprint: input.action.targetFingerprint
584
+ }),
585
+ "dispatchAttemptId",
586
+ 255
587
+ );
588
+ return config.conto.runGuardedCheckoutWithDriver(input, {
589
+ dispatchAttemptId,
590
+ driver: adapter.driver
591
+ });
592
+ }
593
+ });
594
+ }
595
+ function cdpSessionLimit(value) {
596
+ const resolved = value ?? DEFAULT_CDP_SESSION_LIMIT;
597
+ if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_CDP_SESSION_LIMIT) {
598
+ throw verificationError(
599
+ "BROWSER_CHECKOUT_TARGET_INVALID",
600
+ "maxSessions must be a positive integer no greater than 1000."
601
+ );
602
+ }
603
+ return resolved;
604
+ }
605
+ function cdpSessionId(value) {
606
+ const normalized = requiredText(value, "browserSessionId", 255);
607
+ if (!/^[A-Za-z0-9._:-]+$/.test(normalized)) {
608
+ throw verificationError(
609
+ "BROWSER_CHECKOUT_TARGET_INVALID",
610
+ "browserSessionId contains unsupported characters."
611
+ );
612
+ }
613
+ return normalized;
614
+ }
615
+ function secureCdpOrigin(value, field) {
616
+ try {
617
+ const url = new URL(value);
618
+ if (url.protocol !== "wss:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash || value !== url.origin) {
619
+ throw new Error("unsafe origin");
620
+ }
621
+ return url.origin;
622
+ } catch {
623
+ throw verificationError(
624
+ "BROWSER_CHECKOUT_TARGET_INVALID",
625
+ `${field} must be an exact WSS or HTTPS origin.`
626
+ );
627
+ }
628
+ }
629
+ function secureCdpConnectUrl(value, allowedOrigins) {
630
+ const normalized = requiredText(value, "connectUrl", 4096);
631
+ try {
632
+ const url = new URL(normalized);
633
+ if (url.protocol !== "wss:" && url.protocol !== "https:" || url.username || url.password || url.hash || !allowedOrigins.has(url.origin)) {
634
+ throw new Error("unsafe connect URL");
635
+ }
636
+ return url.toString();
637
+ } catch {
638
+ throw verificationError(
639
+ "BROWSER_CHECKOUT_TARGET_INVALID",
640
+ "The remote browser connection URL is not permitted."
641
+ );
642
+ }
643
+ }
644
+ function isBrowser(value) {
645
+ if (!value || typeof value !== "object") return false;
646
+ const candidate = value;
647
+ return typeof candidate.contexts === "function" && typeof candidate.isConnected === "function" && typeof candidate.close === "function" && typeof candidate.on === "function";
648
+ }
649
+ function openBrowserPages(browser) {
650
+ return browser.contexts().flatMap((context) => context.pages()).filter((page) => !page.isClosed());
651
+ }
652
+ function createPlaywrightCdpSessionRegistry(config) {
653
+ if (!config || typeof config.connectUrlForSession !== "function") {
654
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "connectUrlForSession is required.");
655
+ }
656
+ if (typeof config.connectOverCDP !== "function") {
657
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "connectOverCDP is required.");
658
+ }
659
+ if (!Array.isArray(config.allowedConnectOrigins) || config.allowedConnectOrigins.length === 0) {
660
+ throw verificationError(
661
+ "BROWSER_CHECKOUT_TARGET_INVALID",
662
+ "allowedConnectOrigins must contain at least one exact origin."
663
+ );
664
+ }
665
+ const allowedOrigins = new Set(
666
+ config.allowedConnectOrigins.map(
667
+ (origin, index) => secureCdpOrigin(origin, `allowedConnectOrigins[${index}]`)
668
+ )
669
+ );
670
+ if (allowedOrigins.size !== config.allowedConnectOrigins.length) {
671
+ throw verificationError(
672
+ "BROWSER_CHECKOUT_TARGET_INVALID",
673
+ "allowedConnectOrigins must not contain duplicates."
674
+ );
675
+ }
676
+ const maximumSessions = cdpSessionLimit(config.maxSessions);
677
+ const connections = /* @__PURE__ */ new Map();
678
+ async function browserForSession(browserSessionId) {
679
+ let candidate;
680
+ try {
681
+ candidate = await config.connectUrlForSession(browserSessionId);
682
+ } catch {
683
+ throw verificationError(
684
+ "BROWSER_CHECKOUT_TARGET_INVALID",
685
+ "Remote browser session ownership could not be verified."
686
+ );
687
+ }
688
+ if (candidate === null) {
689
+ const revoked = connections.get(browserSessionId);
690
+ connections.delete(browserSessionId);
691
+ if (revoked) {
692
+ try {
693
+ const browser = await revoked;
694
+ if (browser?.isConnected()) await browser.close();
695
+ } catch {
696
+ }
697
+ }
698
+ return null;
699
+ }
700
+ const connectUrl = secureCdpConnectUrl(candidate, allowedOrigins);
701
+ const existing = connections.get(browserSessionId);
702
+ if (existing) {
703
+ const browser = await existing;
704
+ if (browser?.isConnected()) return browser;
705
+ connections.delete(browserSessionId);
706
+ }
707
+ if (connections.size >= maximumSessions) {
708
+ throw verificationError(
709
+ "BROWSER_CHECKOUT_TARGET_INVALID",
710
+ "The remote browser session limit has been reached."
711
+ );
712
+ }
713
+ const pending = (async () => {
714
+ let browser;
715
+ try {
716
+ browser = await config.connectOverCDP(connectUrl);
717
+ } catch {
718
+ throw verificationError(
719
+ "BROWSER_CHECKOUT_TARGET_INVALID",
720
+ "The remote browser connection failed."
721
+ );
722
+ }
723
+ if (!isBrowser(browser) || !browser.isConnected()) {
724
+ try {
725
+ if (isBrowser(browser)) await browser.close();
726
+ } catch {
727
+ }
728
+ throw verificationError(
729
+ "BROWSER_CHECKOUT_TARGET_INVALID",
730
+ "The remote browser connection is invalid."
731
+ );
732
+ }
733
+ browser.on("disconnected", () => {
734
+ if (connections.get(browserSessionId) === pending) connections.delete(browserSessionId);
735
+ });
736
+ return browser;
737
+ })();
738
+ connections.set(browserSessionId, pending);
739
+ try {
740
+ const browser = await pending;
741
+ if (!browser) connections.delete(browserSessionId);
742
+ return browser;
743
+ } catch (error) {
744
+ if (connections.get(browserSessionId) === pending) connections.delete(browserSessionId);
745
+ throw error;
746
+ }
747
+ }
748
+ async function pageForSession(browserSessionIdValue) {
749
+ const browserSessionId = cdpSessionId(browserSessionIdValue);
750
+ const browser = await browserForSession(browserSessionId);
751
+ if (!browser) return null;
752
+ const pages = openBrowserPages(browser);
753
+ if (!config.selectPage) {
754
+ if (pages.length === 0) return null;
755
+ if (pages.length !== 1) {
756
+ throw verificationError(
757
+ "BROWSER_CHECKOUT_TARGET_INVALID",
758
+ "The remote browser session has multiple open pages; configure an exact page selector."
759
+ );
760
+ }
761
+ return pages[0] ?? null;
762
+ }
763
+ const selected = await config.selectPage({
764
+ browserSessionId,
765
+ pages: Object.freeze([...pages])
766
+ });
767
+ if (selected === null) return null;
768
+ if (!pages.includes(selected) || selected.isClosed()) {
769
+ throw verificationError(
770
+ "BROWSER_CHECKOUT_TARGET_INVALID",
771
+ "The selected page does not belong to the requested browser session."
772
+ );
773
+ }
774
+ return selected;
775
+ }
776
+ async function closeSession(browserSessionIdValue) {
777
+ const browserSessionId = cdpSessionId(browserSessionIdValue);
778
+ const pending = connections.get(browserSessionId);
779
+ connections.delete(browserSessionId);
780
+ const browser = await pending;
781
+ if (browser?.isConnected()) await browser.close();
782
+ }
783
+ return Object.freeze({
784
+ pageForSession,
785
+ closeSession,
786
+ async close() {
787
+ const pending = [...connections.values()];
788
+ connections.clear();
789
+ const browsers = await Promise.allSettled(pending);
790
+ await Promise.allSettled(
791
+ browsers.flatMap(
792
+ (result) => result.status === "fulfilled" && result.value?.isConnected() ? [result.value.close()] : []
793
+ )
794
+ );
795
+ }
796
+ });
797
+ }
798
+ function boundedObservationText(value, maximum) {
799
+ if (typeof value !== "string") return "";
800
+ return value.replace(/[\u0000-\u001f\u007f\s]+/g, " ").trim().slice(0, maximum);
801
+ }
802
+ function observedHttpsOrigin(value) {
803
+ if (typeof value !== "string" || !value) return void 0;
804
+ try {
805
+ const url = new URL(value);
806
+ if (url.protocol !== "https:" || url.username || url.password || url.port && url.port !== "443") {
807
+ return void 0;
808
+ }
809
+ return url.origin;
810
+ } catch {
811
+ return void 0;
812
+ }
813
+ }
814
+ function createPlaywrightSessionObserver(config) {
815
+ if (!config || typeof config.pageForSession !== "function") {
816
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "pageForSession is required.");
817
+ }
818
+ const now = config.now ?? (() => /* @__PURE__ */ new Date());
819
+ return Object.freeze({
820
+ async observe(browserSessionIdValue) {
821
+ const browserSessionId = cdpSessionId(browserSessionIdValue);
822
+ const page = await config.pageForSession(browserSessionId);
823
+ if (!page || page.isClosed()) {
824
+ throw verificationError(
825
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
826
+ "The owned browser page is unavailable."
827
+ );
828
+ }
829
+ const beforeUrl = page.url();
830
+ const pageOrigin = beforeUrl === "about:blank" ? null : observedHttpsOrigin(beforeUrl);
831
+ if (beforeUrl !== "about:blank" && !pageOrigin) {
832
+ throw verificationError(
833
+ "BROWSER_CHECKOUT_TARGET_INVALID",
834
+ "The owned browser page is not at a trusted HTTPS origin."
835
+ );
836
+ }
837
+ let title;
838
+ let rawTargets;
839
+ try {
840
+ [title, rawTargets] = await Promise.all([
841
+ page.title(),
842
+ page.locator(SESSION_OBSERVATION_SELECTOR).evaluateAll((nodes, limit) => {
843
+ const candidates = [];
844
+ for (const [index, node] of nodes.entries()) {
845
+ const element = node;
846
+ const style = window.getComputedStyle(element);
847
+ if (element.hidden || style.display === "none" || style.visibility === "hidden" || element.getClientRects().length === 0) {
848
+ continue;
849
+ }
850
+ const tagName = element.tagName.toUpperCase();
851
+ const declaredRole = element.getAttribute("role")?.toLowerCase();
852
+ const role = tagName === "A" || declaredRole === "link" ? "LINK" : "BUTTON";
853
+ const inputValue = tagName === "INPUT" ? element.value : void 0;
854
+ const label = element.getAttribute("aria-label") || element.getAttribute("title") || inputValue || element.textContent || "";
855
+ const disabled = (tagName === "BUTTON" || tagName === "INPUT" ? element.disabled : false) || element.getAttribute("aria-disabled") === "true";
856
+ const href = tagName === "A" ? element.href : void 0;
857
+ candidates.push({
858
+ index,
859
+ role,
860
+ label,
861
+ disabled,
862
+ ...href ? { href } : {}
863
+ });
864
+ if (candidates.length > limit) break;
865
+ }
866
+ return candidates;
867
+ }, SESSION_OBSERVATION_TARGET_LIMIT)
868
+ ]);
869
+ } catch {
870
+ throw verificationError(
871
+ "BROWSER_CHECKOUT_TARGET_INVALID",
872
+ "The owned browser page could not be observed."
873
+ );
874
+ }
875
+ const currentPage = await config.pageForSession(browserSessionId);
876
+ if (!currentPage || currentPage !== page || page.isClosed() || page.url() !== beforeUrl) {
877
+ throw verificationError(
878
+ "BROWSER_CHECKOUT_TARGET_CHANGED",
879
+ "The owned browser page changed while it was being observed."
880
+ );
881
+ }
882
+ const truncated = rawTargets.length > SESSION_OBSERVATION_TARGET_LIMIT;
883
+ const targets = rawTargets.slice(0, SESSION_OBSERVATION_TARGET_LIMIT).map((target) => {
884
+ const destinationOrigin = observedHttpsOrigin(target.href);
885
+ return Object.freeze({
886
+ locator: `:nth-match(${SESSION_OBSERVATION_SELECTOR}, ${target.index + 1})`,
887
+ role: target.role,
888
+ label: boundedObservationText(target.label, SESSION_OBSERVATION_LABEL_LIMIT),
889
+ disabled: target.disabled,
890
+ ...destinationOrigin ? { destinationOrigin } : {}
891
+ });
892
+ });
893
+ return Object.freeze({
894
+ browserSessionId,
895
+ pageOrigin: pageOrigin ?? null,
896
+ title: boundedObservationText(title, SESSION_OBSERVATION_TITLE_LIMIT),
897
+ targets: Object.freeze(targets),
898
+ truncated,
899
+ observedAt: now().toISOString()
900
+ });
901
+ }
902
+ });
903
+ }
904
+ function createGuardedPlaywrightCheckoutRuntimeHost(config) {
905
+ if (!config || typeof config !== "object") {
906
+ throw verificationError(
907
+ "BROWSER_CHECKOUT_TARGET_INVALID",
908
+ "The guarded Playwright runtime host configuration is required."
909
+ );
910
+ }
911
+ const browserSessions = createPlaywrightCdpSessionRegistry(config.browserSessions);
912
+ const observer = createPlaywrightSessionObserver({
913
+ pageForSession: browserSessions.pageForSession,
914
+ now: config.now
915
+ });
916
+ const targetStore = createEncryptedPlaywrightCheckoutTargetStore(config.targetStore);
917
+ const runtime = createGuardedPlaywrightCheckoutRuntime({
918
+ conto: config.conto,
919
+ pageForSession: browserSessions.pageForSession,
920
+ targetStore,
921
+ dispatchAttemptFor: config.dispatchAttemptFor,
922
+ clickTimeoutMs: config.clickTimeoutMs,
923
+ popupTimeoutMs: config.popupTimeoutMs,
924
+ targetTtlMs: config.targetTtlMs,
925
+ now: config.now
926
+ });
927
+ const handler = createGuardedPlaywrightCheckoutHttpHandler({
928
+ runtime,
929
+ token: config.token,
930
+ maxBodyBytes: config.maxBodyBytes
931
+ });
932
+ return Object.freeze({
933
+ handler,
934
+ observeSession: observer.observe,
935
+ closeSession: browserSessions.closeSession,
936
+ close: browserSessions.close
937
+ });
938
+ }
939
+ var GuardedCheckoutHttpInputError = class extends Error {
940
+ constructor(status, code, message) {
941
+ super(message);
942
+ this.status = status;
943
+ this.code = code;
944
+ this.name = "GuardedCheckoutHttpInputError";
945
+ }
946
+ };
947
+ var HTTP_ACTION_KINDS = [
948
+ "INITIATE_CHECKOUT",
949
+ "OPEN_LINK",
950
+ "CONFIRM_PAYMENT",
951
+ "PLACE_ORDER"
952
+ ];
953
+ var HTTP_CHECKOUT_METHODS = ["LINK", "CARD", "SHOP_PAY", "PAYPAL"];
954
+ var HTTP_TOTAL_TYPES = [
955
+ "subtotal",
956
+ "tax",
957
+ "total",
958
+ "items_base_amount",
959
+ "items_discount",
960
+ "discount",
961
+ "fulfillment",
962
+ "shipping",
963
+ "fee",
964
+ "gift_wrap",
965
+ "tip",
966
+ "store_credit"
967
+ ];
968
+ var SAFE_RUNTIME_ERROR_CODES = /* @__PURE__ */ new Set([
969
+ "ACTION_REQUIRED",
970
+ "AUTH_FAILED",
971
+ "BROWSER_ACTION_DISPATCH_INDETERMINATE",
972
+ "BROWSER_ACTION_FINALIZATION_PENDING",
973
+ "BROWSER_ACTION_OBSERVATION_INVALID",
974
+ "BROWSER_CHECKOUT_TARGET_CHANGED",
975
+ "BROWSER_CHECKOUT_TARGET_DISABLED",
976
+ "BROWSER_CHECKOUT_TARGET_INVALID",
977
+ "BROWSER_CHECKOUT_TARGET_NOT_FOUND",
978
+ "CHECKOUT_UNAVAILABLE",
979
+ "COMMERCE_EXECUTION_NOT_READY",
980
+ "CONFLICT",
981
+ "INSUFFICIENT_SCOPE",
982
+ "INVALID_BROWSER_CHECKOUT",
983
+ "INVALID_REQUEST",
984
+ "POLICY_DENIED",
985
+ "RATE_LIMITED",
986
+ "REQUIRES_APPROVAL",
987
+ "SERVER_ERROR"
988
+ ]);
989
+ function httpRecord(value, field) {
990
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
991
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
992
+ }
993
+ return value;
994
+ }
995
+ function httpClosedObject(value, field, allowedKeys) {
996
+ const record = httpRecord(value, field);
997
+ if (Object.keys(record).some((key) => !allowedKeys.includes(key))) {
998
+ throw new GuardedCheckoutHttpInputError(
999
+ 400,
1000
+ "INVALID_BROWSER_CHECKOUT",
1001
+ `${field} contains an unsupported field`
1002
+ );
1003
+ }
1004
+ return record;
1005
+ }
1006
+ function httpText(value, field, maxLength) {
1007
+ if (typeof value !== "string") {
1008
+ throw new GuardedCheckoutHttpInputError(
1009
+ 400,
1010
+ "INVALID_BROWSER_CHECKOUT",
1011
+ `${field} is required`
1012
+ );
1013
+ }
1014
+ const normalized = value.trim();
1015
+ if (!normalized || normalized.length > maxLength) {
1016
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
1017
+ }
1018
+ return normalized;
1019
+ }
1020
+ function httpInteger(value, field, minimum) {
1021
+ if (!Number.isSafeInteger(value) || value < minimum) {
1022
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
1023
+ }
1024
+ return value;
1025
+ }
1026
+ function httpEnum(value, field, choices) {
1027
+ if (typeof value !== "string" || !choices.includes(value)) {
1028
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
1029
+ }
1030
+ return value;
1031
+ }
1032
+ function httpHttpsUrl(value, field) {
1033
+ const text = httpText(value, field, 2048);
1034
+ try {
1035
+ const url = new URL(text);
1036
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("unsafe URL");
1037
+ return url.toString();
1038
+ } catch {
1039
+ throw new GuardedCheckoutHttpInputError(
1040
+ 400,
1041
+ "INVALID_BROWSER_CHECKOUT",
1042
+ `${field} must use HTTPS without credentials`
1043
+ );
1044
+ }
1045
+ }
1046
+ function httpArray(value, field, maxItems) {
1047
+ if (!Array.isArray(value) || value.length === 0 || value.length > maxItems) {
1048
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
1049
+ }
1050
+ return value;
1051
+ }
1052
+ function parseGuardedCheckoutHttpInput(value) {
1053
+ const input = httpClosedObject(value, "body", [
1054
+ "idempotencyKey",
1055
+ "browserSessionId",
1056
+ "locator",
1057
+ "actionKind",
1058
+ "checkoutMethod",
1059
+ "amountMinor",
1060
+ "currency",
1061
+ "purpose",
1062
+ "category",
1063
+ "counterparty",
1064
+ "cart",
1065
+ "providerPresentation"
1066
+ ]);
1067
+ const counterparty = httpClosedObject(input.counterparty, "counterparty", [
1068
+ "canonicalId",
1069
+ "displayName",
1070
+ "checkoutUrl"
1071
+ ]);
1072
+ const cart = httpClosedObject(input.cart, "cart", ["lineItems", "totals", "fulfillment"]);
1073
+ const presentation = httpClosedObject(input.providerPresentation, "providerPresentation", [
1074
+ "approvalSummary",
1075
+ "lineItemLabels"
1076
+ ]);
1077
+ const amountMinor = httpInteger(input.amountMinor, "amountMinor", 1);
1078
+ const lineItems = httpArray(cart.lineItems, "cart.lineItems", 100).map((value2, index) => {
1079
+ const item = httpClosedObject(value2, `cart.lineItems[${index}]`, [
1080
+ "name",
1081
+ "quantity",
1082
+ "unitAmountMinor",
1083
+ "description",
1084
+ "sku",
1085
+ "productUrl"
1086
+ ]);
1087
+ return {
1088
+ name: httpText(item.name, `cart.lineItems[${index}].name`, 500),
1089
+ quantity: httpInteger(item.quantity, `cart.lineItems[${index}].quantity`, 1),
1090
+ unitAmountMinor: httpInteger(
1091
+ item.unitAmountMinor,
1092
+ `cart.lineItems[${index}].unitAmountMinor`,
1093
+ 0
1094
+ ),
1095
+ ...item.description !== void 0 ? { description: httpText(item.description, `cart.lineItems[${index}].description`, 500) } : {},
1096
+ ...item.sku !== void 0 ? { sku: httpText(item.sku, `cart.lineItems[${index}].sku`, 200) } : {},
1097
+ ...item.productUrl !== void 0 ? { productUrl: httpHttpsUrl(item.productUrl, `cart.lineItems[${index}].productUrl`) } : {}
1098
+ };
1099
+ });
1100
+ const totals = httpArray(cart.totals, "cart.totals", 25).map((value2, index) => {
1101
+ const total = httpClosedObject(value2, `cart.totals[${index}]`, [
1102
+ "type",
1103
+ "label",
1104
+ "amountMinor"
1105
+ ]);
1106
+ return {
1107
+ type: httpEnum(total.type, `cart.totals[${index}].type`, HTTP_TOTAL_TYPES),
1108
+ label: httpText(total.label, `cart.totals[${index}].label`, 200),
1109
+ amountMinor: httpInteger(total.amountMinor, `cart.totals[${index}].amountMinor`, 0)
1110
+ };
1111
+ });
1112
+ const finalTotals = totals.filter((total) => total.type === "total");
1113
+ if (finalTotals.length !== 1 || finalTotals[0].amountMinor !== amountMinor) {
1114
+ throw new GuardedCheckoutHttpInputError(
1115
+ 400,
1116
+ "INVALID_BROWSER_CHECKOUT",
1117
+ "cart total must match amountMinor"
1118
+ );
1119
+ }
1120
+ let fulfillment;
1121
+ if (cart.fulfillment !== void 0) {
1122
+ const source = httpClosedObject(cart.fulfillment, "cart.fulfillment", [
1123
+ "recipient",
1124
+ "addressLine1",
1125
+ "addressLine2",
1126
+ "locality",
1127
+ "region",
1128
+ "postalCode",
1129
+ "countryCode"
1130
+ ]);
1131
+ const fields = [
1132
+ "recipient",
1133
+ "addressLine1",
1134
+ "addressLine2",
1135
+ "locality",
1136
+ "region",
1137
+ "postalCode",
1138
+ "countryCode"
1139
+ ];
1140
+ const values = Object.fromEntries(
1141
+ fields.filter((field) => source[field] !== void 0).map((field) => [field, httpText(source[field], `cart.fulfillment.${field}`, 500)])
1142
+ );
1143
+ if (Object.keys(values).length === 0) {
1144
+ throw new GuardedCheckoutHttpInputError(
1145
+ 400,
1146
+ "INVALID_BROWSER_CHECKOUT",
1147
+ "cart.fulfillment cannot be empty"
1148
+ );
1149
+ }
1150
+ if (values.countryCode && !/^[A-Za-z]{2}$/.test(values.countryCode)) {
1151
+ throw new GuardedCheckoutHttpInputError(
1152
+ 400,
1153
+ "INVALID_BROWSER_CHECKOUT",
1154
+ "cart.fulfillment.countryCode is invalid"
1155
+ );
1156
+ }
1157
+ fulfillment = {
1158
+ ...values,
1159
+ ...values.countryCode ? { countryCode: values.countryCode.toUpperCase() } : {}
1160
+ };
1161
+ }
1162
+ const labels = httpArray(
1163
+ presentation.lineItemLabels,
1164
+ "providerPresentation.lineItemLabels",
1165
+ 100
1166
+ ).map((label, index) => httpText(label, `providerPresentation.lineItemLabels[${index}]`, 500));
1167
+ return {
1168
+ idempotencyKey: httpText(input.idempotencyKey, "idempotencyKey", 255),
1169
+ browserSessionId: httpText(input.browserSessionId, "browserSessionId", 255),
1170
+ locator: httpText(input.locator, "locator", 1024),
1171
+ actionKind: httpEnum(input.actionKind, "actionKind", HTTP_ACTION_KINDS),
1172
+ checkoutMethod: httpEnum(input.checkoutMethod, "checkoutMethod", HTTP_CHECKOUT_METHODS),
1173
+ amountMinor,
1174
+ currency: httpEnum(input.currency, "currency", ["USD"]),
1175
+ purpose: httpText(input.purpose, "purpose", 500),
1176
+ ...input.category !== void 0 ? { category: httpText(input.category, "category", 100) } : {},
1177
+ counterparty: {
1178
+ canonicalId: httpText(counterparty.canonicalId, "counterparty.canonicalId", 255),
1179
+ displayName: httpText(counterparty.displayName, "counterparty.displayName", 200),
1180
+ checkoutUrl: httpHttpsUrl(counterparty.checkoutUrl, "counterparty.checkoutUrl")
1181
+ },
1182
+ cart: { lineItems, totals, ...fulfillment ? { fulfillment } : {} },
1183
+ providerPresentation: {
1184
+ approvalSummary: httpText(
1185
+ presentation.approvalSummary,
1186
+ "providerPresentation.approvalSummary",
1187
+ 500
1188
+ ),
1189
+ lineItemLabels: labels
1190
+ }
1191
+ };
1192
+ }
1193
+ function httpJson(status, body, headers) {
1194
+ return new Response(JSON.stringify(body), {
1195
+ status,
1196
+ headers: {
1197
+ "cache-control": "no-store",
1198
+ "content-type": "application/json; charset=utf-8",
1199
+ ...headers
1200
+ }
1201
+ });
1202
+ }
1203
+ async function readBoundedHttpBody(request, maxBodyBytes) {
1204
+ if (!request.body) return "";
1205
+ const reader = request.body.getReader();
1206
+ const chunks = [];
1207
+ let length = 0;
1208
+ try {
1209
+ while (true) {
1210
+ const { done, value } = await reader.read();
1211
+ if (done) break;
1212
+ length += value.byteLength;
1213
+ if (length > maxBodyBytes) {
1214
+ try {
1215
+ await reader.cancel();
1216
+ } catch {
1217
+ }
1218
+ throw new GuardedCheckoutHttpInputError(413, "INVALID_REQUEST", "body is too large");
1219
+ }
1220
+ chunks.push(value);
1221
+ }
1222
+ } finally {
1223
+ reader.releaseLock();
1224
+ }
1225
+ const bytes = new Uint8Array(length);
1226
+ let offset = 0;
1227
+ for (const chunk of chunks) {
1228
+ bytes.set(chunk, offset);
1229
+ offset += chunk.byteLength;
1230
+ }
1231
+ try {
1232
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1233
+ } catch {
1234
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_REQUEST", "body is not valid UTF-8");
1235
+ }
1236
+ }
1237
+ function safeRuntimeIdentifier(value) {
1238
+ return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9._:@/-]+$/.test(value) ? value : void 0;
1239
+ }
1240
+ function safeRuntimeError(error) {
1241
+ if (error instanceof GuardedCheckoutHttpInputError) {
1242
+ return httpJson(error.status, { code: error.code });
1243
+ }
1244
+ const record = error && typeof error === "object" ? error : void 0;
1245
+ const candidateCode = record?.code;
1246
+ const code = typeof candidateCode === "string" && SAFE_RUNTIME_ERROR_CODES.has(candidateCode) ? candidateCode : "SERVER_ERROR";
1247
+ let status = typeof record?.status === "number" ? record.status : code.startsWith("BROWSER_") ? 409 : 500;
1248
+ if (!Number.isInteger(status) || status < 400 || status > 599) status = 409;
1249
+ if (code === "SERVER_ERROR") status = 500;
1250
+ const requestId = safeRuntimeIdentifier(record?.requestId);
1251
+ return httpJson(status, { code, ...requestId ? { requestId } : {} });
1252
+ }
1253
+ function validRuntimeToken(value) {
1254
+ return value.length >= 32 && value.length <= 1024 && /^[\x21-\x7e]+$/.test(value) && !value.startsWith("conto_");
1255
+ }
1256
+ function authorizedRuntimeRequest(request, token) {
1257
+ const header = request.headers.get("authorization");
1258
+ if (!header?.startsWith("Bearer ")) return false;
1259
+ const supplied = Buffer.from(header.slice(7));
1260
+ const expected = Buffer.from(token);
1261
+ return supplied.length === expected.length && (0, import_node_crypto2.timingSafeEqual)(supplied, expected);
1262
+ }
1263
+ function projectedRuntimeResult(value, expected) {
1264
+ const result = httpRecord(value, "runtime result");
1265
+ const execution = httpRecord(result.execution, "runtime execution");
1266
+ const dispatched = httpRecord(result.result, "runtime dispatch result");
1267
+ const outcome = httpRecord(dispatched.browserOutcome, "runtime browser outcome");
1268
+ const evidence = httpRecord(execution.evidence, "runtime evidence");
1269
+ const executionId = safeRuntimeIdentifier(execution.id);
1270
+ const traceId = safeRuntimeIdentifier(evidence.traceId);
1271
+ if (!executionId) throw new Error("invalid runtime execution ID");
1272
+ const status = httpEnum(execution.status, "runtime execution status", [
1273
+ "READY",
1274
+ "PROCESSING",
1275
+ "NEEDS_RECONCILIATION"
1276
+ ]);
1277
+ const amountMinor = httpInteger(execution.amountMinor, "runtime execution amountMinor", 1);
1278
+ const currency = httpEnum(execution.currency, "runtime execution currency", ["USD"]);
1279
+ const checkoutMethod = httpEnum(
1280
+ execution.checkoutMethod,
1281
+ "runtime execution checkoutMethod",
1282
+ HTTP_CHECKOUT_METHODS
1283
+ );
1284
+ if (amountMinor !== expected.amountMinor || currency !== expected.currency || checkoutMethod !== expected.checkoutMethod) {
1285
+ throw new Error("runtime result does not match request");
1286
+ }
1287
+ const pageUrl = new URL(httpHttpsUrl(dispatched.pageUrl, "runtime result pageUrl")).origin;
1288
+ const observedAt = httpText(outcome.observedAt, "runtime result observedAt", 100);
1289
+ if (Number.isNaN(Date.parse(observedAt))) throw new Error("invalid runtime observedAt");
1290
+ return {
1291
+ execution: {
1292
+ id: executionId,
1293
+ status,
1294
+ amountMinor,
1295
+ currency,
1296
+ checkoutMethod,
1297
+ evidence: { ...traceId ? { traceId } : {} }
1298
+ },
1299
+ result: {
1300
+ pageUrl,
1301
+ browserOutcome: {
1302
+ kind: httpEnum(outcome.kind, "runtime result kind", [
1303
+ "NEW_PAGE",
1304
+ "PAGE_NAVIGATION",
1305
+ "SAME_PAGE"
1306
+ ]),
1307
+ observedAt,
1308
+ settlement: httpEnum(outcome.settlement, "runtime result settlement", ["UNVERIFIED"])
1309
+ }
1310
+ }
1311
+ };
1312
+ }
1313
+ function createGuardedPlaywrightCheckoutHttpHandler(config) {
1314
+ if (!config || !config.runtime || typeof config.runtime.discover !== "function" || typeof config.runtime.run !== "function") {
1315
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "runtime is required.");
1316
+ }
1317
+ if (!validRuntimeToken(config.token)) {
1318
+ throw verificationError(
1319
+ "BROWSER_CHECKOUT_TARGET_INVALID",
1320
+ "token must be a dedicated credential containing 32 to 1024 non-space characters."
1321
+ );
1322
+ }
1323
+ const maxBodyBytes = config.maxBodyBytes ?? DEFAULT_HTTP_BODY_LIMIT_BYTES;
1324
+ if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0 || maxBodyBytes > MAX_HTTP_BODY_LIMIT_BYTES) {
1325
+ throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "maxBodyBytes is invalid.");
1326
+ }
1327
+ const runtime = config.runtime;
1328
+ const token = config.token;
1329
+ return async (request) => {
1330
+ if (request.method !== "POST") {
1331
+ return httpJson(405, { code: "INVALID_REQUEST" }, { allow: "POST" });
1332
+ }
1333
+ if (!authorizedRuntimeRequest(request, token)) {
1334
+ return httpJson(401, { code: "AUTH_FAILED" }, { "www-authenticate": "Bearer" });
1335
+ }
1336
+ const mediaType = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
1337
+ if (mediaType !== "application/json") {
1338
+ return httpJson(415, { code: "INVALID_REQUEST" });
1339
+ }
1340
+ const declaredLength = Number(request.headers.get("content-length"));
1341
+ if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
1342
+ return httpJson(413, { code: "INVALID_REQUEST" });
1343
+ }
1344
+ try {
1345
+ const text = await readBoundedHttpBody(request, maxBodyBytes);
1346
+ let parsed;
1347
+ try {
1348
+ parsed = JSON.parse(text);
1349
+ } catch {
1350
+ throw new GuardedCheckoutHttpInputError(400, "INVALID_REQUEST", "body is invalid JSON");
1351
+ }
1352
+ const input = parseGuardedCheckoutHttpInput(parsed);
1353
+ const discovered = await runtime.discover({
1354
+ browserSessionId: input.browserSessionId,
1355
+ locator: input.locator,
1356
+ kind: input.actionKind,
1357
+ checkoutMethod: input.checkoutMethod
1358
+ });
1359
+ const result = await runtime.run({
1360
+ executionMethod: "BROWSER_CHECKOUT",
1361
+ idempotencyKey: input.idempotencyKey,
1362
+ browserSessionId: input.browserSessionId,
1363
+ amountMinor: input.amountMinor,
1364
+ currency: input.currency,
1365
+ purpose: input.purpose,
1366
+ ...input.category ? { category: input.category } : {},
1367
+ counterparty: input.counterparty,
1368
+ cart: input.cart,
1369
+ providerPresentation: input.providerPresentation,
1370
+ action: discovered.action
1371
+ });
1372
+ let output;
1373
+ try {
1374
+ output = projectedRuntimeResult(result, input);
1375
+ } catch {
1376
+ return httpJson(500, { code: "SERVER_ERROR" });
1377
+ }
1378
+ return httpJson(200, output);
1379
+ } catch (error) {
1380
+ return safeRuntimeError(error);
1381
+ }
1382
+ };
1383
+ }
1384
+ // Annotate the CommonJS export names for ESM import in node:
1385
+ 0 && (module.exports = {
1386
+ createEncryptedPlaywrightCheckoutTargetStore,
1387
+ createGuardedPlaywrightCheckoutHttpHandler,
1388
+ createGuardedPlaywrightCheckoutRuntime,
1389
+ createGuardedPlaywrightCheckoutRuntimeHost,
1390
+ createInMemoryPlaywrightCheckoutTargetStore,
1391
+ createPlaywrightCdpSessionRegistry,
1392
+ createPlaywrightCheckoutAdapter,
1393
+ createPlaywrightSessionObserver
1394
+ });