@orkestrel/browser 0.0.25 → 0.0.27
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 +23 -13
- package/dist/bin/main.js +11 -9
- package/dist/bin/main.js.map +1 -1
- package/dist/src/browser/index.d.ts +6 -184
- package/dist/src/browser/index.js +145 -119
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +2324 -1991
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +672 -1366
- package/dist/src/core/index.d.ts +672 -1366
- package/dist/src/core/index.js +2287 -1941
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +223 -215
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +72 -167
- package/dist/src/server/index.d.ts +72 -167
- package/dist/src/server/index.js +222 -209
- package/dist/src/server/index.js.map +1 -1
- package/package.json +8 -7
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let _src_core = require("../core/index.cjs");
|
|
3
|
-
let _orkestrel_contract = require("@orkestrel/contract");
|
|
4
2
|
let node_fs = require("node:fs");
|
|
5
3
|
let node_fs_promises = require("node:fs/promises");
|
|
6
4
|
let node_os = require("node:os");
|
|
7
5
|
let node_path = require("node:path");
|
|
8
6
|
let node_child_process = require("node:child_process");
|
|
7
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
8
|
+
let _src_core = require("../core/index.cjs");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
10
10
|
let node_http = require("node:http");
|
|
11
11
|
let node_https = require("node:https");
|
|
@@ -43,12 +43,15 @@ var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
|
43
43
|
*
|
|
44
44
|
* @remarks
|
|
45
45
|
* Disables account sync so extension installs cannot reset URL loader factories during page imports.
|
|
46
|
-
* Edge
|
|
46
|
+
* Disables Edge's implicit sign-in so fresh automation profiles carry no Microsoft account.
|
|
47
|
+
* Chromium ignores the unknown Edge feature name on Chrome and Chromium.
|
|
48
|
+
* The launcher merges disabled features with the caller's into one switch.
|
|
47
49
|
*/
|
|
48
50
|
var BROWSER_LAUNCH_ARGS = Object.freeze([
|
|
49
51
|
"--no-first-run",
|
|
50
52
|
"--no-default-browser-check",
|
|
51
|
-
"--disable-sync"
|
|
53
|
+
"--disable-sync",
|
|
54
|
+
"--disable-features=msImplicitSignin"
|
|
52
55
|
]);
|
|
53
56
|
/**
|
|
54
57
|
* Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
|
|
@@ -224,9 +227,9 @@ var BROWSER_RUN_DIRECTORY = "runs";
|
|
|
224
227
|
/** Bounds a file-store listing page by default. */
|
|
225
228
|
var BROWSER_FILE_STORE_LIMIT = 100;
|
|
226
229
|
/** Names the refusal when no browser can serve a call. */
|
|
227
|
-
var BROWSER_SERVER_UNAVAILABLE = "
|
|
230
|
+
var BROWSER_SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
|
|
228
231
|
/** Names a failed attempt to warm a browser. */
|
|
229
|
-
var BROWSER_SERVER_LAUNCH = "
|
|
232
|
+
var BROWSER_SERVER_LAUNCH = "SERVER_LAUNCH";
|
|
230
233
|
/** Sets the default number of warm browsers to 1. */
|
|
231
234
|
var BROWSER_SERVER_POOL_SIZE = 1;
|
|
232
235
|
/** Sets the default context capacity per browser, including the shared holder. */
|
|
@@ -240,21 +243,21 @@ var BROWSER_SERVER_RESTARTS = 1;
|
|
|
240
243
|
/** Names the browser process record in each profile. */
|
|
241
244
|
var BROWSER_SERVER_RECORD = "browse.json";
|
|
242
245
|
/** Names the diagnostic when the warm floor spends its restart bound. */
|
|
243
|
-
var BROWSER_SERVER_EXHAUSTED = "
|
|
246
|
+
var BROWSER_SERVER_EXHAUSTED = "SERVER_EXHAUSTED";
|
|
244
247
|
/** Names a failed browser server teardown. */
|
|
245
|
-
var BROWSER_SERVER_TEARDOWN = "
|
|
248
|
+
var BROWSER_SERVER_TEARDOWN = "SERVER_TEARDOWN";
|
|
246
249
|
/** Names a failed profile sweep. */
|
|
247
|
-
var BROWSER_SERVER_SWEEP = "
|
|
250
|
+
var BROWSER_SERVER_SWEEP = "SERVER_SWEEP";
|
|
248
251
|
/** Names a refused browser server option. */
|
|
249
|
-
var BROWSER_SERVER_OPTIONS = "
|
|
252
|
+
var BROWSER_SERVER_OPTIONS = "ARGUMENT";
|
|
250
253
|
/** Names the notice that a browser and its session state were lost. */
|
|
251
|
-
var BROWSER_SERVER_CRASH = "
|
|
254
|
+
var BROWSER_SERVER_CRASH = "SERVER_CRASH";
|
|
252
255
|
/** Names an interrupted call whose outcome is unknown. */
|
|
253
|
-
var BROWSER_SERVER_UNRESOLVED = "
|
|
256
|
+
var BROWSER_SERVER_UNRESOLVED = "SERVER_UNRESOLVED";
|
|
254
257
|
/** Names the refusal when every browser is admitted to a holder. */
|
|
255
|
-
var BROWSER_SERVER_BUSY = "
|
|
258
|
+
var BROWSER_SERVER_BUSY = "SERVER_BUSY";
|
|
256
259
|
/** Names the refusal for an unknown or ended holder. */
|
|
257
|
-
var BROWSER_SERVER_HOLDER = "
|
|
260
|
+
var BROWSER_SERVER_HOLDER = "SERVER_HOLDER";
|
|
258
261
|
/** Defines the holder tools independently of the shared browser's vocabulary. */
|
|
259
262
|
var BROWSER_SERVER_COPY = Object.freeze({
|
|
260
263
|
acquire: Object.freeze({
|
|
@@ -330,46 +333,6 @@ var BROWSER_SERVER_COPY = Object.freeze({
|
|
|
330
333
|
})
|
|
331
334
|
});
|
|
332
335
|
//#endregion
|
|
333
|
-
//#region src/server/errors.ts
|
|
334
|
-
/**
|
|
335
|
-
* Reports that an operation requiring an active connection was attempted while disconnected,
|
|
336
|
-
* under the code `BROWSER_NOT_CONNECTED_ERROR`.
|
|
337
|
-
*/
|
|
338
|
-
var BrowserNotConnectedError = class extends _src_core.BrowserError {
|
|
339
|
-
constructor(context) {
|
|
340
|
-
super("Browser is not connected", "BROWSER_NOT_CONNECTED_ERROR", context);
|
|
341
|
-
this.name = "BrowserNotConnectedError";
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
/**
|
|
345
|
-
* Reports that an operation was attempted after the browser wrapper was destroyed, under the
|
|
346
|
-
* code `BROWSER_DESTROYED_ERROR`.
|
|
347
|
-
*/
|
|
348
|
-
var BrowserDestroyedError = class extends _src_core.BrowserError {
|
|
349
|
-
constructor(context) {
|
|
350
|
-
super("Browser has been destroyed", "BROWSER_DESTROYED_ERROR", context);
|
|
351
|
-
this.name = "BrowserDestroyedError";
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
/**
|
|
355
|
-
* Narrows an unknown value to a `BrowserNotConnectedError`.
|
|
356
|
-
*
|
|
357
|
-
* @param value - Value to check
|
|
358
|
-
* @returns True if value is a BrowserNotConnectedError instance; false otherwise
|
|
359
|
-
*/
|
|
360
|
-
function isBrowserNotConnectedError(value) {
|
|
361
|
-
return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Narrows an unknown value to a `BrowserDestroyedError`.
|
|
365
|
-
*
|
|
366
|
-
* @param value - Value to check
|
|
367
|
-
* @returns True if value is a BrowserDestroyedError instance; false otherwise
|
|
368
|
-
*/
|
|
369
|
-
function isBrowserDestroyedError(value) {
|
|
370
|
-
return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
|
|
371
|
-
}
|
|
372
|
-
//#endregion
|
|
373
336
|
//#region src/server/helpers.ts
|
|
374
337
|
/**
|
|
375
338
|
* Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
|
|
@@ -432,15 +395,15 @@ function parseBrowserProfileRecord(text) {
|
|
|
432
395
|
* @param url - The lost page's last URL, when known
|
|
433
396
|
* @returns The coded diagnostic and any operation-lifecycle guidance
|
|
434
397
|
* @example
|
|
435
|
-
* describeBrowserServerLoss('
|
|
398
|
+
* describeBrowserServerLoss('SERVER_UNAVAILABLE', new Error('Browser exited'))
|
|
436
399
|
*/
|
|
437
400
|
function describeBrowserServerLoss(code, cause, url) {
|
|
438
401
|
const pid = (0, _src_core.isBrowserError)(cause) ? cause.context?.["pid"] : void 0;
|
|
439
402
|
const message = `${code}: ${(cause === void 0 ? "Browser session lost" : (0, _orkestrel_contract.isError)(cause) ? cause.message : String(cause)).replace(/\.+$/u, "")}${(0, _orkestrel_contract.isNumber)(pid) ? ` (pid ${pid})` : ""}.`;
|
|
440
|
-
if (code !== "
|
|
441
|
-
const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the
|
|
403
|
+
if (code !== "SERVER_CRASH" && code !== "SERVER_UNRESOLVED") return message;
|
|
404
|
+
const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the last projection, dialogs, holds, an unsaved recording, the active replay, and the isolated context's cookies.`;
|
|
442
405
|
const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
|
|
443
|
-
return code === "
|
|
406
|
+
return code === "SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
|
|
444
407
|
}
|
|
445
408
|
/**
|
|
446
409
|
* Formats a process identifier and UUID token as a lock entry name.
|
|
@@ -515,16 +478,6 @@ function findSystemBrowsers(options) {
|
|
|
515
478
|
return browsers;
|
|
516
479
|
}
|
|
517
480
|
/**
|
|
518
|
-
* Locates a Chrome/Chromium/Edge executable on this machine — the first entry
|
|
519
|
-
* of {@link findSystemBrowsers}.
|
|
520
|
-
*
|
|
521
|
-
* @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
|
|
522
|
-
* @returns The first discovered browser, or undefined
|
|
523
|
-
*/
|
|
524
|
-
function findSystemBrowser(options) {
|
|
525
|
-
return findSystemBrowsers(options)[0];
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
481
|
* Classifies an executable path/name into a {@link BrowserEngine} by
|
|
529
482
|
* case-insensitive hint, checked in the order edge → chromium → chrome.
|
|
530
483
|
*
|
|
@@ -580,7 +533,7 @@ async function removeBrowserProfile(profile) {
|
|
|
580
533
|
if (!profile.temporary) return;
|
|
581
534
|
const path = (0, node_path.resolve)(profile.path);
|
|
582
535
|
const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
|
|
583
|
-
if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("Refusing to remove an unsafe browser profile path",
|
|
536
|
+
if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("ARGUMENT", "Refusing to remove an unsafe browser profile path", { path });
|
|
584
537
|
await (0, node_fs_promises.rm)(path, {
|
|
585
538
|
recursive: true,
|
|
586
539
|
force: true,
|
|
@@ -687,6 +640,10 @@ function findStorePaths(base, platform) {
|
|
|
687
640
|
* Launches a browser process with raw-CDP debugging flags.
|
|
688
641
|
*
|
|
689
642
|
* @remarks
|
|
643
|
+
* Merges every caller and library `--disable-features` value into one switch because Chromium
|
|
644
|
+
* reads only the last occurrence. Keeps `msImplicitSignin` disabled alongside caller features;
|
|
645
|
+
* Chrome and Chromium ignore this Edge feature name. Preserves the caller's other arguments.
|
|
646
|
+
*
|
|
690
647
|
* POSIX launches own an isolated process group so lifecycle teardown can
|
|
691
648
|
* signal and await every Chromium subprocess without affecting the caller.
|
|
692
649
|
* Windows launches are not detached and own no group, so teardown there
|
|
@@ -704,8 +661,18 @@ function findStorePaths(base, platform) {
|
|
|
704
661
|
*/
|
|
705
662
|
function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
706
663
|
const args = [];
|
|
707
|
-
|
|
708
|
-
|
|
664
|
+
const features = /* @__PURE__ */ new Set();
|
|
665
|
+
for (const arg of [
|
|
666
|
+
...extra ?? [],
|
|
667
|
+
`--remote-debugging-port=${port ?? 0}`,
|
|
668
|
+
...BROWSER_LAUNCH_ARGS
|
|
669
|
+
]) {
|
|
670
|
+
if (arg === "--disable-features") continue;
|
|
671
|
+
if (arg.startsWith("--disable-features=")) {
|
|
672
|
+
for (const feature of arg.slice(19).split(",")) if (feature.trim() !== "") features.add(feature.trim());
|
|
673
|
+
} else args.push(arg);
|
|
674
|
+
}
|
|
675
|
+
args.push(`--disable-features=${[...features].join(",")}`);
|
|
709
676
|
if (headless) args.push(BROWSER_HEADLESS_ARG);
|
|
710
677
|
if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
|
|
711
678
|
return (0, node_child_process.spawn)(executable, args, {
|
|
@@ -756,18 +723,18 @@ function readBrowserEndpoint(stream, signal) {
|
|
|
756
723
|
stream.on("close", () => {
|
|
757
724
|
const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
|
|
758
725
|
if (endpoint !== void 0) read.resolve(endpoint);
|
|
759
|
-
else read.reject(new _src_core.
|
|
726
|
+
else read.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
|
|
760
727
|
});
|
|
761
728
|
return read.promise;
|
|
762
729
|
}
|
|
763
730
|
/**
|
|
764
731
|
* Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
|
|
765
|
-
* carrying either the targets or a coded `
|
|
732
|
+
* carrying either the targets or a coded `BrowserError`.
|
|
766
733
|
*
|
|
767
734
|
* @remarks
|
|
768
735
|
* The endpoint is a network boundary, so an unreachable host, a non-2xx
|
|
769
736
|
* response, and a body that is not a JSON array each come back as a failed
|
|
770
|
-
* `Result` carrying a coded `
|
|
737
|
+
* `Result` carrying a coded `BrowserError`. An entry missing a
|
|
771
738
|
* required string field is skipped rather than failing the whole list.
|
|
772
739
|
*
|
|
773
740
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
@@ -792,7 +759,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
792
759
|
const response = await fetch(url, { signal: controller.signal });
|
|
793
760
|
if (!response.ok) return {
|
|
794
761
|
success: false,
|
|
795
|
-
error: new _src_core.
|
|
762
|
+
error: new _src_core.BrowserError("CONNECTION", "CDP target list request was refused", {
|
|
796
763
|
url,
|
|
797
764
|
status: response.status
|
|
798
765
|
})
|
|
@@ -800,7 +767,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
800
767
|
const list = await response.json();
|
|
801
768
|
if (!(0, _orkestrel_contract.isArray)(list)) return {
|
|
802
769
|
success: false,
|
|
803
|
-
error: new _src_core.
|
|
770
|
+
error: new _src_core.BrowserError("CONNECTION", "CDP target list is not a JSON array", { url })
|
|
804
771
|
};
|
|
805
772
|
const targets = [];
|
|
806
773
|
for (const entry of list) {
|
|
@@ -820,9 +787,9 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
820
787
|
} catch (error) {
|
|
821
788
|
return {
|
|
822
789
|
success: false,
|
|
823
|
-
error: new _src_core.
|
|
790
|
+
error: new _src_core.BrowserError("CONNECTION", "CDP target list is unreachable", {
|
|
824
791
|
url,
|
|
825
|
-
error
|
|
792
|
+
error: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
|
|
826
793
|
})
|
|
827
794
|
};
|
|
828
795
|
} finally {
|
|
@@ -888,7 +855,7 @@ var WebSocketCDPTransport = class {
|
|
|
888
855
|
}
|
|
889
856
|
async send(data) {
|
|
890
857
|
const socket = this.#socket;
|
|
891
|
-
if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new _src_core.
|
|
858
|
+
if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new _src_core.BrowserError("CONNECTION", "WebSocket CDP transport is not open", { url: this.#url });
|
|
892
859
|
socket.send(data);
|
|
893
860
|
}
|
|
894
861
|
async close() {
|
|
@@ -896,12 +863,12 @@ var WebSocketCDPTransport = class {
|
|
|
896
863
|
}
|
|
897
864
|
async #start() {
|
|
898
865
|
const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
|
|
899
|
-
if (!parsed.success) throw new _src_core.
|
|
866
|
+
if (!parsed.success) throw new _src_core.BrowserError("CONNECTION", `WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
900
867
|
url: this.#url,
|
|
901
|
-
error: parsed.error
|
|
868
|
+
error: (0, _orkestrel_contract.isJSONValue)(parsed.error) ? parsed.error : String(parsed.error)
|
|
902
869
|
});
|
|
903
870
|
const url = parsed.value;
|
|
904
|
-
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new _src_core.
|
|
871
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
|
|
905
872
|
const key = (0, node_crypto.randomBytes)(16).toString("base64");
|
|
906
873
|
const deferred = Promise.withResolvers();
|
|
907
874
|
const options = {
|
|
@@ -933,7 +900,7 @@ var WebSocketCDPTransport = class {
|
|
|
933
900
|
async #close() {
|
|
934
901
|
const request = this.#request;
|
|
935
902
|
if (request !== void 0) {
|
|
936
|
-
this.#rejectRequest(request, new _src_core.
|
|
903
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
937
904
|
request.destroy();
|
|
938
905
|
}
|
|
939
906
|
await this.#starting.pending?.catch(() => void 0);
|
|
@@ -962,7 +929,7 @@ var WebSocketCDPTransport = class {
|
|
|
962
929
|
const accept = response.headers["sec-websocket-accept"];
|
|
963
930
|
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
964
931
|
socket.destroy();
|
|
965
|
-
this.#rejectRequest(request, new _src_core.
|
|
932
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
|
|
966
933
|
return;
|
|
967
934
|
}
|
|
968
935
|
try {
|
|
@@ -978,13 +945,13 @@ var WebSocketCDPTransport = class {
|
|
|
978
945
|
#decline(request, response) {
|
|
979
946
|
response.resume();
|
|
980
947
|
const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
|
|
981
|
-
this.#rejectRequest(request, new _src_core.
|
|
948
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
|
|
982
949
|
}
|
|
983
950
|
#fail(request, error) {
|
|
984
|
-
this.#rejectRequest(request, new _src_core.
|
|
951
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
|
|
985
952
|
}
|
|
986
953
|
#expire(request) {
|
|
987
|
-
this.#rejectRequest(request, new _src_core.
|
|
954
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
|
|
988
955
|
url: this.#url,
|
|
989
956
|
timeout: this.#timeout
|
|
990
957
|
}));
|
|
@@ -1056,7 +1023,7 @@ var FileBrowserWriter = class {
|
|
|
1056
1023
|
};
|
|
1057
1024
|
//#endregion
|
|
1058
1025
|
//#region package.json
|
|
1059
|
-
var version = "0.0.
|
|
1026
|
+
var version = "0.0.27";
|
|
1060
1027
|
//#endregion
|
|
1061
1028
|
//#region src/server/BrowserMCPServer.ts
|
|
1062
1029
|
/**
|
|
@@ -1064,9 +1031,9 @@ var version = "0.0.25";
|
|
|
1064
1031
|
* the Model Context Protocol on stdio, and warms Chromium at server start.
|
|
1065
1032
|
*
|
|
1066
1033
|
* @remarks
|
|
1067
|
-
* The server's own manager holds one dispatcher per name of the vocabulary — `
|
|
1068
|
-
* `
|
|
1069
|
-
* `
|
|
1034
|
+
* The server's own manager holds one dispatcher per name of the vocabulary — `read`, `click`,
|
|
1035
|
+
* `type`, `press`, `navigate`, `wait`, `dialog`, `switch`, `record`, `save`, `journeys`, `edit`,
|
|
1036
|
+
* `replay`, `forget`, and `capture` — each carrying the description, parameters, and annotations
|
|
1070
1037
|
* `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
|
|
1071
1038
|
* absent. After the launch, a tool the toolset's manager adds under another name, such as a page
|
|
1072
1039
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
@@ -1103,7 +1070,7 @@ var version = "0.0.25";
|
|
|
1103
1070
|
* ```ts
|
|
1104
1071
|
* import { BrowserMCPServer } from '@orkestrel/browser/server'
|
|
1105
1072
|
*
|
|
1106
|
-
* const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
|
|
1073
|
+
* const server = new BrowserMCPServer({ root: 'tmp/browsers', journeys: { readonly: true } })
|
|
1107
1074
|
* await server.start()
|
|
1108
1075
|
* await server.destroy()
|
|
1109
1076
|
* ```
|
|
@@ -1166,8 +1133,8 @@ var BrowserMCPServer = class {
|
|
|
1166
1133
|
constructor(options) {
|
|
1167
1134
|
const size = options?.pool?.size ?? 1;
|
|
1168
1135
|
const contexts = options?.pool?.contexts ?? 2;
|
|
1169
|
-
if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`,
|
|
1170
|
-
if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(`pool.contexts must be an integer from 1 through 4`,
|
|
1136
|
+
if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(BROWSER_SERVER_OPTIONS, `pool.size must be an integer from 1 through 3`, { subject: "server" });
|
|
1137
|
+
if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(BROWSER_SERVER_OPTIONS, `pool.contexts must be an integer from 1 through 4`, { subject: "server" });
|
|
1171
1138
|
this.#admission = size * contexts;
|
|
1172
1139
|
this.#log = options?.log ?? process.stderr;
|
|
1173
1140
|
this.#reference = this.#issue.bind(this);
|
|
@@ -1182,11 +1149,11 @@ var BrowserMCPServer = class {
|
|
|
1182
1149
|
restarts: 1
|
|
1183
1150
|
});
|
|
1184
1151
|
this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
|
|
1185
|
-
this.#headless = options?.headless ?? true;
|
|
1186
|
-
this.#executable = options?.executable;
|
|
1187
|
-
this.#viewport = options?.viewport;
|
|
1188
|
-
this.#readonly = options?.readonly ?? false;
|
|
1189
|
-
this.#launcher = options?.launch ?? createBrowser;
|
|
1152
|
+
this.#headless = options?.browser?.headless ?? true;
|
|
1153
|
+
this.#executable = options?.browser?.executable;
|
|
1154
|
+
this.#viewport = options?.browser?.viewport;
|
|
1155
|
+
this.#readonly = options?.journeys?.readonly ?? false;
|
|
1156
|
+
this.#launcher = options?.pool?.launch ?? createBrowser;
|
|
1190
1157
|
this.#input = options?.stdio?.input ?? process.stdin;
|
|
1191
1158
|
this.#tools = (0, _orkestrel_tool.createToolManager)();
|
|
1192
1159
|
const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
|
|
@@ -1250,7 +1217,7 @@ var BrowserMCPServer = class {
|
|
|
1250
1217
|
warming.then(this.#exhaust.bind(this));
|
|
1251
1218
|
} catch (error) {
|
|
1252
1219
|
if (this.#closing !== void 0) return;
|
|
1253
|
-
if ((0, _src_core.isBrowserError)(error) && error.code === "
|
|
1220
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "SERVER_UNAVAILABLE") throw error;
|
|
1254
1221
|
throw this.#unavailable((0, _orkestrel_pool.isPoolError)(error) ? error.cause ?? this.#failure : error);
|
|
1255
1222
|
}
|
|
1256
1223
|
}
|
|
@@ -1300,12 +1267,16 @@ var BrowserMCPServer = class {
|
|
|
1300
1267
|
signal: context.signal,
|
|
1301
1268
|
...context.caller === void 0 ? {} : { caller: context.caller }
|
|
1302
1269
|
});
|
|
1303
|
-
if (!result.success
|
|
1270
|
+
if (!result.success) return {
|
|
1271
|
+
...result,
|
|
1272
|
+
error: (0, _src_core.boundBrowserText)(result.error, _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER)
|
|
1273
|
+
};
|
|
1274
|
+
if (typeof result.value !== "string") return result;
|
|
1304
1275
|
return {
|
|
1305
1276
|
resultType: "complete",
|
|
1306
1277
|
content: [{
|
|
1307
1278
|
type: "text",
|
|
1308
|
-
text: result.value
|
|
1279
|
+
text: context.call.name === "acquire" || context.call.name === "tools" ? result.value : (0, _src_core.boundBrowserText)(result.value, _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER)
|
|
1309
1280
|
}]
|
|
1310
1281
|
};
|
|
1311
1282
|
}
|
|
@@ -1321,8 +1292,8 @@ var BrowserMCPServer = class {
|
|
|
1321
1292
|
context.signal.throwIfAborted();
|
|
1322
1293
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1323
1294
|
const purpose = args["purpose"];
|
|
1324
|
-
if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "
|
|
1325
|
-
if (this.#holders.size >= this.#admission) throw new _src_core.BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser
|
|
1295
|
+
if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("ARGUMENT", "purpose must describe the work", { subject: "toolset" });
|
|
1296
|
+
if (this.#holders.size >= this.#admission) throw new _src_core.BrowserError(BROWSER_SERVER_BUSY, `${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`);
|
|
1326
1297
|
const holder = {
|
|
1327
1298
|
id: (0, node_crypto.randomUUID)(),
|
|
1328
1299
|
purpose,
|
|
@@ -1349,14 +1320,14 @@ var BrowserMCPServer = class {
|
|
|
1349
1320
|
}
|
|
1350
1321
|
#holder(value) {
|
|
1351
1322
|
const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
|
|
1352
|
-
if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder
|
|
1323
|
+
if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_HOLDER, `${BROWSER_SERVER_HOLDER}: unknown or ended holder`);
|
|
1353
1324
|
return holder;
|
|
1354
1325
|
}
|
|
1355
1326
|
async #dispatch(args, context) {
|
|
1356
1327
|
const holder = this.#holder(args["holder"]);
|
|
1357
1328
|
const name = args["name"];
|
|
1358
1329
|
const parameters = args["arguments"];
|
|
1359
|
-
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "
|
|
1330
|
+
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("ARGUMENT", "execute requires name and arguments", { subject: "toolset" });
|
|
1360
1331
|
return this.#forward(holder, name, parameters, context);
|
|
1361
1332
|
}
|
|
1362
1333
|
async #catalog(args, context) {
|
|
@@ -1373,8 +1344,8 @@ var BrowserMCPServer = class {
|
|
|
1373
1344
|
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1374
1345
|
} catch (error) {
|
|
1375
1346
|
context.signal.throwIfAborted();
|
|
1376
|
-
if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call
|
|
1377
|
-
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`)
|
|
1347
|
+
if (holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
|
|
1348
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
|
|
1378
1349
|
throw error;
|
|
1379
1350
|
}
|
|
1380
1351
|
}
|
|
@@ -1411,7 +1382,7 @@ var BrowserMCPServer = class {
|
|
|
1411
1382
|
context.signal.throwIfAborted();
|
|
1412
1383
|
const journey = args["journey"];
|
|
1413
1384
|
if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
|
|
1414
|
-
if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError(`
|
|
1385
|
+
if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError("STORE_LOCKED", `STORE_LOCKED: Journey ${journey} is locked; call ${name} again.`, { name: journey });
|
|
1415
1386
|
const reader = Symbol(journey);
|
|
1416
1387
|
if (name === "forget") this.#writers.add(journey);
|
|
1417
1388
|
else {
|
|
@@ -1437,8 +1408,8 @@ var BrowserMCPServer = class {
|
|
|
1437
1408
|
lease = await this.#serve(holder, signal);
|
|
1438
1409
|
} catch (error) {
|
|
1439
1410
|
context.signal.throwIfAborted();
|
|
1440
|
-
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call
|
|
1441
|
-
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`)
|
|
1411
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
|
|
1412
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
|
|
1442
1413
|
throw error;
|
|
1443
1414
|
}
|
|
1444
1415
|
let result;
|
|
@@ -1463,7 +1434,7 @@ var BrowserMCPServer = class {
|
|
|
1463
1434
|
};
|
|
1464
1435
|
}
|
|
1465
1436
|
context.signal.throwIfAborted();
|
|
1466
|
-
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call"
|
|
1437
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
|
|
1467
1438
|
if (!result.success) {
|
|
1468
1439
|
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1469
1440
|
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
@@ -1471,9 +1442,9 @@ var BrowserMCPServer = class {
|
|
|
1471
1442
|
context.signal.throwIfAborted();
|
|
1472
1443
|
if (this.#leases.get(holder) !== lease) {
|
|
1473
1444
|
const loss = this.#departures.get(lease);
|
|
1474
|
-
throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease)
|
|
1445
|
+
throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
|
|
1475
1446
|
}
|
|
1476
|
-
throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
|
|
1447
|
+
throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
|
|
1477
1448
|
}
|
|
1478
1449
|
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1479
1450
|
}
|
|
@@ -1487,6 +1458,10 @@ var BrowserMCPServer = class {
|
|
|
1487
1458
|
}
|
|
1488
1459
|
return [...notes, value].join("\n");
|
|
1489
1460
|
}
|
|
1461
|
+
#notice(context) {
|
|
1462
|
+
const entry = [...this.#leases].find(([, lease]) => lease.generation.context === context);
|
|
1463
|
+
return entry === void 0 ? "" : this.#annotate(entry[0], "");
|
|
1464
|
+
}
|
|
1490
1465
|
async #handshake(options) {
|
|
1491
1466
|
try {
|
|
1492
1467
|
await this.#race(this.#starting, options.signal);
|
|
@@ -1637,7 +1612,7 @@ var BrowserMCPServer = class {
|
|
|
1637
1612
|
if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
|
|
1638
1613
|
this.#departures.set(lease, {
|
|
1639
1614
|
cause,
|
|
1640
|
-
url: lease.generation.toolset.view.url
|
|
1615
|
+
url: lease.generation.toolset.redact(lease.generation.toolset.view.url)
|
|
1641
1616
|
});
|
|
1642
1617
|
if (holder === this.#shared) this.#unmirror();
|
|
1643
1618
|
this.#leases.delete(holder);
|
|
@@ -1779,6 +1754,7 @@ var BrowserMCPServer = class {
|
|
|
1779
1754
|
}
|
|
1780
1755
|
toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1781
1756
|
context,
|
|
1757
|
+
notes: this.#notice.bind(this, context),
|
|
1782
1758
|
journeys: {
|
|
1783
1759
|
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1784
1760
|
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
@@ -1798,7 +1774,7 @@ var BrowserMCPServer = class {
|
|
|
1798
1774
|
} catch (error) {
|
|
1799
1775
|
await toolset?.destroy().catch(this.#fault.bind(this));
|
|
1800
1776
|
await context?.close().catch((failure) => {
|
|
1801
|
-
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !(0, _src_core.
|
|
1777
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
|
|
1802
1778
|
});
|
|
1803
1779
|
await (0, node_fs_promises.rm)(directory, {
|
|
1804
1780
|
recursive: true,
|
|
@@ -1808,7 +1784,8 @@ var BrowserMCPServer = class {
|
|
|
1808
1784
|
}
|
|
1809
1785
|
}
|
|
1810
1786
|
async #destroyRecord(slot) {
|
|
1811
|
-
|
|
1787
|
+
const cause = this.#losses.get(slot)?.cause;
|
|
1788
|
+
if ((0, _src_core.isBrowserError)(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
|
|
1812
1789
|
process.kill(slot.browser.pid, "SIGKILL");
|
|
1813
1790
|
} catch (error) {
|
|
1814
1791
|
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
@@ -1833,7 +1810,7 @@ var BrowserMCPServer = class {
|
|
|
1833
1810
|
}
|
|
1834
1811
|
async #warm() {
|
|
1835
1812
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1836
|
-
if (this.#stranded !== void 0) throw new _src_core.BrowserError((0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded),
|
|
1813
|
+
if (this.#stranded !== void 0) throw new _src_core.BrowserError(BROWSER_SERVER_TEARDOWN, (0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
|
|
1837
1814
|
const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
|
|
1838
1815
|
let browser;
|
|
1839
1816
|
let slot;
|
|
@@ -1906,7 +1883,7 @@ var BrowserMCPServer = class {
|
|
|
1906
1883
|
const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1907
1884
|
if (record === void 0) continue;
|
|
1908
1885
|
if (probeProcess(record.pid)) {
|
|
1909
|
-
const transport =
|
|
1886
|
+
const transport = createWebSocketCDPTransport({ url: record.endpoint });
|
|
1910
1887
|
const client = (0, _src_core.createCDPClient)({ transport });
|
|
1911
1888
|
try {
|
|
1912
1889
|
this.#abort.signal.throwIfAborted();
|
|
@@ -1948,7 +1925,7 @@ var BrowserMCPServer = class {
|
|
|
1948
1925
|
}
|
|
1949
1926
|
#unavailable(cause) {
|
|
1950
1927
|
const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
|
|
1951
|
-
return new _src_core.BrowserError(message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2)
|
|
1928
|
+
return new _src_core.BrowserError(BROWSER_SERVER_UNAVAILABLE, message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2));
|
|
1952
1929
|
}
|
|
1953
1930
|
#issue() {
|
|
1954
1931
|
return `e${++this.#references}`;
|
|
@@ -1980,7 +1957,7 @@ var BrowserMCPServer = class {
|
|
|
1980
1957
|
tools.emitter.off("clear", mirrored.cleared);
|
|
1981
1958
|
}
|
|
1982
1959
|
#ended() {
|
|
1983
|
-
return new _src_core.BrowserError("the browser session ended", "
|
|
1960
|
+
return new _src_core.BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
|
|
1984
1961
|
}
|
|
1985
1962
|
};
|
|
1986
1963
|
//#endregion
|
|
@@ -1993,31 +1970,32 @@ var BrowserMCPServer = class {
|
|
|
1993
1970
|
* const files = new FileBrowserStore({ root: directory })
|
|
1994
1971
|
* files.validateName('check-ready')
|
|
1995
1972
|
*/
|
|
1996
|
-
var FileBrowserStore = class {
|
|
1973
|
+
var FileBrowserStore = class FileBrowserStore {
|
|
1974
|
+
static #locks = /* @__PURE__ */ new Map();
|
|
1997
1975
|
#root;
|
|
1998
1976
|
#limit;
|
|
1999
1977
|
constructor(options) {
|
|
2000
1978
|
this.#limit = options.limit ?? 100;
|
|
2001
|
-
if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("The listing cap must be a positive integer", "
|
|
1979
|
+
if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("ARGUMENT", "The listing cap must be a positive integer", { subject: "store" });
|
|
2002
1980
|
try {
|
|
2003
1981
|
this.#root = (0, node_fs.realpathSync)(options.root);
|
|
2004
1982
|
} catch (error) {
|
|
2005
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new _src_core.BrowserError(`Missing root: ${options.root}`,
|
|
1983
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new _src_core.BrowserError("STORE_PATH", `Missing root: ${options.root}`, { path: options.root });
|
|
2006
1984
|
throw this.translateError(options.root, error);
|
|
2007
1985
|
}
|
|
2008
1986
|
}
|
|
2009
1987
|
/** Checks a journey name before filesystem access. @param name - Journey name */
|
|
2010
1988
|
validateName(name) {
|
|
2011
|
-
if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError(`Refused journey name: ${name}
|
|
1989
|
+
if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError("STORE_PATH", `Refused journey name: ${name}`);
|
|
2012
1990
|
}
|
|
2013
1991
|
/** Checks a run id before filesystem access. @param id - Producer id */
|
|
2014
1992
|
validateId(id) {
|
|
2015
|
-
if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError(`Refused run id: ${id}
|
|
1993
|
+
if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError("STORE_PATH", `Refused run id: ${id}`);
|
|
2016
1994
|
}
|
|
2017
1995
|
/** Resolves a confined target. @param parts - Relative components @returns Absolute target */
|
|
2018
1996
|
resolvePath(...parts) {
|
|
2019
1997
|
const path = (0, node_path.resolve)(this.#root, ...parts);
|
|
2020
|
-
if (path !== this.#root && !path.startsWith(this.#root.endsWith(node_path.sep) ? this.#root : this.#root + node_path.sep)) throw new _src_core.BrowserError(`Refused path: ${path}
|
|
1998
|
+
if (path !== this.#root && !path.startsWith(this.#root.endsWith(node_path.sep) ? this.#root : this.#root + node_path.sep)) throw new _src_core.BrowserError("STORE_PATH", `Refused path: ${path}`);
|
|
2021
1999
|
return path;
|
|
2022
2000
|
}
|
|
2023
2001
|
/**
|
|
@@ -2035,8 +2013,8 @@ var FileBrowserStore = class {
|
|
|
2035
2013
|
try {
|
|
2036
2014
|
const status = await (0, node_fs_promises.lstat)(current);
|
|
2037
2015
|
options?.signal?.throwIfAborted();
|
|
2038
|
-
if (status.isSymbolicLink()) throw new _src_core.BrowserError(`Refused symbolic link: ${current}
|
|
2039
|
-
if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError(`Not a directory: ${current}
|
|
2016
|
+
if (status.isSymbolicLink()) throw new _src_core.BrowserError("STORE_PATH", `Refused symbolic link: ${current}`);
|
|
2017
|
+
if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Not a directory: ${current}`);
|
|
2040
2018
|
} catch (error) {
|
|
2041
2019
|
options?.signal?.throwIfAborted();
|
|
2042
2020
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
@@ -2050,7 +2028,7 @@ var FileBrowserStore = class {
|
|
|
2050
2028
|
/** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
|
|
2051
2029
|
async createDirectory(path, options) {
|
|
2052
2030
|
if (await this.check(path, options)) return;
|
|
2053
|
-
if (path === this.#root) throw new _src_core.BrowserError(`Missing root: ${path}
|
|
2031
|
+
if (path === this.#root) throw new _src_core.BrowserError("STORE_PATH", `Missing root: ${path}`);
|
|
2054
2032
|
await this.createDirectory((0, node_path.dirname)(path), options);
|
|
2055
2033
|
options?.signal?.throwIfAborted();
|
|
2056
2034
|
try {
|
|
@@ -2100,7 +2078,7 @@ var FileBrowserStore = class {
|
|
|
2100
2078
|
await (0, node_fs_promises.rename)(temporary, path);
|
|
2101
2079
|
} catch (error) {
|
|
2102
2080
|
const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
|
|
2103
|
-
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError(`Cannot replace directory: ${path}`,
|
|
2081
|
+
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
|
|
2104
2082
|
throw error;
|
|
2105
2083
|
}
|
|
2106
2084
|
owned = false;
|
|
@@ -2131,6 +2109,20 @@ var FileBrowserStore = class {
|
|
|
2131
2109
|
* @returns The mutation result
|
|
2132
2110
|
*/
|
|
2133
2111
|
async lock(path, action, options) {
|
|
2112
|
+
const key = this.resolvePath(path);
|
|
2113
|
+
const previous = FileBrowserStore.#locks.get(key);
|
|
2114
|
+
const released = Promise.withResolvers();
|
|
2115
|
+
FileBrowserStore.#locks.set(key, released.promise);
|
|
2116
|
+
try {
|
|
2117
|
+
await previous;
|
|
2118
|
+
options?.signal?.throwIfAborted();
|
|
2119
|
+
return await this.#lockFile(key, action, options);
|
|
2120
|
+
} finally {
|
|
2121
|
+
released.resolve();
|
|
2122
|
+
if (FileBrowserStore.#locks.get(key) === released.promise) FileBrowserStore.#locks.delete(key);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async #lockFile(path, action, options) {
|
|
2134
2126
|
await this.createDirectory((0, node_path.dirname)(path), options);
|
|
2135
2127
|
const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
|
|
2136
2128
|
const entry = this.resolvePath(path, name);
|
|
@@ -2173,7 +2165,7 @@ var FileBrowserStore = class {
|
|
|
2173
2165
|
await this.#removeLock(path);
|
|
2174
2166
|
}
|
|
2175
2167
|
}
|
|
2176
|
-
throw new _src_core.BrowserError(`Journey is locked: ${path}
|
|
2168
|
+
throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2177
2169
|
}
|
|
2178
2170
|
/**
|
|
2179
2171
|
* Lists readable entries in sorted order with a bounded page and one extra readable probe.
|
|
@@ -2220,7 +2212,7 @@ var FileBrowserStore = class {
|
|
|
2220
2212
|
const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
|
|
2221
2213
|
faults.push({
|
|
2222
2214
|
name,
|
|
2223
|
-
reason: typeof reason === "string" ? reason : error instanceof _src_core.BrowserError && error.code === "
|
|
2215
|
+
reason: typeof reason === "string" ? reason : error instanceof _src_core.BrowserError && error.code === "STORE_PATH" ? "The entry path is refused" : error instanceof _src_core.BrowserError && error.code === "STORE_ACCESS" ? "Permission denied" : error instanceof _src_core.BrowserError && error.code === "STORE_FORMAT" ? "Unknown file format" : "The stored entry is malformed or unreadable"
|
|
2224
2216
|
});
|
|
2225
2217
|
}
|
|
2226
2218
|
}
|
|
@@ -2263,7 +2255,7 @@ var FileBrowserStore = class {
|
|
|
2263
2255
|
/** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
|
|
2264
2256
|
translateError(path, error) {
|
|
2265
2257
|
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
2266
|
-
return new _src_core.BrowserError(
|
|
2258
|
+
return new _src_core.BrowserError(code === "EACCES" || code === "EPERM" ? "STORE_ACCESS" : error instanceof _src_core.BrowserError && code !== "JOURNEY_INVALID" ? error.code : "STORE_FILE", `${path}: ${error instanceof Error ? error.message : String(error)}`, { path });
|
|
2267
2259
|
}
|
|
2268
2260
|
async #readLock(path, options) {
|
|
2269
2261
|
await this.check(path, options);
|
|
@@ -2271,13 +2263,13 @@ var FileBrowserStore = class {
|
|
|
2271
2263
|
const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true });
|
|
2272
2264
|
for (const entry of entries) {
|
|
2273
2265
|
await this.check(this.resolvePath(path, entry.name), options);
|
|
2274
|
-
if (!entry.isFile()) throw new _src_core.BrowserError(`Journey is locked: ${path}
|
|
2266
|
+
if (!entry.isFile()) throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2275
2267
|
}
|
|
2276
2268
|
return entries.map((entry) => entry.name);
|
|
2277
2269
|
} catch (error) {
|
|
2278
2270
|
if (error instanceof Error && "code" in error) {
|
|
2279
2271
|
if (error.code === "ENOENT") return void 0;
|
|
2280
|
-
if (error.code === "ENOTDIR") throw new _src_core.BrowserError(`Journey is locked: ${path}
|
|
2272
|
+
if (error.code === "ENOTDIR") throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2281
2273
|
}
|
|
2282
2274
|
throw this.translateError(path, error);
|
|
2283
2275
|
}
|
|
@@ -2289,7 +2281,7 @@ var FileBrowserStore = class {
|
|
|
2289
2281
|
return true;
|
|
2290
2282
|
} catch (error) {
|
|
2291
2283
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
|
|
2292
|
-
throw new _src_core.BrowserError(`Cannot remove lock entry: ${path}`,
|
|
2284
|
+
throw new _src_core.BrowserError("STORE_ACCESS", `Cannot remove lock entry: ${path}`, { path });
|
|
2293
2285
|
}
|
|
2294
2286
|
}
|
|
2295
2287
|
async #removeLock(path) {
|
|
@@ -2305,7 +2297,7 @@ var FileBrowserStore = class {
|
|
|
2305
2297
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
|
|
2306
2298
|
}
|
|
2307
2299
|
}
|
|
2308
|
-
throw new _src_core.BrowserError(`Cannot remove lock directory: ${path}`,
|
|
2300
|
+
throw new _src_core.BrowserError("STORE_ACCESS", `Cannot remove lock directory: ${path}`, { path });
|
|
2309
2301
|
}
|
|
2310
2302
|
}
|
|
2311
2303
|
};
|
|
@@ -2331,24 +2323,29 @@ var FileBrowserJourneyStore = class {
|
|
|
2331
2323
|
if (source === void 0) return void 0;
|
|
2332
2324
|
try {
|
|
2333
2325
|
const value = (0, _orkestrel_contract.parseJSON)(source);
|
|
2334
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new _src_core.BrowserError("Malformed journey revision"
|
|
2335
|
-
if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("Missing journey format"
|
|
2326
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new _src_core.BrowserError("STORE_FILE", "Malformed journey revision");
|
|
2327
|
+
if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("STORE_FILE", "Missing journey format");
|
|
2336
2328
|
(0, _src_core.validateBrowserJourney)(value["journey"]);
|
|
2337
|
-
if (value["journey"].name !== name) throw new _src_core.BrowserError("Journey name differs from its directory"
|
|
2329
|
+
if (value["journey"].name !== name) throw new _src_core.BrowserError("STORE_FILE", "Journey name differs from its directory");
|
|
2338
2330
|
return {
|
|
2339
2331
|
journey: value["journey"],
|
|
2340
2332
|
revision: value["revision"]
|
|
2341
2333
|
};
|
|
2342
2334
|
} catch (error) {
|
|
2343
2335
|
const translated = this.#files.translateError(path, error);
|
|
2344
|
-
throw new _src_core.BrowserError(translated.
|
|
2336
|
+
throw new _src_core.BrowserError(translated.code, translated.message, {
|
|
2345
2337
|
...translated.context,
|
|
2346
2338
|
reason: (0, _src_core.normalizeBrowserJourneyReason)(error)
|
|
2347
2339
|
});
|
|
2348
2340
|
}
|
|
2349
2341
|
}
|
|
2350
|
-
async set(journey,
|
|
2342
|
+
async set(journey, options) {
|
|
2351
2343
|
options?.signal?.throwIfAborted();
|
|
2344
|
+
(0, _src_core.validateBrowserJourneyWriteOptions)(options);
|
|
2345
|
+
const condition = {
|
|
2346
|
+
revision: options?.revision,
|
|
2347
|
+
exclusive: options?.exclusive
|
|
2348
|
+
};
|
|
2352
2349
|
this.#files.validateName(journey.name);
|
|
2353
2350
|
(0, _src_core.validateBrowserJourney)(journey);
|
|
2354
2351
|
const owned = structuredClone(journey);
|
|
@@ -2356,12 +2353,12 @@ var FileBrowserJourneyStore = class {
|
|
|
2356
2353
|
const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
|
|
2357
2354
|
return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
|
|
2358
2355
|
const current = await this.get(owned.name, options);
|
|
2359
|
-
if (
|
|
2356
|
+
if (condition.exclusive === true && current !== void 0 || condition.revision !== void 0 && condition.revision !== current?.revision) throw new _src_core.BrowserError("JOURNEY_STALE", `Journey ${owned.name} changed since you read it`);
|
|
2360
2357
|
const source = await this.#files.read(counter, options);
|
|
2361
2358
|
const previous = source === void 0 ? 0 : Number(source);
|
|
2362
|
-
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}
|
|
2359
|
+
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
|
|
2363
2360
|
const revision = Math.max(previous, current?.revision ?? 0) + 1;
|
|
2364
|
-
if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError(`Exhausted revision: ${counter}
|
|
2361
|
+
if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError("STORE_FILE", `Exhausted revision: ${counter}`);
|
|
2365
2362
|
const saved = {
|
|
2366
2363
|
journey: owned,
|
|
2367
2364
|
revision
|
|
@@ -2386,7 +2383,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2386
2383
|
const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
|
|
2387
2384
|
const source = await this.#files.read(counter, options);
|
|
2388
2385
|
const previous = source === void 0 ? 0 : Number(source);
|
|
2389
|
-
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}
|
|
2386
|
+
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
|
|
2390
2387
|
await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
|
|
2391
2388
|
await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
|
|
2392
2389
|
}, options);
|
|
@@ -2408,7 +2405,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2408
2405
|
* still refuses conflicting mutations and allocations from another process.
|
|
2409
2406
|
* @example
|
|
2410
2407
|
* const store = new FileBrowserRunStore({ root: directory })
|
|
2411
|
-
* const slot = await store.
|
|
2408
|
+
* const slot = await store.create('check-ready')
|
|
2412
2409
|
*/
|
|
2413
2410
|
var FileBrowserRunStore = class FileBrowserRunStore {
|
|
2414
2411
|
static #allocations = /* @__PURE__ */ new Map();
|
|
@@ -2418,7 +2415,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2418
2415
|
constructor(options) {
|
|
2419
2416
|
this.#files = new FileBrowserStore(options);
|
|
2420
2417
|
}
|
|
2421
|
-
async
|
|
2418
|
+
async write(bytes, options) {
|
|
2422
2419
|
options?.signal?.throwIfAborted();
|
|
2423
2420
|
const owned = new Uint8Array(bytes);
|
|
2424
2421
|
const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
|
|
@@ -2436,7 +2433,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2436
2433
|
throw this.#files.translateError(path, error);
|
|
2437
2434
|
}
|
|
2438
2435
|
}
|
|
2439
|
-
async
|
|
2436
|
+
async create(name, options) {
|
|
2440
2437
|
options?.signal?.throwIfAborted();
|
|
2441
2438
|
this.#files.validateName(name);
|
|
2442
2439
|
const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
|
|
@@ -2470,10 +2467,10 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2470
2467
|
if (source === void 0) return void 0;
|
|
2471
2468
|
try {
|
|
2472
2469
|
const value = (0, _orkestrel_contract.parseJSON)(source);
|
|
2473
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("Missing run format"
|
|
2474
|
-
if (value["format"] === _src_core.BROWSER_JOURNEY_FORMAT_VERSION && (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"]))) throw new _src_core.BrowserError("Missing run journey format"
|
|
2470
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("STORE_FILE", "Missing run format");
|
|
2471
|
+
if (value["format"] === _src_core.BROWSER_JOURNEY_FORMAT_VERSION && (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"]))) throw new _src_core.BrowserError("STORE_FILE", "Missing run journey format");
|
|
2475
2472
|
(0, _src_core.validateBrowserRun)(value);
|
|
2476
|
-
if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("Run identity differs from its directory"
|
|
2473
|
+
if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("STORE_FILE", "Run identity differs from its directory");
|
|
2477
2474
|
return value;
|
|
2478
2475
|
} catch (error) {
|
|
2479
2476
|
throw this.#files.translateError(path, error);
|
|
@@ -2492,7 +2489,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2492
2489
|
async capture(slot, name, bytes, options) {
|
|
2493
2490
|
options?.signal?.throwIfAborted();
|
|
2494
2491
|
const directory = this.#slots.get(slot);
|
|
2495
|
-
if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new _src_core.BrowserError("Capture requires an opened slot and an sN.png name"
|
|
2492
|
+
if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new _src_core.BrowserError("STORE_PATH", "Capture requires an opened slot and an sN.png name");
|
|
2496
2493
|
const owned = new Uint8Array(bytes);
|
|
2497
2494
|
await this.#requireDirectory(directory, options);
|
|
2498
2495
|
await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
|
|
@@ -2545,12 +2542,12 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2545
2542
|
return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
|
|
2546
2543
|
}
|
|
2547
2544
|
async #requireDirectory(directory, options) {
|
|
2548
|
-
if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new _src_core.BrowserError(`Run directory was not opened or is missing: ${directory}
|
|
2545
|
+
if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new _src_core.BrowserError("STORE_PATH", `Run directory was not opened or is missing: ${directory}`);
|
|
2549
2546
|
try {
|
|
2550
2547
|
options?.signal?.throwIfAborted();
|
|
2551
2548
|
const status = await (0, node_fs_promises.lstat)(directory);
|
|
2552
2549
|
options?.signal?.throwIfAborted();
|
|
2553
|
-
if (!status.isDirectory()) throw new _src_core.BrowserError(`Run directory is not a directory: ${directory}
|
|
2550
|
+
if (!status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Run directory is not a directory: ${directory}`);
|
|
2554
2551
|
} catch (error) {
|
|
2555
2552
|
options?.signal?.throwIfAborted();
|
|
2556
2553
|
throw this.#files.translateError(directory, error);
|
|
@@ -2587,7 +2584,7 @@ function createBrowser(options) {
|
|
|
2587
2584
|
* @param options - The CDP WebSocket debugger URL (and optional timeout)
|
|
2588
2585
|
* @returns A {@link CDPTransportInterface}
|
|
2589
2586
|
*/
|
|
2590
|
-
function
|
|
2587
|
+
function createWebSocketCDPTransport(options) {
|
|
2591
2588
|
return new WebSocketCDPTransport(options);
|
|
2592
2589
|
}
|
|
2593
2590
|
/**
|
|
@@ -2596,7 +2593,7 @@ function createCDPTransport(options) {
|
|
|
2596
2593
|
*
|
|
2597
2594
|
* @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
|
|
2598
2595
|
*/
|
|
2599
|
-
function
|
|
2596
|
+
function createFileBrowserWriter() {
|
|
2600
2597
|
return new FileBrowserWriter();
|
|
2601
2598
|
}
|
|
2602
2599
|
/**
|
|
@@ -2630,7 +2627,7 @@ function createFileBrowserRunStore(options) {
|
|
|
2630
2627
|
* ```ts
|
|
2631
2628
|
* import { createBrowserMCPServer } from '@orkestrel/browser/server'
|
|
2632
2629
|
*
|
|
2633
|
-
* const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
|
|
2630
|
+
* const server = createBrowserMCPServer({ root: 'tmp/browsers', browser: { headless: true } })
|
|
2634
2631
|
* await server.start() // resolves after leasing a connected browser
|
|
2635
2632
|
* await server.destroy()
|
|
2636
2633
|
* ```
|
|
@@ -2685,7 +2682,7 @@ var Browser = class {
|
|
|
2685
2682
|
...options?.error !== void 0 ? { error: options.error } : {}
|
|
2686
2683
|
});
|
|
2687
2684
|
this.#options = options ?? {};
|
|
2688
|
-
this.#engine = this.#options.
|
|
2685
|
+
this.#engine = this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : this.#options.browsers?.engine ?? "chromium";
|
|
2689
2686
|
this.#cdpPort = this.#options.cdp?.port ?? 9222;
|
|
2690
2687
|
this.#cdpHost = this.#options.cdp?.host ?? "127.0.0.1";
|
|
2691
2688
|
queueMicrotask(() => this.#emitter.emit("idle"));
|
|
@@ -2712,9 +2709,9 @@ var Browser = class {
|
|
|
2712
2709
|
return this.#endpoint;
|
|
2713
2710
|
}
|
|
2714
2711
|
async ping(options) {
|
|
2715
|
-
if (this.#destroyed) throw new
|
|
2712
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2716
2713
|
const client = this.#client;
|
|
2717
|
-
if (this.#status !== "connected" || client === void 0) throw new
|
|
2714
|
+
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2718
2715
|
await client.send("Browser.getVersion", void 0, options);
|
|
2719
2716
|
}
|
|
2720
2717
|
async discover() {
|
|
@@ -2723,7 +2720,7 @@ var Browser = class {
|
|
|
2723
2720
|
return result;
|
|
2724
2721
|
}
|
|
2725
2722
|
async connect() {
|
|
2726
|
-
if (this.#destroyed) throw new
|
|
2723
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2727
2724
|
const active = this.#connecting.pending;
|
|
2728
2725
|
if (active !== void 0) {
|
|
2729
2726
|
await active;
|
|
@@ -2733,8 +2730,8 @@ var Browser = class {
|
|
|
2733
2730
|
await this.#connecting.execute(() => this.#establish());
|
|
2734
2731
|
}
|
|
2735
2732
|
adopt() {
|
|
2736
|
-
if (this.#destroyed) throw new
|
|
2737
|
-
if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new
|
|
2733
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2734
|
+
if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2738
2735
|
this.#owned = true;
|
|
2739
2736
|
}
|
|
2740
2737
|
async disconnect() {
|
|
@@ -2765,12 +2762,15 @@ var Browser = class {
|
|
|
2765
2762
|
}
|
|
2766
2763
|
}
|
|
2767
2764
|
async create(options) {
|
|
2768
|
-
if (this.#destroyed) throw new
|
|
2765
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2769
2766
|
const client = this.#client;
|
|
2770
|
-
if (this.#status !== "connected" || client === void 0) throw new
|
|
2767
|
+
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2771
2768
|
let context = this.#contexts[0];
|
|
2772
2769
|
if (context === void 0) {
|
|
2773
|
-
context = new _src_core.BrowserContext(client,
|
|
2770
|
+
context = new _src_core.BrowserContext(client, {
|
|
2771
|
+
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
2772
|
+
writer: createFileBrowserWriter()
|
|
2773
|
+
});
|
|
2774
2774
|
this.#registerContext(context);
|
|
2775
2775
|
}
|
|
2776
2776
|
const page = await context.create(options);
|
|
@@ -2798,10 +2798,11 @@ var Browser = class {
|
|
|
2798
2798
|
return shutdown;
|
|
2799
2799
|
}
|
|
2800
2800
|
async #isolate(options) {
|
|
2801
|
-
if (this.#destroyed) throw new
|
|
2801
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2802
2802
|
const client = this.#client;
|
|
2803
|
-
if (this.#status !== "connected" || client === void 0) throw new
|
|
2803
|
+
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2804
2804
|
(0, _src_core.validateBrowserContextOptions)(options);
|
|
2805
|
+
if (options !== void 0 && "id" in options && options.id !== void 0) throw new _src_core.BrowserError("ARGUMENT", "An isolated context receives its id from the browser");
|
|
2805
2806
|
const params = { disposeOnDetach: false };
|
|
2806
2807
|
if (options?.proxy !== void 0) {
|
|
2807
2808
|
params["proxyServer"] = options.proxy.server;
|
|
@@ -2809,18 +2810,25 @@ var Browser = class {
|
|
|
2809
2810
|
}
|
|
2810
2811
|
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2811
2812
|
const result = await client.send("Target.createBrowserContext", params);
|
|
2812
|
-
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.
|
|
2813
|
+
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserError("CONNECTION", "Failed to create isolated browser context");
|
|
2813
2814
|
const id = result["browserContextId"];
|
|
2814
|
-
const
|
|
2815
|
+
const { proxy: _proxy, origins: _origins, ...defaults } = options ?? {};
|
|
2816
|
+
const viewport = options?.viewport ?? options?.emulation?.viewport ?? this.#options.viewport;
|
|
2817
|
+
const context = new _src_core.BrowserContext(client, {
|
|
2818
|
+
...defaults,
|
|
2819
|
+
id,
|
|
2820
|
+
...viewport === void 0 ? {} : { viewport },
|
|
2821
|
+
writer: options?.writer ?? createFileBrowserWriter()
|
|
2822
|
+
});
|
|
2815
2823
|
try {
|
|
2816
|
-
if (this.#destroyed || this.#client !== client) throw new
|
|
2824
|
+
if (this.#destroyed || this.#client !== client) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2817
2825
|
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
2818
2826
|
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
2819
2827
|
browserContextId: id,
|
|
2820
2828
|
downloadPath: options.downloads.path,
|
|
2821
2829
|
eventsEnabled: true
|
|
2822
2830
|
});
|
|
2823
|
-
if (this.#destroyed || this.#client !== client) throw new
|
|
2831
|
+
if (this.#destroyed || this.#client !== client) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2824
2832
|
} catch (error) {
|
|
2825
2833
|
await context.close().catch(() => void 0);
|
|
2826
2834
|
throw error;
|
|
@@ -2832,7 +2840,7 @@ var Browser = class {
|
|
|
2832
2840
|
const disconnecting = this.#disconnecting.pending;
|
|
2833
2841
|
if (disconnecting !== void 0) await disconnecting;
|
|
2834
2842
|
await this.#settleExit();
|
|
2835
|
-
if (this.#destroyed) throw new
|
|
2843
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2836
2844
|
if (this.#status === "connected") return;
|
|
2837
2845
|
this.#assertNotAborted();
|
|
2838
2846
|
this.#status = "connecting";
|
|
@@ -2857,12 +2865,12 @@ var Browser = class {
|
|
|
2857
2865
|
this.#assertNotAborted();
|
|
2858
2866
|
await this.#launch();
|
|
2859
2867
|
} catch (error) {
|
|
2860
|
-
if (this.#destroyed) throw new
|
|
2868
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2861
2869
|
this.#status = "error";
|
|
2862
2870
|
this.#emitter.emit("error", error);
|
|
2863
|
-
if ((0, _src_core.
|
|
2871
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
|
|
2864
2872
|
const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
|
|
2865
|
-
throw new _src_core.
|
|
2873
|
+
throw new _src_core.BrowserError("CONNECTION", message, this.#options.executable === void 0 ? void 0 : { executable: this.#options.executable });
|
|
2866
2874
|
}
|
|
2867
2875
|
}
|
|
2868
2876
|
async #detach() {
|
|
@@ -2883,7 +2891,7 @@ var Browser = class {
|
|
|
2883
2891
|
this.#emitter.emit("idle");
|
|
2884
2892
|
}
|
|
2885
2893
|
#assertNotAborted() {
|
|
2886
|
-
if (this.#signal().aborted) throw new _src_core.
|
|
2894
|
+
if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
|
|
2887
2895
|
}
|
|
2888
2896
|
#signal() {
|
|
2889
2897
|
const external = this.#options.signal;
|
|
@@ -2891,10 +2899,10 @@ var Browser = class {
|
|
|
2891
2899
|
}
|
|
2892
2900
|
async #raceAbort(promise) {
|
|
2893
2901
|
const signal = this.#signal();
|
|
2894
|
-
if (signal.aborted) throw new _src_core.
|
|
2902
|
+
if (signal.aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
|
|
2895
2903
|
const aborted = Promise.withResolvers();
|
|
2896
2904
|
const listener = (0, node_events.addAbortListener)(signal, () => {
|
|
2897
|
-
aborted.reject(new _src_core.
|
|
2905
|
+
aborted.reject(new _src_core.BrowserError("CONNECTION", "Connection aborted"));
|
|
2898
2906
|
});
|
|
2899
2907
|
try {
|
|
2900
2908
|
return await Promise.race([promise, aborted.promise]);
|
|
@@ -2922,7 +2930,7 @@ var Browser = class {
|
|
|
2922
2930
|
this.#owned = void 0;
|
|
2923
2931
|
this.#endpoint = void 0;
|
|
2924
2932
|
if (connected) this.#status = "disconnected";
|
|
2925
|
-
this.#emitter.emit("error", new _src_core.
|
|
2933
|
+
this.#emitter.emit("error", new _src_core.BrowserError("CONNECTION", "The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
2926
2934
|
if (connected) {
|
|
2927
2935
|
this.#emitter.emit("disconnect");
|
|
2928
2936
|
this.#emitter.emit("idle");
|
|
@@ -2960,7 +2968,7 @@ var Browser = class {
|
|
|
2960
2968
|
}
|
|
2961
2969
|
this.#destroyContextList(contexts);
|
|
2962
2970
|
this.#closeClient(client);
|
|
2963
|
-
this.#emitter.emit("error", new _src_core.
|
|
2971
|
+
this.#emitter.emit("error", new _src_core.BrowserError("CONNECTION", "The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
|
|
2964
2972
|
this.#emitter.emit("disconnect");
|
|
2965
2973
|
this.#emitter.emit("idle");
|
|
2966
2974
|
}
|
|
@@ -3013,7 +3021,7 @@ var Browser = class {
|
|
|
3013
3021
|
};
|
|
3014
3022
|
}
|
|
3015
3023
|
async #assertPortFree() {
|
|
3016
|
-
if (await this.#probePort()) throw new _src_core.
|
|
3024
|
+
if (await this.#probePort()) throw new _src_core.BrowserError("CONNECTION", `Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
|
|
3017
3025
|
port: this.#cdpPort,
|
|
3018
3026
|
host: this.#cdpHost
|
|
3019
3027
|
});
|
|
@@ -3027,7 +3035,7 @@ var Browser = class {
|
|
|
3027
3035
|
}
|
|
3028
3036
|
}
|
|
3029
3037
|
async #connectCDP(endpoint) {
|
|
3030
|
-
const transport =
|
|
3038
|
+
const transport = createWebSocketCDPTransport({
|
|
3031
3039
|
url: endpoint,
|
|
3032
3040
|
timeout: this.#timeout()
|
|
3033
3041
|
});
|
|
@@ -3060,20 +3068,20 @@ var Browser = class {
|
|
|
3060
3068
|
}
|
|
3061
3069
|
}
|
|
3062
3070
|
async #launch() {
|
|
3063
|
-
if (this.#process !== void 0) throw new _src_core.
|
|
3064
|
-
const requestedEngine = this.#options.
|
|
3071
|
+
if (this.#process !== void 0) throw new _src_core.BrowserError("CONNECTION", "A browser process is already active on this instance");
|
|
3072
|
+
const requestedEngine = this.#options.browsers?.engine;
|
|
3065
3073
|
let executable = this.#options.executable;
|
|
3066
3074
|
let resolvedEngine;
|
|
3067
3075
|
if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
|
|
3068
3076
|
else {
|
|
3069
|
-
const found =
|
|
3077
|
+
const found = findSystemBrowsers({
|
|
3070
3078
|
...this.#options.browsers,
|
|
3071
3079
|
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
3072
|
-
});
|
|
3080
|
+
})[0];
|
|
3073
3081
|
executable = found?.executable;
|
|
3074
3082
|
resolvedEngine = found?.engine;
|
|
3075
3083
|
}
|
|
3076
|
-
if (executable === void 0) throw new _src_core.
|
|
3084
|
+
if (executable === void 0) throw new _src_core.BrowserError("CONNECTION", "No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
3077
3085
|
this.#engine = resolvedEngine ?? "chromium";
|
|
3078
3086
|
await this.#releaseProfile();
|
|
3079
3087
|
const profile = await createBrowserProfile(this.#options.profile);
|
|
@@ -3089,7 +3097,7 @@ var Browser = class {
|
|
|
3089
3097
|
let client;
|
|
3090
3098
|
try {
|
|
3091
3099
|
const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
|
|
3092
|
-
const transport =
|
|
3100
|
+
const transport = createWebSocketCDPTransport({
|
|
3093
3101
|
url: endpoint,
|
|
3094
3102
|
timeout: this.#timeout()
|
|
3095
3103
|
});
|
|
@@ -3153,9 +3161,9 @@ var Browser = class {
|
|
|
3153
3161
|
}
|
|
3154
3162
|
if (pid === void 0) {
|
|
3155
3163
|
await this.#closeRemote(client);
|
|
3156
|
-
throw new _src_core.
|
|
3157
|
-
executable: this.#options.executable,
|
|
3158
|
-
pid: process.pid
|
|
3164
|
+
throw new _src_core.BrowserError("CONNECTION", "The browser launcher exited without naming the process serving its CDP endpoint", {
|
|
3165
|
+
...this.#options.executable === void 0 ? {} : { executable: this.#options.executable },
|
|
3166
|
+
...process.pid === void 0 ? {} : { pid: process.pid }
|
|
3159
3167
|
});
|
|
3160
3168
|
}
|
|
3161
3169
|
this.#servingPid = pid;
|
|
@@ -3163,7 +3171,7 @@ var Browser = class {
|
|
|
3163
3171
|
async #waitForLaunch(process, executable, args) {
|
|
3164
3172
|
const context = {
|
|
3165
3173
|
executable,
|
|
3166
|
-
args
|
|
3174
|
+
...args === void 0 ? {} : { args }
|
|
3167
3175
|
};
|
|
3168
3176
|
const timeout = this.#timeout();
|
|
3169
3177
|
const controller = new AbortController();
|
|
@@ -3174,32 +3182,32 @@ var Browser = class {
|
|
|
3174
3182
|
deadline
|
|
3175
3183
|
]);
|
|
3176
3184
|
const stderr = process.stderr;
|
|
3177
|
-
if (stderr === null) throw new _src_core.
|
|
3185
|
+
if (stderr === null) throw new _src_core.BrowserError("CONNECTION", "The browser process has no standard error to read", context);
|
|
3178
3186
|
const exit = (0, node_events.once)(process, "exit", { signal });
|
|
3179
3187
|
exit.catch(() => void 0);
|
|
3180
3188
|
const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
|
|
3181
3189
|
if (signal.aborted) throw error;
|
|
3182
3190
|
const values = await exit;
|
|
3183
3191
|
if (values[0] === 0 && values[1] === null) throw error;
|
|
3184
|
-
throw new _src_core.
|
|
3192
|
+
throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit((0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null, (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null), context);
|
|
3185
3193
|
});
|
|
3186
3194
|
const exited = exit.then((values) => {
|
|
3187
3195
|
const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
|
|
3188
3196
|
const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
|
|
3189
3197
|
if (code === 0 && exitSignal === null) return ready;
|
|
3190
|
-
throw new _src_core.
|
|
3198
|
+
throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit(code, exitSignal), context);
|
|
3191
3199
|
});
|
|
3192
3200
|
try {
|
|
3193
3201
|
return await Promise.race([ready, exited]);
|
|
3194
3202
|
} catch (error) {
|
|
3195
|
-
if ((0, _src_core.
|
|
3196
|
-
if (this.#signal().aborted) throw new _src_core.
|
|
3197
|
-
if (deadline.aborted) throw new _src_core.
|
|
3203
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
|
|
3204
|
+
if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted", context);
|
|
3205
|
+
if (deadline.aborted) throw new _src_core.BrowserError("CONNECTION", `Browser did not report a CDP endpoint within ${timeout}ms`, {
|
|
3198
3206
|
...context,
|
|
3199
3207
|
timeout
|
|
3200
3208
|
});
|
|
3201
3209
|
const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
|
|
3202
|
-
throw new _src_core.
|
|
3210
|
+
throw new _src_core.BrowserError("CONNECTION", message, context);
|
|
3203
3211
|
} finally {
|
|
3204
3212
|
controller.abort();
|
|
3205
3213
|
ready.catch(() => void 0);
|
|
@@ -3236,7 +3244,10 @@ var Browser = class {
|
|
|
3236
3244
|
});
|
|
3237
3245
|
}
|
|
3238
3246
|
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
3239
|
-
const context = new _src_core.BrowserContext(client,
|
|
3247
|
+
const context = new _src_core.BrowserContext(client, {
|
|
3248
|
+
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
3249
|
+
writer: createFileBrowserWriter()
|
|
3250
|
+
});
|
|
3240
3251
|
await context.sync(pages);
|
|
3241
3252
|
this.#registerContext(context);
|
|
3242
3253
|
}
|
|
@@ -3316,7 +3327,7 @@ var Browser = class {
|
|
|
3316
3327
|
let remote = client;
|
|
3317
3328
|
let temporary = false;
|
|
3318
3329
|
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
3319
|
-
const transport =
|
|
3330
|
+
const transport = createWebSocketCDPTransport({
|
|
3320
3331
|
url: this.#endpoint,
|
|
3321
3332
|
timeout: this.#timeout()
|
|
3322
3333
|
});
|
|
@@ -3419,10 +3430,11 @@ var Browser = class {
|
|
|
3419
3430
|
const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
|
|
3420
3431
|
if (code === "ESRCH") return false;
|
|
3421
3432
|
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
3422
|
-
|
|
3423
|
-
|
|
3433
|
+
const target = serving ?? pid;
|
|
3434
|
+
throw new _src_core.BrowserError("CONNECTION", "Failed to signal the browser process", {
|
|
3435
|
+
...target === void 0 ? {} : { pid: target },
|
|
3424
3436
|
signal,
|
|
3425
|
-
cause: error
|
|
3437
|
+
cause: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
|
|
3426
3438
|
});
|
|
3427
3439
|
}
|
|
3428
3440
|
}
|
|
@@ -3461,7 +3473,8 @@ var Browser = class {
|
|
|
3461
3473
|
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
3462
3474
|
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
3463
3475
|
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
3464
|
-
|
|
3476
|
+
const target = this.#servingPid ?? process.pid;
|
|
3477
|
+
throw new _src_core.BrowserError("CONNECTION", "Browser process did not exit after SIGKILL", { ...target === void 0 ? {} : { pid: target } });
|
|
3465
3478
|
} finally {
|
|
3466
3479
|
await this.#closeProcessPipe(process);
|
|
3467
3480
|
}
|
|
@@ -3541,9 +3554,7 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
|
|
|
3541
3554
|
exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
|
|
3542
3555
|
exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
|
|
3543
3556
|
exports.Browser = Browser;
|
|
3544
|
-
exports.BrowserDestroyedError = BrowserDestroyedError;
|
|
3545
3557
|
exports.BrowserMCPServer = BrowserMCPServer;
|
|
3546
|
-
exports.BrowserNotConnectedError = BrowserNotConnectedError;
|
|
3547
3558
|
exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
|
|
3548
3559
|
exports.FileBrowserRunStore = FileBrowserRunStore;
|
|
3549
3560
|
exports.FileBrowserWriter = FileBrowserWriter;
|
|
@@ -3555,20 +3566,17 @@ exports.buildWindowsRoots = buildWindowsRoots;
|
|
|
3555
3566
|
exports.createBrowser = createBrowser;
|
|
3556
3567
|
exports.createBrowserMCPServer = createBrowserMCPServer;
|
|
3557
3568
|
exports.createBrowserProfile = createBrowserProfile;
|
|
3558
|
-
exports.createBrowserWriter = createBrowserWriter;
|
|
3559
|
-
exports.createCDPTransport = createCDPTransport;
|
|
3560
3569
|
exports.createFileBrowserJourneyStore = createFileBrowserJourneyStore;
|
|
3561
3570
|
exports.createFileBrowserRunStore = createFileBrowserRunStore;
|
|
3571
|
+
exports.createFileBrowserWriter = createFileBrowserWriter;
|
|
3572
|
+
exports.createWebSocketCDPTransport = createWebSocketCDPTransport;
|
|
3562
3573
|
exports.describeBrowserServerLoss = describeBrowserServerLoss;
|
|
3563
3574
|
exports.fetchCDPTargets = fetchCDPTargets;
|
|
3564
3575
|
exports.findEnvOverrides = findEnvOverrides;
|
|
3565
3576
|
exports.findInstallPaths = findInstallPaths;
|
|
3566
3577
|
exports.findStorePaths = findStorePaths;
|
|
3567
|
-
exports.findSystemBrowser = findSystemBrowser;
|
|
3568
3578
|
exports.findSystemBrowsers = findSystemBrowsers;
|
|
3569
3579
|
exports.formatBrowserLockEntry = formatBrowserLockEntry;
|
|
3570
|
-
exports.isBrowserDestroyedError = isBrowserDestroyedError;
|
|
3571
|
-
exports.isBrowserNotConnectedError = isBrowserNotConnectedError;
|
|
3572
3580
|
exports.launchBrowserProcess = launchBrowserProcess;
|
|
3573
3581
|
exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
3574
3582
|
exports.parseBrowserEngine = parseBrowserEngine;
|