@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.
- package/README.md +357 -6
- package/dist/browserbase.d.mts +141 -0
- package/dist/browserbase.d.ts +141 -0
- package/dist/browserbase.js +1985 -0
- package/dist/browserbase.mjs +608 -0
- package/dist/checkout-verifier.d.mts +95 -0
- package/dist/checkout-verifier.d.ts +95 -0
- package/dist/checkout-verifier.js +400 -0
- package/dist/checkout-verifier.mjs +372 -0
- package/dist/chunk-B7F5QLE5.mjs +34 -0
- package/dist/chunk-HM4EG5MH.mjs +1256 -0
- package/dist/chunk-O5RAAB2U.mjs +123 -0
- package/dist/index.d.mts +1065 -14
- package/dist/index.d.ts +1065 -14
- package/dist/index.js +792 -59
- package/dist/index.mjs +676 -85
- package/dist/playwright.d.mts +255 -0
- package/dist/playwright.d.ts +255 -0
- package/dist/playwright.js +1394 -0
- package/dist/playwright.mjs +22 -0
- package/package.json +31 -6
|
@@ -0,0 +1,1985 @@
|
|
|
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/browserbase.ts
|
|
21
|
+
var browserbase_exports = {};
|
|
22
|
+
__export(browserbase_exports, {
|
|
23
|
+
BROWSERBASE_CONNECT_ORIGINS: () => BROWSERBASE_CONNECT_ORIGINS,
|
|
24
|
+
BrowserbaseSessionError: () => BrowserbaseSessionError,
|
|
25
|
+
createBrowserbaseGuardedPlaywrightCheckoutRuntimeHost: () => createBrowserbaseGuardedPlaywrightCheckoutRuntimeHost,
|
|
26
|
+
createBrowserbaseSessionHandoffAssertion: () => createBrowserbaseSessionHandoffAssertion
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(browserbase_exports);
|
|
29
|
+
var import_node_crypto3 = require("crypto");
|
|
30
|
+
|
|
31
|
+
// src/playwright.ts
|
|
32
|
+
var import_node_crypto2 = require("crypto");
|
|
33
|
+
|
|
34
|
+
// src/browser-checkout.ts
|
|
35
|
+
var import_node_crypto = require("crypto");
|
|
36
|
+
var MAX_URL_LENGTH = 2048;
|
|
37
|
+
var MAX_LOCATOR_LENGTH = 1024;
|
|
38
|
+
var MAX_TEXT_LENGTH = 500;
|
|
39
|
+
var BrowserCheckoutTargetVerificationError = class extends Error {
|
|
40
|
+
constructor(code, message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.name = "BrowserCheckoutTargetVerificationError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function invalid(message) {
|
|
47
|
+
throw new BrowserCheckoutTargetVerificationError("BROWSER_CHECKOUT_TARGET_INVALID", message);
|
|
48
|
+
}
|
|
49
|
+
function canonicalHttpsUrl(value, field) {
|
|
50
|
+
if (typeof value !== "string" || !value || value.length > MAX_URL_LENGTH) {
|
|
51
|
+
invalid(`${field} is invalid.`);
|
|
52
|
+
}
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(value);
|
|
56
|
+
} catch {
|
|
57
|
+
invalid(`${field} is invalid.`);
|
|
58
|
+
}
|
|
59
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
60
|
+
invalid(`${field} must use HTTPS without credentials.`);
|
|
61
|
+
}
|
|
62
|
+
url.hostname = url.hostname.toLowerCase();
|
|
63
|
+
url.hash = "";
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
function canonicalText(value, field, options = {}) {
|
|
67
|
+
if (value === void 0) {
|
|
68
|
+
if (options.required) invalid(`${field} is required.`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value !== "string") invalid(`${field} is invalid.`);
|
|
72
|
+
const trimmed = value.trim();
|
|
73
|
+
const normalized = options.collapseWhitespace === false ? trimmed : trimmed.replace(/\s+/g, " ");
|
|
74
|
+
if (options.required && !normalized || normalized.length > (options.maxLength ?? MAX_TEXT_LENGTH)) {
|
|
75
|
+
invalid(`${field} is invalid.`);
|
|
76
|
+
}
|
|
77
|
+
return normalized || null;
|
|
78
|
+
}
|
|
79
|
+
function canonicalizeBrowserCheckoutTarget(snapshot) {
|
|
80
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
81
|
+
invalid("The browser checkout target is invalid.");
|
|
82
|
+
}
|
|
83
|
+
if (typeof snapshot.disabled !== "boolean") {
|
|
84
|
+
invalid("target.disabled is invalid.");
|
|
85
|
+
}
|
|
86
|
+
const tagName = canonicalText(snapshot.tagName, "target.tagName", {
|
|
87
|
+
required: true,
|
|
88
|
+
maxLength: 80
|
|
89
|
+
});
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
pageUrl: canonicalHttpsUrl(snapshot.pageUrl, "target.pageUrl"),
|
|
92
|
+
locator: canonicalText(snapshot.locator, "target.locator", {
|
|
93
|
+
collapseWhitespace: false,
|
|
94
|
+
required: true,
|
|
95
|
+
maxLength: MAX_LOCATOR_LENGTH
|
|
96
|
+
}) ?? "",
|
|
97
|
+
tagName: (tagName ?? "").toLowerCase(),
|
|
98
|
+
role: canonicalText(snapshot.role, "target.role", { maxLength: 80 })?.toLowerCase() ?? null,
|
|
99
|
+
accessibleName: canonicalText(snapshot.accessibleName, "target.accessibleName"),
|
|
100
|
+
targetUrl: snapshot.targetUrl ? canonicalHttpsUrl(snapshot.targetUrl, "target.targetUrl") : null,
|
|
101
|
+
formAction: snapshot.formAction ? canonicalHttpsUrl(snapshot.formAction, "target.formAction") : null,
|
|
102
|
+
opensNewPage: snapshot.opensNewPage === true,
|
|
103
|
+
disabled: snapshot.disabled
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function fingerprintBrowserCheckoutTarget(snapshot) {
|
|
107
|
+
const canonical = canonicalizeBrowserCheckoutTarget(snapshot);
|
|
108
|
+
const digest = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
109
|
+
return `browser_target_sha256_${digest}`;
|
|
110
|
+
}
|
|
111
|
+
function createVerifiedBrowserCheckoutDriver(resolver) {
|
|
112
|
+
if (!resolver || typeof resolver.resolve !== "function") {
|
|
113
|
+
invalid("A browser checkout target resolver is required.");
|
|
114
|
+
}
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
async dispatch(input) {
|
|
117
|
+
const target = await resolver.resolve(input);
|
|
118
|
+
if (!target) {
|
|
119
|
+
throw new BrowserCheckoutTargetVerificationError(
|
|
120
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
121
|
+
"The approved browser checkout target was not found."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (typeof target.dispatch !== "function") {
|
|
125
|
+
invalid("The resolved browser checkout target cannot be dispatched.");
|
|
126
|
+
}
|
|
127
|
+
const canonical = canonicalizeBrowserCheckoutTarget(target.snapshot);
|
|
128
|
+
const approvedPageUrl = canonicalHttpsUrl(input.action.pageUrl, "action.pageUrl");
|
|
129
|
+
const approvedTargetUrl = input.action.targetUrl ? canonicalHttpsUrl(input.action.targetUrl, "action.targetUrl") : null;
|
|
130
|
+
const observedDestinations = [canonical.targetUrl, canonical.formAction].filter(
|
|
131
|
+
(value) => value !== null
|
|
132
|
+
);
|
|
133
|
+
const destinationsAreBound = approvedTargetUrl === null ? observedDestinations.length === 0 : observedDestinations.length > 0 && observedDestinations.every((value) => value === approvedTargetUrl);
|
|
134
|
+
if (canonical.pageUrl !== approvedPageUrl || !destinationsAreBound || fingerprintBrowserCheckoutTarget(target.snapshot) !== input.action.targetFingerprint.trim()) {
|
|
135
|
+
throw new BrowserCheckoutTargetVerificationError(
|
|
136
|
+
"BROWSER_CHECKOUT_TARGET_CHANGED",
|
|
137
|
+
"The browser checkout target changed after Conto authorization."
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (canonical.disabled) {
|
|
141
|
+
throw new BrowserCheckoutTargetVerificationError(
|
|
142
|
+
"BROWSER_CHECKOUT_TARGET_DISABLED",
|
|
143
|
+
"The approved browser checkout target is disabled."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return target.dispatch();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/playwright.ts
|
|
152
|
+
var DEFAULT_CLICK_TIMEOUT_MS = 3e4;
|
|
153
|
+
var MAX_CLICK_TIMEOUT_MS = 12e4;
|
|
154
|
+
var DEFAULT_POPUP_TIMEOUT_MS = 5e3;
|
|
155
|
+
var MAX_POPUP_TIMEOUT_MS = 3e4;
|
|
156
|
+
var DEFAULT_TARGET_TTL_MS = 10 * 6e4;
|
|
157
|
+
var MAX_TARGET_TTL_MS = 60 * 6e4;
|
|
158
|
+
var TARGET_STORE_ENVELOPE_VERSION = 1;
|
|
159
|
+
var DEFAULT_HTTP_BODY_LIMIT_BYTES = 64 * 1024;
|
|
160
|
+
var MAX_HTTP_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
161
|
+
var DEFAULT_CDP_SESSION_LIMIT = 100;
|
|
162
|
+
var MAX_CDP_SESSION_LIMIT = 1e3;
|
|
163
|
+
var SESSION_OBSERVATION_TARGET_LIMIT = 50;
|
|
164
|
+
var SESSION_OBSERVATION_LABEL_LIMIT = 160;
|
|
165
|
+
var SESSION_OBSERVATION_TITLE_LIMIT = 240;
|
|
166
|
+
var SESSION_OBSERVATION_SELECTOR = 'a[href], button, input[type="submit"], input[type="button"], [role="button"], [role="link"]';
|
|
167
|
+
function verificationError(code, message) {
|
|
168
|
+
return new BrowserCheckoutTargetVerificationError(code, message);
|
|
169
|
+
}
|
|
170
|
+
function requiredText(value, field, maxLength) {
|
|
171
|
+
if (typeof value !== "string") {
|
|
172
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", `${field} is required.`);
|
|
173
|
+
}
|
|
174
|
+
const normalized = value.trim();
|
|
175
|
+
if (!normalized || normalized.length > maxLength) {
|
|
176
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", `${field} is invalid.`);
|
|
177
|
+
}
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
function clickTimeout(value) {
|
|
181
|
+
const resolved = value ?? DEFAULT_CLICK_TIMEOUT_MS;
|
|
182
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_CLICK_TIMEOUT_MS) {
|
|
183
|
+
throw verificationError(
|
|
184
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
185
|
+
"clickTimeoutMs must be a positive integer no greater than 120000."
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
function targetTtl(value) {
|
|
191
|
+
const resolved = value ?? DEFAULT_TARGET_TTL_MS;
|
|
192
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_TARGET_TTL_MS) {
|
|
193
|
+
throw verificationError(
|
|
194
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
195
|
+
"targetTtlMs must be a positive integer no greater than 3600000."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
function popupTimeout(value) {
|
|
201
|
+
const resolved = value ?? DEFAULT_POPUP_TIMEOUT_MS;
|
|
202
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_POPUP_TIMEOUT_MS) {
|
|
203
|
+
throw verificationError(
|
|
204
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
205
|
+
"popupTimeoutMs must be a positive integer no greater than 30000."
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return resolved;
|
|
209
|
+
}
|
|
210
|
+
function currentDate(now) {
|
|
211
|
+
const value = now();
|
|
212
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
213
|
+
throw verificationError(
|
|
214
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
215
|
+
"The Playwright checkout clock returned an invalid date."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
function observedHttpsPageUrl(value) {
|
|
221
|
+
if (typeof value !== "string" || !value || value.length > 2048) {
|
|
222
|
+
throw verificationError(
|
|
223
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
224
|
+
"The observed checkout page URL is invalid."
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const url = new URL(value);
|
|
229
|
+
if (url.protocol !== "https:" || url.username || url.password) throw new Error("unsafe URL");
|
|
230
|
+
return value;
|
|
231
|
+
} catch {
|
|
232
|
+
throw verificationError(
|
|
233
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
234
|
+
"The observed checkout page URL must use HTTPS without credentials."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function targetStoreKey(input) {
|
|
239
|
+
return `${input.browserSessionId}\0${input.targetFingerprint}`;
|
|
240
|
+
}
|
|
241
|
+
function createInMemoryPlaywrightCheckoutTargetStore() {
|
|
242
|
+
const registrations = /* @__PURE__ */ new Map();
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
async put(registration) {
|
|
245
|
+
registrations.set(targetStoreKey(registration), Object.freeze({ ...registration }));
|
|
246
|
+
},
|
|
247
|
+
async get(input) {
|
|
248
|
+
return registrations.get(targetStoreKey(input)) ?? null;
|
|
249
|
+
},
|
|
250
|
+
async delete(input) {
|
|
251
|
+
registrations.delete(targetStoreKey(input));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function encryptedStoreKey(namespace, input) {
|
|
256
|
+
const digest = (0, import_node_crypto2.createHash)("sha256").update(namespace).update("\0").update(input.browserSessionId).update("\0").update(input.targetFingerprint).digest("hex");
|
|
257
|
+
return `${namespace}:${digest}`;
|
|
258
|
+
}
|
|
259
|
+
function validRegistration(value, expected) {
|
|
260
|
+
if (!value || typeof value !== "object") return false;
|
|
261
|
+
const registration = value;
|
|
262
|
+
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);
|
|
263
|
+
}
|
|
264
|
+
function createEncryptedPlaywrightCheckoutTargetStore(config) {
|
|
265
|
+
if (!config || !config.blobStore || typeof config.blobStore.put !== "function" || typeof config.blobStore.get !== "function" || typeof config.blobStore.delete !== "function") {
|
|
266
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "blobStore is required.");
|
|
267
|
+
}
|
|
268
|
+
const key = Buffer.from(config.encryptionKey ?? []);
|
|
269
|
+
if (key.length !== 32) {
|
|
270
|
+
throw verificationError(
|
|
271
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
272
|
+
"encryptionKey must contain exactly 32 bytes."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const namespace = requiredText(
|
|
276
|
+
config.namespace ?? "conto:playwright-target:v1",
|
|
277
|
+
"namespace",
|
|
278
|
+
120
|
|
279
|
+
);
|
|
280
|
+
const now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
281
|
+
return Object.freeze({
|
|
282
|
+
async put(registration) {
|
|
283
|
+
if (!validRegistration(registration)) {
|
|
284
|
+
throw verificationError(
|
|
285
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
286
|
+
"The Playwright checkout target registration is invalid."
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (Date.parse(registration.expiresAt) <= currentDate(now).getTime()) {
|
|
290
|
+
throw verificationError(
|
|
291
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
292
|
+
"The Playwright checkout target registration is already expired."
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const storageKey = encryptedStoreKey(namespace, registration);
|
|
296
|
+
const iv = (0, import_node_crypto2.randomBytes)(12);
|
|
297
|
+
const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", key, iv);
|
|
298
|
+
cipher.setAAD(Buffer.from(storageKey));
|
|
299
|
+
const ciphertext = Buffer.concat([
|
|
300
|
+
cipher.update(JSON.stringify(registration), "utf8"),
|
|
301
|
+
cipher.final()
|
|
302
|
+
]);
|
|
303
|
+
const envelope = JSON.stringify({
|
|
304
|
+
version: TARGET_STORE_ENVELOPE_VERSION,
|
|
305
|
+
iv: iv.toString("base64url"),
|
|
306
|
+
ciphertext: ciphertext.toString("base64url"),
|
|
307
|
+
tag: cipher.getAuthTag().toString("base64url")
|
|
308
|
+
});
|
|
309
|
+
await config.blobStore.put({
|
|
310
|
+
key: storageKey,
|
|
311
|
+
value: envelope,
|
|
312
|
+
expiresAt: registration.expiresAt
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
async get(input) {
|
|
316
|
+
const storageKey = encryptedStoreKey(namespace, input);
|
|
317
|
+
const envelopeText = await config.blobStore.get(storageKey);
|
|
318
|
+
if (envelopeText === null) return null;
|
|
319
|
+
try {
|
|
320
|
+
const envelope = JSON.parse(envelopeText);
|
|
321
|
+
if (envelope.version !== TARGET_STORE_ENVELOPE_VERSION || typeof envelope.iv !== "string" || typeof envelope.ciphertext !== "string" || typeof envelope.tag !== "string") {
|
|
322
|
+
throw new Error("invalid envelope");
|
|
323
|
+
}
|
|
324
|
+
const decipher = (0, import_node_crypto2.createDecipheriv)(
|
|
325
|
+
"aes-256-gcm",
|
|
326
|
+
key,
|
|
327
|
+
Buffer.from(envelope.iv, "base64url")
|
|
328
|
+
);
|
|
329
|
+
decipher.setAAD(Buffer.from(storageKey));
|
|
330
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
|
|
331
|
+
const plaintext = Buffer.concat([
|
|
332
|
+
decipher.update(Buffer.from(envelope.ciphertext, "base64url")),
|
|
333
|
+
decipher.final()
|
|
334
|
+
]).toString("utf8");
|
|
335
|
+
const registration = JSON.parse(plaintext);
|
|
336
|
+
if (!validRegistration(registration, input)) throw new Error("invalid registration");
|
|
337
|
+
if (Date.parse(registration.expiresAt) <= currentDate(now).getTime()) {
|
|
338
|
+
await config.blobStore.delete(storageKey);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return Object.freeze({ ...registration });
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error instanceof BrowserCheckoutTargetVerificationError) throw error;
|
|
344
|
+
throw verificationError(
|
|
345
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
346
|
+
"The encrypted Playwright checkout target registration cannot be verified."
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
async delete(input) {
|
|
351
|
+
await config.blobStore.delete(encryptedStoreKey(namespace, input));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async function exactElementHandle(page, locatorExpression) {
|
|
356
|
+
const locator = page.locator(locatorExpression);
|
|
357
|
+
const count = await locator.count();
|
|
358
|
+
if (count === 0) {
|
|
359
|
+
throw verificationError(
|
|
360
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
361
|
+
"The Playwright checkout target was not found."
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (count !== 1) {
|
|
365
|
+
throw verificationError(
|
|
366
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
367
|
+
"The Playwright checkout locator must resolve to exactly one element."
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const handle = await locator.elementHandle();
|
|
371
|
+
if (!handle) {
|
|
372
|
+
throw verificationError(
|
|
373
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
374
|
+
"The Playwright checkout target became unavailable."
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return handle;
|
|
378
|
+
}
|
|
379
|
+
async function snapshotPlaywrightTarget(input) {
|
|
380
|
+
const handle = input.handle;
|
|
381
|
+
if (!handle) {
|
|
382
|
+
throw verificationError(
|
|
383
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
384
|
+
"The Playwright checkout target became unavailable."
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const element = await handle.evaluate((node) => {
|
|
388
|
+
const html = node;
|
|
389
|
+
const tagName = html.tagName.toLowerCase();
|
|
390
|
+
const explicitRole = html.getAttribute("role")?.trim();
|
|
391
|
+
const inputType = html.getAttribute("type")?.trim().toLowerCase();
|
|
392
|
+
const implicitRole = tagName === "a" && html.href ? "link" : tagName === "button" || tagName === "input" && ["button", "image", "reset", "submit"].includes(inputType ?? "") ? "button" : void 0;
|
|
393
|
+
const accessibleName = html.getAttribute("aria-label")?.trim() || html.textContent?.trim() || html.value?.trim() || html.getAttribute("title")?.trim() || void 0;
|
|
394
|
+
const targetUrl = tagName === "a" && html.href ? html.href : void 0;
|
|
395
|
+
const explicitFormAction = html.getAttribute("formaction")?.trim();
|
|
396
|
+
const formAction = tagName === "button" || tagName === "input" && inputType === "submit" ? explicitFormAction ? new URL(explicitFormAction, document.baseURI).toString() : html.form?.action || void 0 : void 0;
|
|
397
|
+
const target = html.getAttribute("target")?.trim().toLowerCase() || html.getAttribute("formtarget")?.trim().toLowerCase() || html.form?.getAttribute("target")?.trim().toLowerCase();
|
|
398
|
+
return {
|
|
399
|
+
tagName,
|
|
400
|
+
role: explicitRole || implicitRole,
|
|
401
|
+
accessibleName,
|
|
402
|
+
targetUrl,
|
|
403
|
+
formAction,
|
|
404
|
+
opensNewPage: target === "_blank",
|
|
405
|
+
disabled: Boolean(html.disabled) || html.getAttribute("aria-disabled") === "true"
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
pageUrl: input.page.url(),
|
|
410
|
+
locator: input.locator,
|
|
411
|
+
...element
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function boundDestination(snapshot) {
|
|
415
|
+
const destinations = [snapshot.targetUrl, snapshot.formAction].filter(
|
|
416
|
+
(value) => Boolean(value)
|
|
417
|
+
);
|
|
418
|
+
const unique = new Set(destinations);
|
|
419
|
+
if (unique.size > 1) {
|
|
420
|
+
throw verificationError(
|
|
421
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
422
|
+
"The checkout target exposes conflicting navigation and form destinations."
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return destinations[0];
|
|
426
|
+
}
|
|
427
|
+
function createPlaywrightCheckoutAdapter(config) {
|
|
428
|
+
if (!config || typeof config.pageForSession !== "function") {
|
|
429
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "pageForSession is required.");
|
|
430
|
+
}
|
|
431
|
+
const store = config.targetStore ?? createInMemoryPlaywrightCheckoutTargetStore();
|
|
432
|
+
const timeout = clickTimeout(config.clickTimeoutMs);
|
|
433
|
+
const popupWait = popupTimeout(config.popupTimeoutMs);
|
|
434
|
+
const ttl = targetTtl(config.targetTtlMs);
|
|
435
|
+
const now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
436
|
+
async function pageForSession(browserSessionId) {
|
|
437
|
+
const page = await config.pageForSession(browserSessionId);
|
|
438
|
+
if (!page) {
|
|
439
|
+
throw verificationError(
|
|
440
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
441
|
+
"The privileged Playwright page was not found for this browser session."
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return page;
|
|
445
|
+
}
|
|
446
|
+
const driver = createVerifiedBrowserCheckoutDriver({
|
|
447
|
+
resolve: async ({ authorization, browserSessionId, action }) => {
|
|
448
|
+
const registration = await store.get({
|
|
449
|
+
browserSessionId,
|
|
450
|
+
targetFingerprint: action.targetFingerprint
|
|
451
|
+
});
|
|
452
|
+
if (!registration || !validRegistration(registration) || registration.browserSessionId !== browserSessionId || registration.browserSessionId !== authorization.browserSessionId || registration.targetFingerprint !== action.targetFingerprint) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
if (Date.parse(registration.expiresAt) <= currentDate(now).getTime()) {
|
|
456
|
+
await store.delete({ browserSessionId, targetFingerprint: action.targetFingerprint });
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
await store.delete({ browserSessionId, targetFingerprint: action.targetFingerprint });
|
|
460
|
+
const page = await pageForSession(browserSessionId);
|
|
461
|
+
const pageUrlBeforeClick = page.url();
|
|
462
|
+
const handle = await exactElementHandle(page, registration.locator);
|
|
463
|
+
const snapshot = await snapshotPlaywrightTarget({
|
|
464
|
+
page,
|
|
465
|
+
locator: registration.locator,
|
|
466
|
+
handle
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
snapshot,
|
|
470
|
+
dispatch: async () => {
|
|
471
|
+
let popupPage = null;
|
|
472
|
+
let resolvePopup;
|
|
473
|
+
const popupEvent = new Promise((resolve) => {
|
|
474
|
+
resolvePopup = resolve;
|
|
475
|
+
});
|
|
476
|
+
const capturePopup = (popup) => {
|
|
477
|
+
popupPage = popup;
|
|
478
|
+
resolvePopup?.(popup);
|
|
479
|
+
};
|
|
480
|
+
page.on("popup", capturePopup);
|
|
481
|
+
try {
|
|
482
|
+
await handle.click({ timeout });
|
|
483
|
+
if (snapshot.opensNewPage && !popupPage) {
|
|
484
|
+
let timer;
|
|
485
|
+
try {
|
|
486
|
+
popupPage = await Promise.race([
|
|
487
|
+
popupEvent,
|
|
488
|
+
new Promise((_resolve, reject) => {
|
|
489
|
+
timer = setTimeout(
|
|
490
|
+
() => reject(
|
|
491
|
+
verificationError(
|
|
492
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
493
|
+
"The approved checkout target did not open its declared page."
|
|
494
|
+
)
|
|
495
|
+
),
|
|
496
|
+
popupWait
|
|
497
|
+
);
|
|
498
|
+
})
|
|
499
|
+
]);
|
|
500
|
+
} finally {
|
|
501
|
+
if (timer) clearTimeout(timer);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} finally {
|
|
505
|
+
page.off("popup", capturePopup);
|
|
506
|
+
}
|
|
507
|
+
const observedAt = currentDate(now);
|
|
508
|
+
const observedPage = popupPage ?? page;
|
|
509
|
+
const pageUrl = observedHttpsPageUrl(observedPage.url());
|
|
510
|
+
const kind = popupPage ? "NEW_PAGE" : pageUrl === pageUrlBeforeClick ? "SAME_PAGE" : "PAGE_NAVIGATION";
|
|
511
|
+
return {
|
|
512
|
+
result: {
|
|
513
|
+
clicked: true,
|
|
514
|
+
pageUrl,
|
|
515
|
+
targetFingerprint: action.targetFingerprint,
|
|
516
|
+
browserOutcome: {
|
|
517
|
+
kind,
|
|
518
|
+
observedAt: observedAt.toISOString(),
|
|
519
|
+
settlement: "UNVERIFIED"
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
observation: {
|
|
523
|
+
reference: `browser_dispatch_${authorization.authorizationId}`,
|
|
524
|
+
observedAt: observedAt.toISOString(),
|
|
525
|
+
pageUrl
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
return Object.freeze({
|
|
533
|
+
async discover(input) {
|
|
534
|
+
const browserSessionId = requiredText(input.browserSessionId, "browserSessionId", 255);
|
|
535
|
+
const locator = requiredText(input.locator, "locator", 1024);
|
|
536
|
+
const page = await pageForSession(browserSessionId);
|
|
537
|
+
const handle = await exactElementHandle(page, locator);
|
|
538
|
+
const snapshot = await snapshotPlaywrightTarget({ page, locator, handle });
|
|
539
|
+
if (!snapshot.accessibleName) {
|
|
540
|
+
throw verificationError(
|
|
541
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
542
|
+
"The Playwright checkout target must have an accessible name."
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const targetFingerprint = fingerprintBrowserCheckoutTarget(snapshot);
|
|
546
|
+
const targetUrl = boundDestination(snapshot);
|
|
547
|
+
const action = Object.freeze({
|
|
548
|
+
kind: input.kind,
|
|
549
|
+
checkoutMethod: input.checkoutMethod,
|
|
550
|
+
pageUrl: snapshot.pageUrl,
|
|
551
|
+
targetFingerprint,
|
|
552
|
+
...targetUrl ? { targetUrl } : {},
|
|
553
|
+
targetLabel: snapshot.accessibleName
|
|
554
|
+
});
|
|
555
|
+
const discoveredAt = currentDate(now);
|
|
556
|
+
await store.put({
|
|
557
|
+
browserSessionId,
|
|
558
|
+
targetFingerprint,
|
|
559
|
+
locator,
|
|
560
|
+
expiresAt: new Date(discoveredAt.getTime() + ttl).toISOString()
|
|
561
|
+
});
|
|
562
|
+
return Object.freeze({ action, targetFingerprint });
|
|
563
|
+
},
|
|
564
|
+
driver
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function createGuardedPlaywrightCheckoutRuntime(config) {
|
|
568
|
+
if (!config || !config.conto || typeof config.conto.runGuardedCheckoutWithDriver !== "function") {
|
|
569
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "conto client is required.");
|
|
570
|
+
}
|
|
571
|
+
if (typeof config.dispatchAttemptFor !== "function") {
|
|
572
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "dispatchAttemptFor is required.");
|
|
573
|
+
}
|
|
574
|
+
const adapter = createPlaywrightCheckoutAdapter(config);
|
|
575
|
+
return Object.freeze({
|
|
576
|
+
discover: adapter.discover,
|
|
577
|
+
async run(input) {
|
|
578
|
+
const dispatchAttemptId = requiredText(
|
|
579
|
+
await config.dispatchAttemptFor({
|
|
580
|
+
idempotencyKey: input.idempotencyKey,
|
|
581
|
+
browserSessionId: input.browserSessionId,
|
|
582
|
+
targetFingerprint: input.action.targetFingerprint
|
|
583
|
+
}),
|
|
584
|
+
"dispatchAttemptId",
|
|
585
|
+
255
|
|
586
|
+
);
|
|
587
|
+
return config.conto.runGuardedCheckoutWithDriver(input, {
|
|
588
|
+
dispatchAttemptId,
|
|
589
|
+
driver: adapter.driver
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function cdpSessionLimit(value) {
|
|
595
|
+
const resolved = value ?? DEFAULT_CDP_SESSION_LIMIT;
|
|
596
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_CDP_SESSION_LIMIT) {
|
|
597
|
+
throw verificationError(
|
|
598
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
599
|
+
"maxSessions must be a positive integer no greater than 1000."
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
return resolved;
|
|
603
|
+
}
|
|
604
|
+
function cdpSessionId(value) {
|
|
605
|
+
const normalized = requiredText(value, "browserSessionId", 255);
|
|
606
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(normalized)) {
|
|
607
|
+
throw verificationError(
|
|
608
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
609
|
+
"browserSessionId contains unsupported characters."
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return normalized;
|
|
613
|
+
}
|
|
614
|
+
function secureCdpOrigin(value, field) {
|
|
615
|
+
try {
|
|
616
|
+
const url = new URL(value);
|
|
617
|
+
if (url.protocol !== "wss:" && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash || value !== url.origin) {
|
|
618
|
+
throw new Error("unsafe origin");
|
|
619
|
+
}
|
|
620
|
+
return url.origin;
|
|
621
|
+
} catch {
|
|
622
|
+
throw verificationError(
|
|
623
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
624
|
+
`${field} must be an exact WSS or HTTPS origin.`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function secureCdpConnectUrl(value, allowedOrigins) {
|
|
629
|
+
const normalized = requiredText(value, "connectUrl", 4096);
|
|
630
|
+
try {
|
|
631
|
+
const url = new URL(normalized);
|
|
632
|
+
if (url.protocol !== "wss:" && url.protocol !== "https:" || url.username || url.password || url.hash || !allowedOrigins.has(url.origin)) {
|
|
633
|
+
throw new Error("unsafe connect URL");
|
|
634
|
+
}
|
|
635
|
+
return url.toString();
|
|
636
|
+
} catch {
|
|
637
|
+
throw verificationError(
|
|
638
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
639
|
+
"The remote browser connection URL is not permitted."
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function isBrowser(value) {
|
|
644
|
+
if (!value || typeof value !== "object") return false;
|
|
645
|
+
const candidate = value;
|
|
646
|
+
return typeof candidate.contexts === "function" && typeof candidate.isConnected === "function" && typeof candidate.close === "function" && typeof candidate.on === "function";
|
|
647
|
+
}
|
|
648
|
+
function openBrowserPages(browser) {
|
|
649
|
+
return browser.contexts().flatMap((context) => context.pages()).filter((page) => !page.isClosed());
|
|
650
|
+
}
|
|
651
|
+
function createPlaywrightCdpSessionRegistry(config) {
|
|
652
|
+
if (!config || typeof config.connectUrlForSession !== "function") {
|
|
653
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "connectUrlForSession is required.");
|
|
654
|
+
}
|
|
655
|
+
if (typeof config.connectOverCDP !== "function") {
|
|
656
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "connectOverCDP is required.");
|
|
657
|
+
}
|
|
658
|
+
if (!Array.isArray(config.allowedConnectOrigins) || config.allowedConnectOrigins.length === 0) {
|
|
659
|
+
throw verificationError(
|
|
660
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
661
|
+
"allowedConnectOrigins must contain at least one exact origin."
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const allowedOrigins = new Set(
|
|
665
|
+
config.allowedConnectOrigins.map(
|
|
666
|
+
(origin, index) => secureCdpOrigin(origin, `allowedConnectOrigins[${index}]`)
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
if (allowedOrigins.size !== config.allowedConnectOrigins.length) {
|
|
670
|
+
throw verificationError(
|
|
671
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
672
|
+
"allowedConnectOrigins must not contain duplicates."
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const maximumSessions = cdpSessionLimit(config.maxSessions);
|
|
676
|
+
const connections = /* @__PURE__ */ new Map();
|
|
677
|
+
async function browserForSession(browserSessionId) {
|
|
678
|
+
let candidate;
|
|
679
|
+
try {
|
|
680
|
+
candidate = await config.connectUrlForSession(browserSessionId);
|
|
681
|
+
} catch {
|
|
682
|
+
throw verificationError(
|
|
683
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
684
|
+
"Remote browser session ownership could not be verified."
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if (candidate === null) {
|
|
688
|
+
const revoked = connections.get(browserSessionId);
|
|
689
|
+
connections.delete(browserSessionId);
|
|
690
|
+
if (revoked) {
|
|
691
|
+
try {
|
|
692
|
+
const browser = await revoked;
|
|
693
|
+
if (browser?.isConnected()) await browser.close();
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const connectUrl = secureCdpConnectUrl(candidate, allowedOrigins);
|
|
700
|
+
const existing = connections.get(browserSessionId);
|
|
701
|
+
if (existing) {
|
|
702
|
+
const browser = await existing;
|
|
703
|
+
if (browser?.isConnected()) return browser;
|
|
704
|
+
connections.delete(browserSessionId);
|
|
705
|
+
}
|
|
706
|
+
if (connections.size >= maximumSessions) {
|
|
707
|
+
throw verificationError(
|
|
708
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
709
|
+
"The remote browser session limit has been reached."
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const pending = (async () => {
|
|
713
|
+
let browser;
|
|
714
|
+
try {
|
|
715
|
+
browser = await config.connectOverCDP(connectUrl);
|
|
716
|
+
} catch {
|
|
717
|
+
throw verificationError(
|
|
718
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
719
|
+
"The remote browser connection failed."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (!isBrowser(browser) || !browser.isConnected()) {
|
|
723
|
+
try {
|
|
724
|
+
if (isBrowser(browser)) await browser.close();
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
throw verificationError(
|
|
728
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
729
|
+
"The remote browser connection is invalid."
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
browser.on("disconnected", () => {
|
|
733
|
+
if (connections.get(browserSessionId) === pending) connections.delete(browserSessionId);
|
|
734
|
+
});
|
|
735
|
+
return browser;
|
|
736
|
+
})();
|
|
737
|
+
connections.set(browserSessionId, pending);
|
|
738
|
+
try {
|
|
739
|
+
const browser = await pending;
|
|
740
|
+
if (!browser) connections.delete(browserSessionId);
|
|
741
|
+
return browser;
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (connections.get(browserSessionId) === pending) connections.delete(browserSessionId);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function pageForSession(browserSessionIdValue) {
|
|
748
|
+
const browserSessionId = cdpSessionId(browserSessionIdValue);
|
|
749
|
+
const browser = await browserForSession(browserSessionId);
|
|
750
|
+
if (!browser) return null;
|
|
751
|
+
const pages = openBrowserPages(browser);
|
|
752
|
+
if (!config.selectPage) {
|
|
753
|
+
if (pages.length === 0) return null;
|
|
754
|
+
if (pages.length !== 1) {
|
|
755
|
+
throw verificationError(
|
|
756
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
757
|
+
"The remote browser session has multiple open pages; configure an exact page selector."
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
return pages[0] ?? null;
|
|
761
|
+
}
|
|
762
|
+
const selected = await config.selectPage({
|
|
763
|
+
browserSessionId,
|
|
764
|
+
pages: Object.freeze([...pages])
|
|
765
|
+
});
|
|
766
|
+
if (selected === null) return null;
|
|
767
|
+
if (!pages.includes(selected) || selected.isClosed()) {
|
|
768
|
+
throw verificationError(
|
|
769
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
770
|
+
"The selected page does not belong to the requested browser session."
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
return selected;
|
|
774
|
+
}
|
|
775
|
+
async function closeSession(browserSessionIdValue) {
|
|
776
|
+
const browserSessionId = cdpSessionId(browserSessionIdValue);
|
|
777
|
+
const pending = connections.get(browserSessionId);
|
|
778
|
+
connections.delete(browserSessionId);
|
|
779
|
+
const browser = await pending;
|
|
780
|
+
if (browser?.isConnected()) await browser.close();
|
|
781
|
+
}
|
|
782
|
+
return Object.freeze({
|
|
783
|
+
pageForSession,
|
|
784
|
+
closeSession,
|
|
785
|
+
async close() {
|
|
786
|
+
const pending = [...connections.values()];
|
|
787
|
+
connections.clear();
|
|
788
|
+
const browsers = await Promise.allSettled(pending);
|
|
789
|
+
await Promise.allSettled(
|
|
790
|
+
browsers.flatMap(
|
|
791
|
+
(result) => result.status === "fulfilled" && result.value?.isConnected() ? [result.value.close()] : []
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function boundedObservationText(value, maximum) {
|
|
798
|
+
if (typeof value !== "string") return "";
|
|
799
|
+
return value.replace(/[\u0000-\u001f\u007f\s]+/g, " ").trim().slice(0, maximum);
|
|
800
|
+
}
|
|
801
|
+
function observedHttpsOrigin(value) {
|
|
802
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
803
|
+
try {
|
|
804
|
+
const url = new URL(value);
|
|
805
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port && url.port !== "443") {
|
|
806
|
+
return void 0;
|
|
807
|
+
}
|
|
808
|
+
return url.origin;
|
|
809
|
+
} catch {
|
|
810
|
+
return void 0;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function createPlaywrightSessionObserver(config) {
|
|
814
|
+
if (!config || typeof config.pageForSession !== "function") {
|
|
815
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "pageForSession is required.");
|
|
816
|
+
}
|
|
817
|
+
const now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
818
|
+
return Object.freeze({
|
|
819
|
+
async observe(browserSessionIdValue) {
|
|
820
|
+
const browserSessionId = cdpSessionId(browserSessionIdValue);
|
|
821
|
+
const page = await config.pageForSession(browserSessionId);
|
|
822
|
+
if (!page || page.isClosed()) {
|
|
823
|
+
throw verificationError(
|
|
824
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
825
|
+
"The owned browser page is unavailable."
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const beforeUrl = page.url();
|
|
829
|
+
const pageOrigin = beforeUrl === "about:blank" ? null : observedHttpsOrigin(beforeUrl);
|
|
830
|
+
if (beforeUrl !== "about:blank" && !pageOrigin) {
|
|
831
|
+
throw verificationError(
|
|
832
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
833
|
+
"The owned browser page is not at a trusted HTTPS origin."
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
let title;
|
|
837
|
+
let rawTargets;
|
|
838
|
+
try {
|
|
839
|
+
[title, rawTargets] = await Promise.all([
|
|
840
|
+
page.title(),
|
|
841
|
+
page.locator(SESSION_OBSERVATION_SELECTOR).evaluateAll((nodes, limit) => {
|
|
842
|
+
const candidates = [];
|
|
843
|
+
for (const [index, node] of nodes.entries()) {
|
|
844
|
+
const element = node;
|
|
845
|
+
const style = window.getComputedStyle(element);
|
|
846
|
+
if (element.hidden || style.display === "none" || style.visibility === "hidden" || element.getClientRects().length === 0) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const tagName = element.tagName.toUpperCase();
|
|
850
|
+
const declaredRole = element.getAttribute("role")?.toLowerCase();
|
|
851
|
+
const role = tagName === "A" || declaredRole === "link" ? "LINK" : "BUTTON";
|
|
852
|
+
const inputValue = tagName === "INPUT" ? element.value : void 0;
|
|
853
|
+
const label = element.getAttribute("aria-label") || element.getAttribute("title") || inputValue || element.textContent || "";
|
|
854
|
+
const disabled = (tagName === "BUTTON" || tagName === "INPUT" ? element.disabled : false) || element.getAttribute("aria-disabled") === "true";
|
|
855
|
+
const href = tagName === "A" ? element.href : void 0;
|
|
856
|
+
candidates.push({
|
|
857
|
+
index,
|
|
858
|
+
role,
|
|
859
|
+
label,
|
|
860
|
+
disabled,
|
|
861
|
+
...href ? { href } : {}
|
|
862
|
+
});
|
|
863
|
+
if (candidates.length > limit) break;
|
|
864
|
+
}
|
|
865
|
+
return candidates;
|
|
866
|
+
}, SESSION_OBSERVATION_TARGET_LIMIT)
|
|
867
|
+
]);
|
|
868
|
+
} catch {
|
|
869
|
+
throw verificationError(
|
|
870
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
871
|
+
"The owned browser page could not be observed."
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
const currentPage = await config.pageForSession(browserSessionId);
|
|
875
|
+
if (!currentPage || currentPage !== page || page.isClosed() || page.url() !== beforeUrl) {
|
|
876
|
+
throw verificationError(
|
|
877
|
+
"BROWSER_CHECKOUT_TARGET_CHANGED",
|
|
878
|
+
"The owned browser page changed while it was being observed."
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
const truncated = rawTargets.length > SESSION_OBSERVATION_TARGET_LIMIT;
|
|
882
|
+
const targets = rawTargets.slice(0, SESSION_OBSERVATION_TARGET_LIMIT).map((target) => {
|
|
883
|
+
const destinationOrigin = observedHttpsOrigin(target.href);
|
|
884
|
+
return Object.freeze({
|
|
885
|
+
locator: `:nth-match(${SESSION_OBSERVATION_SELECTOR}, ${target.index + 1})`,
|
|
886
|
+
role: target.role,
|
|
887
|
+
label: boundedObservationText(target.label, SESSION_OBSERVATION_LABEL_LIMIT),
|
|
888
|
+
disabled: target.disabled,
|
|
889
|
+
...destinationOrigin ? { destinationOrigin } : {}
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
return Object.freeze({
|
|
893
|
+
browserSessionId,
|
|
894
|
+
pageOrigin: pageOrigin ?? null,
|
|
895
|
+
title: boundedObservationText(title, SESSION_OBSERVATION_TITLE_LIMIT),
|
|
896
|
+
targets: Object.freeze(targets),
|
|
897
|
+
truncated,
|
|
898
|
+
observedAt: now().toISOString()
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
function createGuardedPlaywrightCheckoutRuntimeHost(config) {
|
|
904
|
+
if (!config || typeof config !== "object") {
|
|
905
|
+
throw verificationError(
|
|
906
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
907
|
+
"The guarded Playwright runtime host configuration is required."
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const browserSessions = createPlaywrightCdpSessionRegistry(config.browserSessions);
|
|
911
|
+
const observer = createPlaywrightSessionObserver({
|
|
912
|
+
pageForSession: browserSessions.pageForSession,
|
|
913
|
+
now: config.now
|
|
914
|
+
});
|
|
915
|
+
const targetStore = createEncryptedPlaywrightCheckoutTargetStore(config.targetStore);
|
|
916
|
+
const runtime = createGuardedPlaywrightCheckoutRuntime({
|
|
917
|
+
conto: config.conto,
|
|
918
|
+
pageForSession: browserSessions.pageForSession,
|
|
919
|
+
targetStore,
|
|
920
|
+
dispatchAttemptFor: config.dispatchAttemptFor,
|
|
921
|
+
clickTimeoutMs: config.clickTimeoutMs,
|
|
922
|
+
popupTimeoutMs: config.popupTimeoutMs,
|
|
923
|
+
targetTtlMs: config.targetTtlMs,
|
|
924
|
+
now: config.now
|
|
925
|
+
});
|
|
926
|
+
const handler = createGuardedPlaywrightCheckoutHttpHandler({
|
|
927
|
+
runtime,
|
|
928
|
+
token: config.token,
|
|
929
|
+
maxBodyBytes: config.maxBodyBytes
|
|
930
|
+
});
|
|
931
|
+
return Object.freeze({
|
|
932
|
+
handler,
|
|
933
|
+
observeSession: observer.observe,
|
|
934
|
+
closeSession: browserSessions.closeSession,
|
|
935
|
+
close: browserSessions.close
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
var GuardedCheckoutHttpInputError = class extends Error {
|
|
939
|
+
constructor(status, code, message) {
|
|
940
|
+
super(message);
|
|
941
|
+
this.status = status;
|
|
942
|
+
this.code = code;
|
|
943
|
+
this.name = "GuardedCheckoutHttpInputError";
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
var HTTP_ACTION_KINDS = [
|
|
947
|
+
"INITIATE_CHECKOUT",
|
|
948
|
+
"OPEN_LINK",
|
|
949
|
+
"CONFIRM_PAYMENT",
|
|
950
|
+
"PLACE_ORDER"
|
|
951
|
+
];
|
|
952
|
+
var HTTP_CHECKOUT_METHODS = ["LINK", "CARD", "SHOP_PAY", "PAYPAL"];
|
|
953
|
+
var HTTP_TOTAL_TYPES = [
|
|
954
|
+
"subtotal",
|
|
955
|
+
"tax",
|
|
956
|
+
"total",
|
|
957
|
+
"items_base_amount",
|
|
958
|
+
"items_discount",
|
|
959
|
+
"discount",
|
|
960
|
+
"fulfillment",
|
|
961
|
+
"shipping",
|
|
962
|
+
"fee",
|
|
963
|
+
"gift_wrap",
|
|
964
|
+
"tip",
|
|
965
|
+
"store_credit"
|
|
966
|
+
];
|
|
967
|
+
var SAFE_RUNTIME_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
968
|
+
"ACTION_REQUIRED",
|
|
969
|
+
"AUTH_FAILED",
|
|
970
|
+
"BROWSER_ACTION_DISPATCH_INDETERMINATE",
|
|
971
|
+
"BROWSER_ACTION_FINALIZATION_PENDING",
|
|
972
|
+
"BROWSER_ACTION_OBSERVATION_INVALID",
|
|
973
|
+
"BROWSER_CHECKOUT_TARGET_CHANGED",
|
|
974
|
+
"BROWSER_CHECKOUT_TARGET_DISABLED",
|
|
975
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
976
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
977
|
+
"CHECKOUT_UNAVAILABLE",
|
|
978
|
+
"COMMERCE_EXECUTION_NOT_READY",
|
|
979
|
+
"CONFLICT",
|
|
980
|
+
"INSUFFICIENT_SCOPE",
|
|
981
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
982
|
+
"INVALID_REQUEST",
|
|
983
|
+
"POLICY_DENIED",
|
|
984
|
+
"RATE_LIMITED",
|
|
985
|
+
"REQUIRES_APPROVAL",
|
|
986
|
+
"SERVER_ERROR"
|
|
987
|
+
]);
|
|
988
|
+
function httpRecord(value, field) {
|
|
989
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
990
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
|
|
991
|
+
}
|
|
992
|
+
return value;
|
|
993
|
+
}
|
|
994
|
+
function httpClosedObject(value, field, allowedKeys) {
|
|
995
|
+
const record = httpRecord(value, field);
|
|
996
|
+
if (Object.keys(record).some((key) => !allowedKeys.includes(key))) {
|
|
997
|
+
throw new GuardedCheckoutHttpInputError(
|
|
998
|
+
400,
|
|
999
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
1000
|
+
`${field} contains an unsupported field`
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
return record;
|
|
1004
|
+
}
|
|
1005
|
+
function httpText(value, field, maxLength) {
|
|
1006
|
+
if (typeof value !== "string") {
|
|
1007
|
+
throw new GuardedCheckoutHttpInputError(
|
|
1008
|
+
400,
|
|
1009
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
1010
|
+
`${field} is required`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const normalized = value.trim();
|
|
1014
|
+
if (!normalized || normalized.length > maxLength) {
|
|
1015
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
|
|
1016
|
+
}
|
|
1017
|
+
return normalized;
|
|
1018
|
+
}
|
|
1019
|
+
function httpInteger(value, field, minimum) {
|
|
1020
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
1021
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
|
|
1022
|
+
}
|
|
1023
|
+
return value;
|
|
1024
|
+
}
|
|
1025
|
+
function httpEnum(value, field, choices) {
|
|
1026
|
+
if (typeof value !== "string" || !choices.includes(value)) {
|
|
1027
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
|
|
1028
|
+
}
|
|
1029
|
+
return value;
|
|
1030
|
+
}
|
|
1031
|
+
function httpHttpsUrl(value, field) {
|
|
1032
|
+
const text = httpText(value, field, 2048);
|
|
1033
|
+
try {
|
|
1034
|
+
const url = new URL(text);
|
|
1035
|
+
if (url.protocol !== "https:" || url.username || url.password) throw new Error("unsafe URL");
|
|
1036
|
+
return url.toString();
|
|
1037
|
+
} catch {
|
|
1038
|
+
throw new GuardedCheckoutHttpInputError(
|
|
1039
|
+
400,
|
|
1040
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
1041
|
+
`${field} must use HTTPS without credentials`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function httpArray(value, field, maxItems) {
|
|
1046
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > maxItems) {
|
|
1047
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_BROWSER_CHECKOUT", `${field} is invalid`);
|
|
1048
|
+
}
|
|
1049
|
+
return value;
|
|
1050
|
+
}
|
|
1051
|
+
function parseGuardedCheckoutHttpInput(value) {
|
|
1052
|
+
const input = httpClosedObject(value, "body", [
|
|
1053
|
+
"idempotencyKey",
|
|
1054
|
+
"browserSessionId",
|
|
1055
|
+
"locator",
|
|
1056
|
+
"actionKind",
|
|
1057
|
+
"checkoutMethod",
|
|
1058
|
+
"amountMinor",
|
|
1059
|
+
"currency",
|
|
1060
|
+
"purpose",
|
|
1061
|
+
"category",
|
|
1062
|
+
"counterparty",
|
|
1063
|
+
"cart",
|
|
1064
|
+
"providerPresentation"
|
|
1065
|
+
]);
|
|
1066
|
+
const counterparty = httpClosedObject(input.counterparty, "counterparty", [
|
|
1067
|
+
"canonicalId",
|
|
1068
|
+
"displayName",
|
|
1069
|
+
"checkoutUrl"
|
|
1070
|
+
]);
|
|
1071
|
+
const cart = httpClosedObject(input.cart, "cart", ["lineItems", "totals", "fulfillment"]);
|
|
1072
|
+
const presentation = httpClosedObject(input.providerPresentation, "providerPresentation", [
|
|
1073
|
+
"approvalSummary",
|
|
1074
|
+
"lineItemLabels"
|
|
1075
|
+
]);
|
|
1076
|
+
const amountMinor = httpInteger(input.amountMinor, "amountMinor", 1);
|
|
1077
|
+
const lineItems = httpArray(cart.lineItems, "cart.lineItems", 100).map((value2, index) => {
|
|
1078
|
+
const item = httpClosedObject(value2, `cart.lineItems[${index}]`, [
|
|
1079
|
+
"name",
|
|
1080
|
+
"quantity",
|
|
1081
|
+
"unitAmountMinor",
|
|
1082
|
+
"description",
|
|
1083
|
+
"sku",
|
|
1084
|
+
"productUrl"
|
|
1085
|
+
]);
|
|
1086
|
+
return {
|
|
1087
|
+
name: httpText(item.name, `cart.lineItems[${index}].name`, 500),
|
|
1088
|
+
quantity: httpInteger(item.quantity, `cart.lineItems[${index}].quantity`, 1),
|
|
1089
|
+
unitAmountMinor: httpInteger(
|
|
1090
|
+
item.unitAmountMinor,
|
|
1091
|
+
`cart.lineItems[${index}].unitAmountMinor`,
|
|
1092
|
+
0
|
|
1093
|
+
),
|
|
1094
|
+
...item.description !== void 0 ? { description: httpText(item.description, `cart.lineItems[${index}].description`, 500) } : {},
|
|
1095
|
+
...item.sku !== void 0 ? { sku: httpText(item.sku, `cart.lineItems[${index}].sku`, 200) } : {},
|
|
1096
|
+
...item.productUrl !== void 0 ? { productUrl: httpHttpsUrl(item.productUrl, `cart.lineItems[${index}].productUrl`) } : {}
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
const totals = httpArray(cart.totals, "cart.totals", 25).map((value2, index) => {
|
|
1100
|
+
const total = httpClosedObject(value2, `cart.totals[${index}]`, [
|
|
1101
|
+
"type",
|
|
1102
|
+
"label",
|
|
1103
|
+
"amountMinor"
|
|
1104
|
+
]);
|
|
1105
|
+
return {
|
|
1106
|
+
type: httpEnum(total.type, `cart.totals[${index}].type`, HTTP_TOTAL_TYPES),
|
|
1107
|
+
label: httpText(total.label, `cart.totals[${index}].label`, 200),
|
|
1108
|
+
amountMinor: httpInteger(total.amountMinor, `cart.totals[${index}].amountMinor`, 0)
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
const finalTotals = totals.filter((total) => total.type === "total");
|
|
1112
|
+
if (finalTotals.length !== 1 || finalTotals[0].amountMinor !== amountMinor) {
|
|
1113
|
+
throw new GuardedCheckoutHttpInputError(
|
|
1114
|
+
400,
|
|
1115
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
1116
|
+
"cart total must match amountMinor"
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
let fulfillment;
|
|
1120
|
+
if (cart.fulfillment !== void 0) {
|
|
1121
|
+
const source = httpClosedObject(cart.fulfillment, "cart.fulfillment", [
|
|
1122
|
+
"recipient",
|
|
1123
|
+
"addressLine1",
|
|
1124
|
+
"addressLine2",
|
|
1125
|
+
"locality",
|
|
1126
|
+
"region",
|
|
1127
|
+
"postalCode",
|
|
1128
|
+
"countryCode"
|
|
1129
|
+
]);
|
|
1130
|
+
const fields = [
|
|
1131
|
+
"recipient",
|
|
1132
|
+
"addressLine1",
|
|
1133
|
+
"addressLine2",
|
|
1134
|
+
"locality",
|
|
1135
|
+
"region",
|
|
1136
|
+
"postalCode",
|
|
1137
|
+
"countryCode"
|
|
1138
|
+
];
|
|
1139
|
+
const values = Object.fromEntries(
|
|
1140
|
+
fields.filter((field) => source[field] !== void 0).map((field) => [field, httpText(source[field], `cart.fulfillment.${field}`, 500)])
|
|
1141
|
+
);
|
|
1142
|
+
if (Object.keys(values).length === 0) {
|
|
1143
|
+
throw new GuardedCheckoutHttpInputError(
|
|
1144
|
+
400,
|
|
1145
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
1146
|
+
"cart.fulfillment cannot be empty"
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if (values.countryCode && !/^[A-Za-z]{2}$/.test(values.countryCode)) {
|
|
1150
|
+
throw new GuardedCheckoutHttpInputError(
|
|
1151
|
+
400,
|
|
1152
|
+
"INVALID_BROWSER_CHECKOUT",
|
|
1153
|
+
"cart.fulfillment.countryCode is invalid"
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
fulfillment = {
|
|
1157
|
+
...values,
|
|
1158
|
+
...values.countryCode ? { countryCode: values.countryCode.toUpperCase() } : {}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
const labels = httpArray(
|
|
1162
|
+
presentation.lineItemLabels,
|
|
1163
|
+
"providerPresentation.lineItemLabels",
|
|
1164
|
+
100
|
|
1165
|
+
).map((label, index) => httpText(label, `providerPresentation.lineItemLabels[${index}]`, 500));
|
|
1166
|
+
return {
|
|
1167
|
+
idempotencyKey: httpText(input.idempotencyKey, "idempotencyKey", 255),
|
|
1168
|
+
browserSessionId: httpText(input.browserSessionId, "browserSessionId", 255),
|
|
1169
|
+
locator: httpText(input.locator, "locator", 1024),
|
|
1170
|
+
actionKind: httpEnum(input.actionKind, "actionKind", HTTP_ACTION_KINDS),
|
|
1171
|
+
checkoutMethod: httpEnum(input.checkoutMethod, "checkoutMethod", HTTP_CHECKOUT_METHODS),
|
|
1172
|
+
amountMinor,
|
|
1173
|
+
currency: httpEnum(input.currency, "currency", ["USD"]),
|
|
1174
|
+
purpose: httpText(input.purpose, "purpose", 500),
|
|
1175
|
+
...input.category !== void 0 ? { category: httpText(input.category, "category", 100) } : {},
|
|
1176
|
+
counterparty: {
|
|
1177
|
+
canonicalId: httpText(counterparty.canonicalId, "counterparty.canonicalId", 255),
|
|
1178
|
+
displayName: httpText(counterparty.displayName, "counterparty.displayName", 200),
|
|
1179
|
+
checkoutUrl: httpHttpsUrl(counterparty.checkoutUrl, "counterparty.checkoutUrl")
|
|
1180
|
+
},
|
|
1181
|
+
cart: { lineItems, totals, ...fulfillment ? { fulfillment } : {} },
|
|
1182
|
+
providerPresentation: {
|
|
1183
|
+
approvalSummary: httpText(
|
|
1184
|
+
presentation.approvalSummary,
|
|
1185
|
+
"providerPresentation.approvalSummary",
|
|
1186
|
+
500
|
|
1187
|
+
),
|
|
1188
|
+
lineItemLabels: labels
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function httpJson(status, body, headers) {
|
|
1193
|
+
return new Response(JSON.stringify(body), {
|
|
1194
|
+
status,
|
|
1195
|
+
headers: {
|
|
1196
|
+
"cache-control": "no-store",
|
|
1197
|
+
"content-type": "application/json; charset=utf-8",
|
|
1198
|
+
...headers
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
async function readBoundedHttpBody(request, maxBodyBytes) {
|
|
1203
|
+
if (!request.body) return "";
|
|
1204
|
+
const reader = request.body.getReader();
|
|
1205
|
+
const chunks = [];
|
|
1206
|
+
let length = 0;
|
|
1207
|
+
try {
|
|
1208
|
+
while (true) {
|
|
1209
|
+
const { done, value } = await reader.read();
|
|
1210
|
+
if (done) break;
|
|
1211
|
+
length += value.byteLength;
|
|
1212
|
+
if (length > maxBodyBytes) {
|
|
1213
|
+
try {
|
|
1214
|
+
await reader.cancel();
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
throw new GuardedCheckoutHttpInputError(413, "INVALID_REQUEST", "body is too large");
|
|
1218
|
+
}
|
|
1219
|
+
chunks.push(value);
|
|
1220
|
+
}
|
|
1221
|
+
} finally {
|
|
1222
|
+
reader.releaseLock();
|
|
1223
|
+
}
|
|
1224
|
+
const bytes = new Uint8Array(length);
|
|
1225
|
+
let offset = 0;
|
|
1226
|
+
for (const chunk of chunks) {
|
|
1227
|
+
bytes.set(chunk, offset);
|
|
1228
|
+
offset += chunk.byteLength;
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1232
|
+
} catch {
|
|
1233
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_REQUEST", "body is not valid UTF-8");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function safeRuntimeIdentifier(value) {
|
|
1237
|
+
return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9._:@/-]+$/.test(value) ? value : void 0;
|
|
1238
|
+
}
|
|
1239
|
+
function safeRuntimeError(error) {
|
|
1240
|
+
if (error instanceof GuardedCheckoutHttpInputError) {
|
|
1241
|
+
return httpJson(error.status, { code: error.code });
|
|
1242
|
+
}
|
|
1243
|
+
const record = error && typeof error === "object" ? error : void 0;
|
|
1244
|
+
const candidateCode = record?.code;
|
|
1245
|
+
const code = typeof candidateCode === "string" && SAFE_RUNTIME_ERROR_CODES.has(candidateCode) ? candidateCode : "SERVER_ERROR";
|
|
1246
|
+
let status = typeof record?.status === "number" ? record.status : code.startsWith("BROWSER_") ? 409 : 500;
|
|
1247
|
+
if (!Number.isInteger(status) || status < 400 || status > 599) status = 409;
|
|
1248
|
+
if (code === "SERVER_ERROR") status = 500;
|
|
1249
|
+
const requestId = safeRuntimeIdentifier(record?.requestId);
|
|
1250
|
+
return httpJson(status, { code, ...requestId ? { requestId } : {} });
|
|
1251
|
+
}
|
|
1252
|
+
function validRuntimeToken(value) {
|
|
1253
|
+
return value.length >= 32 && value.length <= 1024 && /^[\x21-\x7e]+$/.test(value) && !value.startsWith("conto_");
|
|
1254
|
+
}
|
|
1255
|
+
function authorizedRuntimeRequest(request, token) {
|
|
1256
|
+
const header = request.headers.get("authorization");
|
|
1257
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
1258
|
+
const supplied = Buffer.from(header.slice(7));
|
|
1259
|
+
const expected = Buffer.from(token);
|
|
1260
|
+
return supplied.length === expected.length && (0, import_node_crypto2.timingSafeEqual)(supplied, expected);
|
|
1261
|
+
}
|
|
1262
|
+
function projectedRuntimeResult(value, expected) {
|
|
1263
|
+
const result = httpRecord(value, "runtime result");
|
|
1264
|
+
const execution = httpRecord(result.execution, "runtime execution");
|
|
1265
|
+
const dispatched = httpRecord(result.result, "runtime dispatch result");
|
|
1266
|
+
const outcome = httpRecord(dispatched.browserOutcome, "runtime browser outcome");
|
|
1267
|
+
const evidence = httpRecord(execution.evidence, "runtime evidence");
|
|
1268
|
+
const executionId = safeRuntimeIdentifier(execution.id);
|
|
1269
|
+
const traceId = safeRuntimeIdentifier(evidence.traceId);
|
|
1270
|
+
if (!executionId) throw new Error("invalid runtime execution ID");
|
|
1271
|
+
const status = httpEnum(execution.status, "runtime execution status", [
|
|
1272
|
+
"READY",
|
|
1273
|
+
"PROCESSING",
|
|
1274
|
+
"NEEDS_RECONCILIATION"
|
|
1275
|
+
]);
|
|
1276
|
+
const amountMinor = httpInteger(execution.amountMinor, "runtime execution amountMinor", 1);
|
|
1277
|
+
const currency = httpEnum(execution.currency, "runtime execution currency", ["USD"]);
|
|
1278
|
+
const checkoutMethod = httpEnum(
|
|
1279
|
+
execution.checkoutMethod,
|
|
1280
|
+
"runtime execution checkoutMethod",
|
|
1281
|
+
HTTP_CHECKOUT_METHODS
|
|
1282
|
+
);
|
|
1283
|
+
if (amountMinor !== expected.amountMinor || currency !== expected.currency || checkoutMethod !== expected.checkoutMethod) {
|
|
1284
|
+
throw new Error("runtime result does not match request");
|
|
1285
|
+
}
|
|
1286
|
+
const pageUrl = new URL(httpHttpsUrl(dispatched.pageUrl, "runtime result pageUrl")).origin;
|
|
1287
|
+
const observedAt = httpText(outcome.observedAt, "runtime result observedAt", 100);
|
|
1288
|
+
if (Number.isNaN(Date.parse(observedAt))) throw new Error("invalid runtime observedAt");
|
|
1289
|
+
return {
|
|
1290
|
+
execution: {
|
|
1291
|
+
id: executionId,
|
|
1292
|
+
status,
|
|
1293
|
+
amountMinor,
|
|
1294
|
+
currency,
|
|
1295
|
+
checkoutMethod,
|
|
1296
|
+
evidence: { ...traceId ? { traceId } : {} }
|
|
1297
|
+
},
|
|
1298
|
+
result: {
|
|
1299
|
+
pageUrl,
|
|
1300
|
+
browserOutcome: {
|
|
1301
|
+
kind: httpEnum(outcome.kind, "runtime result kind", [
|
|
1302
|
+
"NEW_PAGE",
|
|
1303
|
+
"PAGE_NAVIGATION",
|
|
1304
|
+
"SAME_PAGE"
|
|
1305
|
+
]),
|
|
1306
|
+
observedAt,
|
|
1307
|
+
settlement: httpEnum(outcome.settlement, "runtime result settlement", ["UNVERIFIED"])
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function createGuardedPlaywrightCheckoutHttpHandler(config) {
|
|
1313
|
+
if (!config || !config.runtime || typeof config.runtime.discover !== "function" || typeof config.runtime.run !== "function") {
|
|
1314
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "runtime is required.");
|
|
1315
|
+
}
|
|
1316
|
+
if (!validRuntimeToken(config.token)) {
|
|
1317
|
+
throw verificationError(
|
|
1318
|
+
"BROWSER_CHECKOUT_TARGET_INVALID",
|
|
1319
|
+
"token must be a dedicated credential containing 32 to 1024 non-space characters."
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const maxBodyBytes = config.maxBodyBytes ?? DEFAULT_HTTP_BODY_LIMIT_BYTES;
|
|
1323
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0 || maxBodyBytes > MAX_HTTP_BODY_LIMIT_BYTES) {
|
|
1324
|
+
throw verificationError("BROWSER_CHECKOUT_TARGET_INVALID", "maxBodyBytes is invalid.");
|
|
1325
|
+
}
|
|
1326
|
+
const runtime = config.runtime;
|
|
1327
|
+
const token = config.token;
|
|
1328
|
+
return async (request) => {
|
|
1329
|
+
if (request.method !== "POST") {
|
|
1330
|
+
return httpJson(405, { code: "INVALID_REQUEST" }, { allow: "POST" });
|
|
1331
|
+
}
|
|
1332
|
+
if (!authorizedRuntimeRequest(request, token)) {
|
|
1333
|
+
return httpJson(401, { code: "AUTH_FAILED" }, { "www-authenticate": "Bearer" });
|
|
1334
|
+
}
|
|
1335
|
+
const mediaType = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
|
|
1336
|
+
if (mediaType !== "application/json") {
|
|
1337
|
+
return httpJson(415, { code: "INVALID_REQUEST" });
|
|
1338
|
+
}
|
|
1339
|
+
const declaredLength = Number(request.headers.get("content-length"));
|
|
1340
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
1341
|
+
return httpJson(413, { code: "INVALID_REQUEST" });
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
const text = await readBoundedHttpBody(request, maxBodyBytes);
|
|
1345
|
+
let parsed;
|
|
1346
|
+
try {
|
|
1347
|
+
parsed = JSON.parse(text);
|
|
1348
|
+
} catch {
|
|
1349
|
+
throw new GuardedCheckoutHttpInputError(400, "INVALID_REQUEST", "body is invalid JSON");
|
|
1350
|
+
}
|
|
1351
|
+
const input = parseGuardedCheckoutHttpInput(parsed);
|
|
1352
|
+
const discovered = await runtime.discover({
|
|
1353
|
+
browserSessionId: input.browserSessionId,
|
|
1354
|
+
locator: input.locator,
|
|
1355
|
+
kind: input.actionKind,
|
|
1356
|
+
checkoutMethod: input.checkoutMethod
|
|
1357
|
+
});
|
|
1358
|
+
const result = await runtime.run({
|
|
1359
|
+
executionMethod: "BROWSER_CHECKOUT",
|
|
1360
|
+
idempotencyKey: input.idempotencyKey,
|
|
1361
|
+
browserSessionId: input.browserSessionId,
|
|
1362
|
+
amountMinor: input.amountMinor,
|
|
1363
|
+
currency: input.currency,
|
|
1364
|
+
purpose: input.purpose,
|
|
1365
|
+
...input.category ? { category: input.category } : {},
|
|
1366
|
+
counterparty: input.counterparty,
|
|
1367
|
+
cart: input.cart,
|
|
1368
|
+
providerPresentation: input.providerPresentation,
|
|
1369
|
+
action: discovered.action
|
|
1370
|
+
});
|
|
1371
|
+
let output;
|
|
1372
|
+
try {
|
|
1373
|
+
output = projectedRuntimeResult(result, input);
|
|
1374
|
+
} catch {
|
|
1375
|
+
return httpJson(500, { code: "SERVER_ERROR" });
|
|
1376
|
+
}
|
|
1377
|
+
return httpJson(200, output);
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
return safeRuntimeError(error);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/browserbase.ts
|
|
1385
|
+
var BROWSERBASE_API_ORIGIN = "https://api.browserbase.com";
|
|
1386
|
+
var BROWSERBASE_CONNECT_ORIGINS = Object.freeze([
|
|
1387
|
+
"wss://connect.browserbase.com",
|
|
1388
|
+
"wss://connect.usw2.browserbase.com"
|
|
1389
|
+
]);
|
|
1390
|
+
var BROWSERBASE_LIVE_VIEW_DOMAIN = "browserbase.com";
|
|
1391
|
+
var DEFAULT_SESSION_TIMEOUT_SECONDS = 900;
|
|
1392
|
+
var MIN_SESSION_TIMEOUT_SECONDS = 60;
|
|
1393
|
+
var MAX_SESSION_TIMEOUT_SECONDS = 21600;
|
|
1394
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
1395
|
+
var MAX_REQUEST_TIMEOUT_MS = 6e4;
|
|
1396
|
+
var MAX_PROVIDER_RESPONSE_BYTES = 64 * 1024;
|
|
1397
|
+
var MAX_HANDOFF_ASSERTION_TTL_MS = 5 * 60 * 1e3;
|
|
1398
|
+
var HANDOFF_ASSERTION_VERSION = "bbh1";
|
|
1399
|
+
var BrowserbaseSessionError = class extends Error {
|
|
1400
|
+
constructor(code, message) {
|
|
1401
|
+
super(message);
|
|
1402
|
+
this.code = code;
|
|
1403
|
+
this.name = "BrowserbaseSessionError";
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
function handoffKey(value) {
|
|
1407
|
+
if (value === void 0) return void 0;
|
|
1408
|
+
const key = Buffer.from(value);
|
|
1409
|
+
if (key.length !== 32) {
|
|
1410
|
+
fail("BROWSERBASE_CONFIG_INVALID", "browserbase.handoffKey must contain exactly 32 bytes.");
|
|
1411
|
+
}
|
|
1412
|
+
return key;
|
|
1413
|
+
}
|
|
1414
|
+
function handoffExpiresAt(value) {
|
|
1415
|
+
const normalized = requiredText2(value, "handoff.expiresAt", 100);
|
|
1416
|
+
const milliseconds = Date.parse(normalized);
|
|
1417
|
+
if (!Number.isFinite(milliseconds)) {
|
|
1418
|
+
fail("BROWSERBASE_CONFIG_INVALID", "handoff.expiresAt is invalid.");
|
|
1419
|
+
}
|
|
1420
|
+
return new Date(milliseconds).toISOString();
|
|
1421
|
+
}
|
|
1422
|
+
function handoffPayload(input) {
|
|
1423
|
+
return JSON.stringify([
|
|
1424
|
+
HANDOFF_ASSERTION_VERSION,
|
|
1425
|
+
sessionId(input.browserSessionId),
|
|
1426
|
+
requiredText2(input.projectId, "handoff.projectId"),
|
|
1427
|
+
requiredText2(input.organizationId, "handoff.organizationId"),
|
|
1428
|
+
requiredText2(input.agentId, "handoff.agentId"),
|
|
1429
|
+
handoffExpiresAt(input.expiresAt)
|
|
1430
|
+
]);
|
|
1431
|
+
}
|
|
1432
|
+
function createBrowserbaseSessionHandoffAssertion(input, keyValue) {
|
|
1433
|
+
const key = handoffKey(keyValue);
|
|
1434
|
+
if (!key) fail("BROWSERBASE_CONFIG_INVALID", "A Browserbase handoff key is required.");
|
|
1435
|
+
const expiresAt = handoffExpiresAt(input.expiresAt);
|
|
1436
|
+
const signature = (0, import_node_crypto3.createHmac)("sha256", key).update(handoffPayload({ ...input, expiresAt })).digest("base64url");
|
|
1437
|
+
return `${HANDOFF_ASSERTION_VERSION}.${Date.parse(expiresAt)}.${signature}`;
|
|
1438
|
+
}
|
|
1439
|
+
function verifyBrowserbaseSessionHandoffAssertion(assertionValue, input, key, now) {
|
|
1440
|
+
if (typeof assertionValue !== "string" || assertionValue.length > 512) return false;
|
|
1441
|
+
const match = /^bbh1\.([0-9]{13})\.([A-Za-z0-9_-]{43})$/.exec(assertionValue);
|
|
1442
|
+
if (!match) return false;
|
|
1443
|
+
const expiresAtMilliseconds = Number(match[1]);
|
|
1444
|
+
if (!Number.isSafeInteger(expiresAtMilliseconds) || expiresAtMilliseconds <= now.getTime() || expiresAtMilliseconds > now.getTime() + MAX_HANDOFF_ASSERTION_TTL_MS) {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
const expiresAt = new Date(expiresAtMilliseconds).toISOString();
|
|
1448
|
+
const expected = (0, import_node_crypto3.createHmac)("sha256", key).update(handoffPayload({ ...input, expiresAt })).digest();
|
|
1449
|
+
const received = Buffer.from(match[2], "base64url");
|
|
1450
|
+
return received.length === expected.length && (0, import_node_crypto3.timingSafeEqual)(received, expected);
|
|
1451
|
+
}
|
|
1452
|
+
function lifecycleOwner(binding) {
|
|
1453
|
+
return binding.lifecycleOwner === "AGENT_HOST" ? "AGENT_HOST" : "CONTO_RUNTIME";
|
|
1454
|
+
}
|
|
1455
|
+
function fail(code, message) {
|
|
1456
|
+
throw new BrowserbaseSessionError(code, message);
|
|
1457
|
+
}
|
|
1458
|
+
function requiredText2(value, field, maxLength = 255) {
|
|
1459
|
+
if (typeof value !== "string") fail("BROWSERBASE_CONFIG_INVALID", `${field} is invalid.`);
|
|
1460
|
+
const normalized = value.trim();
|
|
1461
|
+
if (!normalized || normalized.length > maxLength || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
1462
|
+
fail("BROWSERBASE_CONFIG_INVALID", `${field} is invalid.`);
|
|
1463
|
+
}
|
|
1464
|
+
return normalized;
|
|
1465
|
+
}
|
|
1466
|
+
function providerText(value, field, maxLength = 255) {
|
|
1467
|
+
if (typeof value !== "string") {
|
|
1468
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", `Browserbase returned an invalid ${field}.`);
|
|
1469
|
+
}
|
|
1470
|
+
const normalized = value.trim();
|
|
1471
|
+
if (!normalized || normalized.length > maxLength || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
1472
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", `Browserbase returned an invalid ${field}.`);
|
|
1473
|
+
}
|
|
1474
|
+
return normalized;
|
|
1475
|
+
}
|
|
1476
|
+
function sessionId(value, source = "config") {
|
|
1477
|
+
const code = source === "config" ? "BROWSERBASE_CONFIG_INVALID" : "BROWSERBASE_PROVIDER_RESPONSE_INVALID";
|
|
1478
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,200}$/.test(value)) {
|
|
1479
|
+
fail(code, "The Browserbase session ID is invalid.");
|
|
1480
|
+
}
|
|
1481
|
+
return value;
|
|
1482
|
+
}
|
|
1483
|
+
function timestamp(value, field) {
|
|
1484
|
+
const normalized = providerText(value, field, 100);
|
|
1485
|
+
const milliseconds = Date.parse(normalized);
|
|
1486
|
+
if (!Number.isFinite(milliseconds)) {
|
|
1487
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", `Browserbase returned an invalid ${field}.`);
|
|
1488
|
+
}
|
|
1489
|
+
return new Date(milliseconds).toISOString();
|
|
1490
|
+
}
|
|
1491
|
+
function timeoutSeconds(value) {
|
|
1492
|
+
const normalized = value ?? DEFAULT_SESSION_TIMEOUT_SECONDS;
|
|
1493
|
+
if (!Number.isSafeInteger(normalized) || normalized < MIN_SESSION_TIMEOUT_SECONDS || normalized > MAX_SESSION_TIMEOUT_SECONDS) {
|
|
1494
|
+
fail(
|
|
1495
|
+
"BROWSERBASE_CONFIG_INVALID",
|
|
1496
|
+
`timeoutSeconds must be an integer from ${MIN_SESSION_TIMEOUT_SECONDS} to ${MAX_SESSION_TIMEOUT_SECONDS}.`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
return normalized;
|
|
1500
|
+
}
|
|
1501
|
+
function requestTimeoutMs(value) {
|
|
1502
|
+
const normalized = value ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1503
|
+
if (!Number.isSafeInteger(normalized) || normalized < 1 || normalized > MAX_REQUEST_TIMEOUT_MS) {
|
|
1504
|
+
fail(
|
|
1505
|
+
"BROWSERBASE_CONFIG_INVALID",
|
|
1506
|
+
`requestTimeoutMs must be an integer from 1 to ${MAX_REQUEST_TIMEOUT_MS}.`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
return normalized;
|
|
1510
|
+
}
|
|
1511
|
+
function secureConnectUrl(value) {
|
|
1512
|
+
const normalized = providerText(value, "connectUrl", 4096);
|
|
1513
|
+
try {
|
|
1514
|
+
const url = new URL(normalized);
|
|
1515
|
+
if (url.protocol !== "wss:" || !BROWSERBASE_CONNECT_ORIGINS.includes(url.origin) || url.username || url.password || url.hash) {
|
|
1516
|
+
throw new Error("unsafe Browserbase connect URL");
|
|
1517
|
+
}
|
|
1518
|
+
return url.toString();
|
|
1519
|
+
} catch {
|
|
1520
|
+
fail(
|
|
1521
|
+
"BROWSERBASE_PROVIDER_RESPONSE_INVALID",
|
|
1522
|
+
"Browserbase returned an untrusted connection URL."
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function secureLiveViewUrl(value) {
|
|
1527
|
+
const normalized = providerText(value, "debuggerFullscreenUrl", 8192);
|
|
1528
|
+
try {
|
|
1529
|
+
const url = new URL(normalized);
|
|
1530
|
+
const trustedHostname = url.hostname === BROWSERBASE_LIVE_VIEW_DOMAIN || url.hostname.endsWith(`.${BROWSERBASE_LIVE_VIEW_DOMAIN}`);
|
|
1531
|
+
if (url.protocol !== "https:" || !trustedHostname || url.username || url.password || url.port && url.port !== "443") {
|
|
1532
|
+
throw new Error("unsafe Browserbase live view URL");
|
|
1533
|
+
}
|
|
1534
|
+
return url.toString();
|
|
1535
|
+
} catch {
|
|
1536
|
+
fail(
|
|
1537
|
+
"BROWSERBASE_PROVIDER_RESPONSE_INVALID",
|
|
1538
|
+
"Browserbase returned an untrusted live view URL."
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function parseProviderSession(value, expectedProjectId, expectedSessionId) {
|
|
1543
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1544
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned an invalid session.");
|
|
1545
|
+
}
|
|
1546
|
+
const record = value;
|
|
1547
|
+
const id = sessionId(record.id, "provider");
|
|
1548
|
+
const projectId = providerText(record.projectId, "projectId");
|
|
1549
|
+
const status = record.status;
|
|
1550
|
+
if (!["PENDING", "RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"].includes(
|
|
1551
|
+
typeof status === "string" ? status : ""
|
|
1552
|
+
)) {
|
|
1553
|
+
fail(
|
|
1554
|
+
"BROWSERBASE_PROVIDER_RESPONSE_INVALID",
|
|
1555
|
+
"Browserbase returned an invalid session status."
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (projectId !== expectedProjectId || expectedSessionId && id !== expectedSessionId) {
|
|
1559
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned a mismatched session.");
|
|
1560
|
+
}
|
|
1561
|
+
const createdAt = timestamp(record.createdAt, "createdAt");
|
|
1562
|
+
const expiresAt = timestamp(record.expiresAt, "expiresAt");
|
|
1563
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt)) {
|
|
1564
|
+
fail(
|
|
1565
|
+
"BROWSERBASE_PROVIDER_RESPONSE_INVALID",
|
|
1566
|
+
"Browserbase returned invalid session timestamps."
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
return Object.freeze({
|
|
1570
|
+
id,
|
|
1571
|
+
projectId,
|
|
1572
|
+
status,
|
|
1573
|
+
createdAt,
|
|
1574
|
+
expiresAt,
|
|
1575
|
+
connectUrl: secureConnectUrl(record.connectUrl)
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
async function readProviderJson(response) {
|
|
1579
|
+
let body;
|
|
1580
|
+
try {
|
|
1581
|
+
body = await response.text();
|
|
1582
|
+
} catch {
|
|
1583
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned an unreadable response.");
|
|
1584
|
+
}
|
|
1585
|
+
if (body.length > MAX_PROVIDER_RESPONSE_BYTES) {
|
|
1586
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned an oversized response.");
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
return JSON.parse(body);
|
|
1590
|
+
} catch {
|
|
1591
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned invalid JSON.");
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function activeProviderSession(session, now) {
|
|
1595
|
+
return (session.status === "PENDING" || session.status === "RUNNING") && Date.parse(session.expiresAt) > now.getTime();
|
|
1596
|
+
}
|
|
1597
|
+
function validBinding(value, ownership, expectedSessionId, now) {
|
|
1598
|
+
if (!value || typeof value !== "object") return false;
|
|
1599
|
+
return value.browserSessionId === expectedSessionId && value.organizationId === ownership.organizationId && value.agentId === ownership.agentId && value.projectId === ownership.projectId && value.state === "ACTIVE" && Number.isFinite(Date.parse(value.expiresAt)) && Date.parse(value.expiresAt) > now.getTime();
|
|
1600
|
+
}
|
|
1601
|
+
function createBrowserbaseSessionManager(config) {
|
|
1602
|
+
if (!config || typeof config !== "object") {
|
|
1603
|
+
fail("BROWSERBASE_CONFIG_INVALID", "The Browserbase configuration is required.");
|
|
1604
|
+
}
|
|
1605
|
+
const apiKey = requiredText2(config.apiKey, "browserbase.apiKey", 1024);
|
|
1606
|
+
if (apiKey.startsWith("conto_")) {
|
|
1607
|
+
fail("BROWSERBASE_CONFIG_INVALID", "A Browserbase API key is required.");
|
|
1608
|
+
}
|
|
1609
|
+
const projectId = requiredText2(config.projectId, "browserbase.projectId");
|
|
1610
|
+
const organizationId = requiredText2(config.organizationId, "browserbase.organizationId");
|
|
1611
|
+
const agentId = requiredText2(config.agentId, "browserbase.agentId");
|
|
1612
|
+
const contextId = config.contextId === void 0 ? void 0 : requiredText2(config.contextId, "browserbase.contextId");
|
|
1613
|
+
const sessionHandoffKey = handoffKey(config.handoffKey);
|
|
1614
|
+
if (!config.bindingStore || typeof config.bindingStore.create !== "function" || typeof config.bindingStore.get !== "function" || typeof config.bindingStore.revoke !== "function") {
|
|
1615
|
+
fail("BROWSERBASE_CONFIG_INVALID", "A durable Browserbase session binding store is required.");
|
|
1616
|
+
}
|
|
1617
|
+
if (contextId && typeof config.bindingStore.releaseContextLease !== "function") {
|
|
1618
|
+
fail(
|
|
1619
|
+
"BROWSERBASE_CONFIG_INVALID",
|
|
1620
|
+
"A durable Browserbase Context lease store is required when contextId is configured."
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
1624
|
+
if (typeof fetchImpl !== "function") {
|
|
1625
|
+
fail("BROWSERBASE_CONFIG_INVALID", "A fetch implementation is required.");
|
|
1626
|
+
}
|
|
1627
|
+
const durationSeconds = timeoutSeconds(config.timeoutSeconds);
|
|
1628
|
+
const providerRequestTimeoutMs = requestTimeoutMs(config.requestTimeoutMs);
|
|
1629
|
+
const now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
1630
|
+
const ownership = Object.freeze({ organizationId, agentId, projectId });
|
|
1631
|
+
async function providerRequest(path, init) {
|
|
1632
|
+
const controller = new AbortController();
|
|
1633
|
+
const timeout = setTimeout(() => controller.abort(), providerRequestTimeoutMs);
|
|
1634
|
+
try {
|
|
1635
|
+
return await fetchImpl(`${BROWSERBASE_API_ORIGIN}${path}`, {
|
|
1636
|
+
...init,
|
|
1637
|
+
redirect: "error",
|
|
1638
|
+
cache: "no-store",
|
|
1639
|
+
signal: controller.signal,
|
|
1640
|
+
headers: {
|
|
1641
|
+
Accept: "application/json",
|
|
1642
|
+
"X-BB-API-Key": apiKey,
|
|
1643
|
+
...init.headers
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
} catch {
|
|
1647
|
+
fail("BROWSERBASE_PROVIDER_UNAVAILABLE", "Browserbase could not be reached.");
|
|
1648
|
+
} finally {
|
|
1649
|
+
clearTimeout(timeout);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function releaseProviderSession(browserSessionId) {
|
|
1653
|
+
const response = await providerRequest(`/v1/sessions/${encodeURIComponent(browserSessionId)}`, {
|
|
1654
|
+
method: "POST",
|
|
1655
|
+
headers: { "Content-Type": "application/json" },
|
|
1656
|
+
body: JSON.stringify({ status: "REQUEST_RELEASE" })
|
|
1657
|
+
});
|
|
1658
|
+
if (!response.ok) {
|
|
1659
|
+
fail("BROWSERBASE_SESSION_RELEASE_FAILED", "Browserbase did not release the session.");
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return Object.freeze({
|
|
1663
|
+
async createSession() {
|
|
1664
|
+
const response = await providerRequest("/v1/sessions", {
|
|
1665
|
+
method: "POST",
|
|
1666
|
+
headers: { "Content-Type": "application/json" },
|
|
1667
|
+
body: JSON.stringify({
|
|
1668
|
+
projectId,
|
|
1669
|
+
timeout: durationSeconds,
|
|
1670
|
+
keepAlive: true,
|
|
1671
|
+
...contextId ? {
|
|
1672
|
+
browserSettings: {
|
|
1673
|
+
context: { id: contextId, persist: true }
|
|
1674
|
+
}
|
|
1675
|
+
} : {}
|
|
1676
|
+
})
|
|
1677
|
+
});
|
|
1678
|
+
if (response.status !== 201) {
|
|
1679
|
+
fail("BROWSERBASE_PROVIDER_UNAVAILABLE", "Browserbase did not create a session.");
|
|
1680
|
+
}
|
|
1681
|
+
const providerSession = parseProviderSession(await readProviderJson(response), projectId);
|
|
1682
|
+
if (!activeProviderSession(providerSession, now())) {
|
|
1683
|
+
try {
|
|
1684
|
+
await releaseProviderSession(providerSession.id);
|
|
1685
|
+
} catch {
|
|
1686
|
+
}
|
|
1687
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned an inactive session.");
|
|
1688
|
+
}
|
|
1689
|
+
const binding = Object.freeze({
|
|
1690
|
+
browserSessionId: providerSession.id,
|
|
1691
|
+
...ownership,
|
|
1692
|
+
lifecycleOwner: "CONTO_RUNTIME",
|
|
1693
|
+
state: "ACTIVE",
|
|
1694
|
+
createdAt: providerSession.createdAt,
|
|
1695
|
+
expiresAt: providerSession.expiresAt
|
|
1696
|
+
});
|
|
1697
|
+
let created = false;
|
|
1698
|
+
try {
|
|
1699
|
+
created = await config.bindingStore.create(binding);
|
|
1700
|
+
} catch {
|
|
1701
|
+
}
|
|
1702
|
+
if (!created) {
|
|
1703
|
+
try {
|
|
1704
|
+
await releaseProviderSession(providerSession.id);
|
|
1705
|
+
} catch {
|
|
1706
|
+
}
|
|
1707
|
+
fail(
|
|
1708
|
+
"BROWSERBASE_SESSION_BINDING_FAILED",
|
|
1709
|
+
"The Browserbase session ownership binding could not be stored."
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
return Object.freeze({
|
|
1713
|
+
browserSessionId: providerSession.id,
|
|
1714
|
+
status: providerSession.status,
|
|
1715
|
+
expiresAt: providerSession.expiresAt
|
|
1716
|
+
});
|
|
1717
|
+
},
|
|
1718
|
+
async attachSession(input) {
|
|
1719
|
+
if (!sessionHandoffKey) {
|
|
1720
|
+
fail(
|
|
1721
|
+
"BROWSERBASE_SESSION_HANDOFF_DISABLED",
|
|
1722
|
+
"Browserbase session handoff is not configured."
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
if (!input || typeof input !== "object") {
|
|
1726
|
+
fail("BROWSERBASE_SESSION_HANDOFF_INVALID", "The Browserbase session handoff is invalid.");
|
|
1727
|
+
}
|
|
1728
|
+
if (typeof input.browserSessionId !== "string" || !/^[A-Za-z0-9_-]{1,200}$/.test(input.browserSessionId)) {
|
|
1729
|
+
fail("BROWSERBASE_SESSION_HANDOFF_INVALID", "The Browserbase session handoff is invalid.");
|
|
1730
|
+
}
|
|
1731
|
+
const browserSessionId = input.browserSessionId;
|
|
1732
|
+
if (!verifyBrowserbaseSessionHandoffAssertion(
|
|
1733
|
+
input.handoffAssertion,
|
|
1734
|
+
{ browserSessionId, ...ownership },
|
|
1735
|
+
sessionHandoffKey,
|
|
1736
|
+
now()
|
|
1737
|
+
)) {
|
|
1738
|
+
fail("BROWSERBASE_SESSION_HANDOFF_INVALID", "The Browserbase session handoff is invalid.");
|
|
1739
|
+
}
|
|
1740
|
+
const response = await providerRequest(
|
|
1741
|
+
`/v1/sessions/${encodeURIComponent(browserSessionId)}`,
|
|
1742
|
+
{ method: "GET" }
|
|
1743
|
+
);
|
|
1744
|
+
if (response.status === 404) {
|
|
1745
|
+
fail("BROWSERBASE_SESSION_NOT_OWNED", "The Browserbase session is not owned.");
|
|
1746
|
+
}
|
|
1747
|
+
if (!response.ok) {
|
|
1748
|
+
fail("BROWSERBASE_PROVIDER_UNAVAILABLE", "Browserbase could not verify the session.");
|
|
1749
|
+
}
|
|
1750
|
+
const providerSession = parseProviderSession(
|
|
1751
|
+
await readProviderJson(response),
|
|
1752
|
+
projectId,
|
|
1753
|
+
browserSessionId
|
|
1754
|
+
);
|
|
1755
|
+
const currentTime = now();
|
|
1756
|
+
if (!activeProviderSession(providerSession, currentTime)) {
|
|
1757
|
+
fail("BROWSERBASE_SESSION_NOT_OWNED", "The Browserbase session is not active.");
|
|
1758
|
+
}
|
|
1759
|
+
const binding = Object.freeze({
|
|
1760
|
+
browserSessionId,
|
|
1761
|
+
...ownership,
|
|
1762
|
+
lifecycleOwner: "AGENT_HOST",
|
|
1763
|
+
state: "ACTIVE",
|
|
1764
|
+
createdAt: providerSession.createdAt,
|
|
1765
|
+
expiresAt: providerSession.expiresAt
|
|
1766
|
+
});
|
|
1767
|
+
let created = false;
|
|
1768
|
+
try {
|
|
1769
|
+
created = await config.bindingStore.create(binding);
|
|
1770
|
+
} catch {
|
|
1771
|
+
}
|
|
1772
|
+
if (!created) {
|
|
1773
|
+
let existing = null;
|
|
1774
|
+
try {
|
|
1775
|
+
existing = await config.bindingStore.get(browserSessionId);
|
|
1776
|
+
} catch {
|
|
1777
|
+
}
|
|
1778
|
+
if (!validBinding(existing, ownership, browserSessionId, currentTime) || lifecycleOwner(existing) !== "AGENT_HOST" || existing.createdAt !== providerSession.createdAt || existing.expiresAt !== providerSession.expiresAt) {
|
|
1779
|
+
fail(
|
|
1780
|
+
"BROWSERBASE_SESSION_BINDING_FAILED",
|
|
1781
|
+
"The Browserbase session ownership binding could not be stored."
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return Object.freeze({
|
|
1786
|
+
browserSessionId,
|
|
1787
|
+
status: providerSession.status,
|
|
1788
|
+
expiresAt: providerSession.expiresAt,
|
|
1789
|
+
lifecycleOwner: "AGENT_HOST"
|
|
1790
|
+
});
|
|
1791
|
+
},
|
|
1792
|
+
async connectUrlForSession(browserSessionIdValue) {
|
|
1793
|
+
const browserSessionId = sessionId(browserSessionIdValue);
|
|
1794
|
+
let binding;
|
|
1795
|
+
try {
|
|
1796
|
+
binding = await config.bindingStore.get(browserSessionId);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
if (!validBinding(binding, ownership, browserSessionId, now())) return null;
|
|
1801
|
+
const response = await providerRequest(
|
|
1802
|
+
`/v1/sessions/${encodeURIComponent(browserSessionId)}`,
|
|
1803
|
+
{
|
|
1804
|
+
method: "GET"
|
|
1805
|
+
}
|
|
1806
|
+
);
|
|
1807
|
+
if (!response.ok) return null;
|
|
1808
|
+
const providerSession = parseProviderSession(
|
|
1809
|
+
await readProviderJson(response),
|
|
1810
|
+
projectId,
|
|
1811
|
+
browserSessionId
|
|
1812
|
+
);
|
|
1813
|
+
if (!activeProviderSession(providerSession, now())) return null;
|
|
1814
|
+
let currentBinding;
|
|
1815
|
+
try {
|
|
1816
|
+
currentBinding = await config.bindingStore.get(browserSessionId);
|
|
1817
|
+
} catch {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
if (!validBinding(currentBinding, ownership, browserSessionId, now()) || providerSession.expiresAt !== binding.expiresAt || providerSession.expiresAt !== currentBinding.expiresAt) {
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
return providerSession.connectUrl;
|
|
1824
|
+
},
|
|
1825
|
+
async liveViewForSession(browserSessionIdValue) {
|
|
1826
|
+
const browserSessionId = sessionId(browserSessionIdValue);
|
|
1827
|
+
let binding;
|
|
1828
|
+
try {
|
|
1829
|
+
binding = await config.bindingStore.get(browserSessionId);
|
|
1830
|
+
} catch {
|
|
1831
|
+
fail("BROWSERBASE_LIVE_VIEW_UNAVAILABLE", "The Browserbase live view is unavailable.");
|
|
1832
|
+
}
|
|
1833
|
+
if (!validBinding(binding, ownership, browserSessionId, now())) {
|
|
1834
|
+
fail("BROWSERBASE_SESSION_NOT_OWNED", "The Browserbase session is not owned.");
|
|
1835
|
+
}
|
|
1836
|
+
const response = await providerRequest(
|
|
1837
|
+
`/v1/sessions/${encodeURIComponent(browserSessionId)}/debug`,
|
|
1838
|
+
{ method: "GET" }
|
|
1839
|
+
);
|
|
1840
|
+
if (!response.ok) {
|
|
1841
|
+
fail("BROWSERBASE_LIVE_VIEW_UNAVAILABLE", "The Browserbase live view is unavailable.");
|
|
1842
|
+
}
|
|
1843
|
+
const value = await readProviderJson(response);
|
|
1844
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1845
|
+
fail("BROWSERBASE_PROVIDER_RESPONSE_INVALID", "Browserbase returned an invalid live view.");
|
|
1846
|
+
}
|
|
1847
|
+
const liveViewUrl = secureLiveViewUrl(
|
|
1848
|
+
value.debuggerFullscreenUrl
|
|
1849
|
+
);
|
|
1850
|
+
let currentBinding;
|
|
1851
|
+
try {
|
|
1852
|
+
currentBinding = await config.bindingStore.get(browserSessionId);
|
|
1853
|
+
} catch {
|
|
1854
|
+
fail("BROWSERBASE_LIVE_VIEW_UNAVAILABLE", "The Browserbase live view is unavailable.");
|
|
1855
|
+
}
|
|
1856
|
+
if (!validBinding(currentBinding, ownership, browserSessionId, now()) || currentBinding.expiresAt !== binding.expiresAt) {
|
|
1857
|
+
fail("BROWSERBASE_SESSION_NOT_OWNED", "The Browserbase session is not owned.");
|
|
1858
|
+
}
|
|
1859
|
+
return Object.freeze({
|
|
1860
|
+
browserSessionId,
|
|
1861
|
+
liveViewUrl,
|
|
1862
|
+
expiresAt: currentBinding.expiresAt
|
|
1863
|
+
});
|
|
1864
|
+
},
|
|
1865
|
+
async revokeSession(browserSessionIdValue, expectedLifecycleOwner) {
|
|
1866
|
+
const browserSessionId = sessionId(browserSessionIdValue);
|
|
1867
|
+
let binding;
|
|
1868
|
+
try {
|
|
1869
|
+
binding = await config.bindingStore.get(browserSessionId);
|
|
1870
|
+
} catch {
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1873
|
+
if (!validBinding(binding, ownership, browserSessionId, now()) || lifecycleOwner(binding) !== expectedLifecycleOwner) {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
try {
|
|
1877
|
+
return await config.bindingStore.revoke({
|
|
1878
|
+
browserSessionId,
|
|
1879
|
+
...ownership,
|
|
1880
|
+
revokedAt: now().toISOString()
|
|
1881
|
+
});
|
|
1882
|
+
} catch {
|
|
1883
|
+
fail(
|
|
1884
|
+
"BROWSERBASE_SESSION_RELEASE_FAILED",
|
|
1885
|
+
"The Browserbase session ownership binding could not be revoked."
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
},
|
|
1889
|
+
async releaseContextLease(browserSessionIdValue) {
|
|
1890
|
+
if (!contextId) return true;
|
|
1891
|
+
const browserSessionId = sessionId(browserSessionIdValue);
|
|
1892
|
+
try {
|
|
1893
|
+
return await config.bindingStore.releaseContextLease({
|
|
1894
|
+
browserSessionId,
|
|
1895
|
+
...ownership
|
|
1896
|
+
});
|
|
1897
|
+
} catch {
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
releaseProviderSession
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
function createBrowserbaseGuardedPlaywrightCheckoutRuntimeHost(config) {
|
|
1905
|
+
if (!config || typeof config !== "object") {
|
|
1906
|
+
fail(
|
|
1907
|
+
"BROWSERBASE_CONFIG_INVALID",
|
|
1908
|
+
"The Browserbase guarded runtime configuration is required."
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
if (config.token === config.browserbase?.apiKey) {
|
|
1912
|
+
fail("BROWSERBASE_CONFIG_INVALID", "Use a dedicated browser runtime token.");
|
|
1913
|
+
}
|
|
1914
|
+
const manager = createBrowserbaseSessionManager(config.browserbase);
|
|
1915
|
+
const host = createGuardedPlaywrightCheckoutRuntimeHost({
|
|
1916
|
+
conto: config.conto,
|
|
1917
|
+
browserSessions: {
|
|
1918
|
+
allowedConnectOrigins: BROWSERBASE_CONNECT_ORIGINS,
|
|
1919
|
+
connectUrlForSession: manager.connectUrlForSession,
|
|
1920
|
+
connectOverCDP: config.connectOverCDP,
|
|
1921
|
+
selectPage: config.selectPage,
|
|
1922
|
+
maxSessions: config.maxSessions
|
|
1923
|
+
},
|
|
1924
|
+
targetStore: config.targetStore,
|
|
1925
|
+
dispatchAttemptFor: config.dispatchAttemptFor,
|
|
1926
|
+
token: config.token,
|
|
1927
|
+
maxBodyBytes: config.maxBodyBytes,
|
|
1928
|
+
clickTimeoutMs: config.clickTimeoutMs,
|
|
1929
|
+
popupTimeoutMs: config.popupTimeoutMs,
|
|
1930
|
+
targetTtlMs: config.targetTtlMs,
|
|
1931
|
+
now: config.now
|
|
1932
|
+
});
|
|
1933
|
+
return Object.freeze({
|
|
1934
|
+
handler: host.handler,
|
|
1935
|
+
createSession: manager.createSession,
|
|
1936
|
+
attachSession: manager.attachSession,
|
|
1937
|
+
observeSession: host.observeSession,
|
|
1938
|
+
liveViewForSession: manager.liveViewForSession,
|
|
1939
|
+
async releaseSession(browserSessionId) {
|
|
1940
|
+
const revoked = await manager.revokeSession(browserSessionId, "CONTO_RUNTIME");
|
|
1941
|
+
if (!revoked) {
|
|
1942
|
+
fail(
|
|
1943
|
+
"BROWSERBASE_SESSION_NOT_OWNED",
|
|
1944
|
+
"The Browserbase session is not active for this organization and agent."
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
const results = await Promise.allSettled([
|
|
1948
|
+
host.closeSession(browserSessionId),
|
|
1949
|
+
manager.releaseProviderSession(browserSessionId)
|
|
1950
|
+
]);
|
|
1951
|
+
const contextLeaseReleased = results[1].status === "fulfilled" ? await manager.releaseContextLease(browserSessionId) : false;
|
|
1952
|
+
if (results.some((result) => result.status === "rejected") || !contextLeaseReleased) {
|
|
1953
|
+
fail(
|
|
1954
|
+
"BROWSERBASE_SESSION_RELEASE_FAILED",
|
|
1955
|
+
"The Browserbase session could not be released."
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
async detachSession(browserSessionId) {
|
|
1960
|
+
const revoked = await manager.revokeSession(browserSessionId, "AGENT_HOST");
|
|
1961
|
+
if (!revoked) {
|
|
1962
|
+
fail(
|
|
1963
|
+
"BROWSERBASE_SESSION_NOT_OWNED",
|
|
1964
|
+
"The Browserbase session is not active for this organization and agent."
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
try {
|
|
1968
|
+
await host.closeSession(browserSessionId);
|
|
1969
|
+
} catch {
|
|
1970
|
+
fail(
|
|
1971
|
+
"BROWSERBASE_SESSION_RELEASE_FAILED",
|
|
1972
|
+
"The Browserbase session handoff could not be detached."
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1976
|
+
close: host.close
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1980
|
+
0 && (module.exports = {
|
|
1981
|
+
BROWSERBASE_CONNECT_ORIGINS,
|
|
1982
|
+
BrowserbaseSessionError,
|
|
1983
|
+
createBrowserbaseGuardedPlaywrightCheckoutRuntimeHost,
|
|
1984
|
+
createBrowserbaseSessionHandoffAssertion
|
|
1985
|
+
});
|