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