@orkestrel/browser 0.0.26 → 0.0.28
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 +202 -211
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +65 -166
- package/dist/src/server/index.d.ts +65 -166
- package/dist/src/server/index.js +201 -205
- package/dist/src/server/index.js.map +1 -1
- package/package.json +14 -13
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";
|
|
@@ -226,9 +226,9 @@ var BROWSER_RUN_DIRECTORY = "runs";
|
|
|
226
226
|
/** Bounds a file-store listing page by default. */
|
|
227
227
|
var BROWSER_FILE_STORE_LIMIT = 100;
|
|
228
228
|
/** Names the refusal when no browser can serve a call. */
|
|
229
|
-
var BROWSER_SERVER_UNAVAILABLE = "
|
|
229
|
+
var BROWSER_SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
|
|
230
230
|
/** Names a failed attempt to warm a browser. */
|
|
231
|
-
var BROWSER_SERVER_LAUNCH = "
|
|
231
|
+
var BROWSER_SERVER_LAUNCH = "SERVER_LAUNCH";
|
|
232
232
|
/** Sets the default number of warm browsers to 1. */
|
|
233
233
|
var BROWSER_SERVER_POOL_SIZE = 1;
|
|
234
234
|
/** Sets the default context capacity per browser, including the shared holder. */
|
|
@@ -242,21 +242,21 @@ var BROWSER_SERVER_RESTARTS = 1;
|
|
|
242
242
|
/** Names the browser process record in each profile. */
|
|
243
243
|
var BROWSER_SERVER_RECORD = "browse.json";
|
|
244
244
|
/** Names the diagnostic when the warm floor spends its restart bound. */
|
|
245
|
-
var BROWSER_SERVER_EXHAUSTED = "
|
|
245
|
+
var BROWSER_SERVER_EXHAUSTED = "SERVER_EXHAUSTED";
|
|
246
246
|
/** Names a failed browser server teardown. */
|
|
247
|
-
var BROWSER_SERVER_TEARDOWN = "
|
|
247
|
+
var BROWSER_SERVER_TEARDOWN = "SERVER_TEARDOWN";
|
|
248
248
|
/** Names a failed profile sweep. */
|
|
249
|
-
var BROWSER_SERVER_SWEEP = "
|
|
249
|
+
var BROWSER_SERVER_SWEEP = "SERVER_SWEEP";
|
|
250
250
|
/** Names a refused browser server option. */
|
|
251
|
-
var BROWSER_SERVER_OPTIONS = "
|
|
251
|
+
var BROWSER_SERVER_OPTIONS = "ARGUMENT";
|
|
252
252
|
/** Names the notice that a browser and its session state were lost. */
|
|
253
|
-
var BROWSER_SERVER_CRASH = "
|
|
253
|
+
var BROWSER_SERVER_CRASH = "SERVER_CRASH";
|
|
254
254
|
/** Names an interrupted call whose outcome is unknown. */
|
|
255
|
-
var BROWSER_SERVER_UNRESOLVED = "
|
|
255
|
+
var BROWSER_SERVER_UNRESOLVED = "SERVER_UNRESOLVED";
|
|
256
256
|
/** Names the refusal when every browser is admitted to a holder. */
|
|
257
|
-
var BROWSER_SERVER_BUSY = "
|
|
257
|
+
var BROWSER_SERVER_BUSY = "SERVER_BUSY";
|
|
258
258
|
/** Names the refusal for an unknown or ended holder. */
|
|
259
|
-
var BROWSER_SERVER_HOLDER = "
|
|
259
|
+
var BROWSER_SERVER_HOLDER = "SERVER_HOLDER";
|
|
260
260
|
/** Defines the holder tools independently of the shared browser's vocabulary. */
|
|
261
261
|
var BROWSER_SERVER_COPY = Object.freeze({
|
|
262
262
|
acquire: Object.freeze({
|
|
@@ -332,46 +332,6 @@ var BROWSER_SERVER_COPY = Object.freeze({
|
|
|
332
332
|
})
|
|
333
333
|
});
|
|
334
334
|
//#endregion
|
|
335
|
-
//#region src/server/errors.ts
|
|
336
|
-
/**
|
|
337
|
-
* Reports that an operation requiring an active connection was attempted while disconnected,
|
|
338
|
-
* under the code `BROWSER_NOT_CONNECTED_ERROR`.
|
|
339
|
-
*/
|
|
340
|
-
var BrowserNotConnectedError = class extends BrowserError {
|
|
341
|
-
constructor(context) {
|
|
342
|
-
super("Browser is not connected", "BROWSER_NOT_CONNECTED_ERROR", context);
|
|
343
|
-
this.name = "BrowserNotConnectedError";
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
/**
|
|
347
|
-
* Reports that an operation was attempted after the browser wrapper was destroyed, under the
|
|
348
|
-
* code `BROWSER_DESTROYED_ERROR`.
|
|
349
|
-
*/
|
|
350
|
-
var BrowserDestroyedError = class extends BrowserError {
|
|
351
|
-
constructor(context) {
|
|
352
|
-
super("Browser has been destroyed", "BROWSER_DESTROYED_ERROR", context);
|
|
353
|
-
this.name = "BrowserDestroyedError";
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
/**
|
|
357
|
-
* Narrows an unknown value to a `BrowserNotConnectedError`.
|
|
358
|
-
*
|
|
359
|
-
* @param value - Value to check
|
|
360
|
-
* @returns True if value is a BrowserNotConnectedError instance; false otherwise
|
|
361
|
-
*/
|
|
362
|
-
function isBrowserNotConnectedError(value) {
|
|
363
|
-
return isInstance(value, BrowserNotConnectedError);
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Narrows an unknown value to a `BrowserDestroyedError`.
|
|
367
|
-
*
|
|
368
|
-
* @param value - Value to check
|
|
369
|
-
* @returns True if value is a BrowserDestroyedError instance; false otherwise
|
|
370
|
-
*/
|
|
371
|
-
function isBrowserDestroyedError(value) {
|
|
372
|
-
return isInstance(value, BrowserDestroyedError);
|
|
373
|
-
}
|
|
374
|
-
//#endregion
|
|
375
335
|
//#region src/server/helpers.ts
|
|
376
336
|
/**
|
|
377
337
|
* Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
|
|
@@ -434,15 +394,15 @@ function parseBrowserProfileRecord(text) {
|
|
|
434
394
|
* @param url - The lost page's last URL, when known
|
|
435
395
|
* @returns The coded diagnostic and any operation-lifecycle guidance
|
|
436
396
|
* @example
|
|
437
|
-
* describeBrowserServerLoss('
|
|
397
|
+
* describeBrowserServerLoss('SERVER_UNAVAILABLE', new Error('Browser exited'))
|
|
438
398
|
*/
|
|
439
399
|
function describeBrowserServerLoss(code, cause, url) {
|
|
440
400
|
const pid = isBrowserError(cause) ? cause.context?.["pid"] : void 0;
|
|
441
401
|
const message = `${code}: ${(cause === void 0 ? "Browser session lost" : isError(cause) ? cause.message : String(cause)).replace(/\.+$/u, "")}${isNumber(pid) ? ` (pid ${pid})` : ""}.`;
|
|
442
|
-
if (code !== "
|
|
443
|
-
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.`;
|
|
444
404
|
const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
|
|
445
|
-
return code === "
|
|
405
|
+
return code === "SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
|
|
446
406
|
}
|
|
447
407
|
/**
|
|
448
408
|
* Formats a process identifier and UUID token as a lock entry name.
|
|
@@ -517,16 +477,6 @@ function findSystemBrowsers(options) {
|
|
|
517
477
|
return browsers;
|
|
518
478
|
}
|
|
519
479
|
/**
|
|
520
|
-
* Locates a Chrome/Chromium/Edge executable on this machine — the first entry
|
|
521
|
-
* of {@link findSystemBrowsers}.
|
|
522
|
-
*
|
|
523
|
-
* @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
|
|
524
|
-
* @returns The first discovered browser, or undefined
|
|
525
|
-
*/
|
|
526
|
-
function findSystemBrowser(options) {
|
|
527
|
-
return findSystemBrowsers(options)[0];
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
480
|
* Classifies an executable path/name into a {@link BrowserEngine} by
|
|
531
481
|
* case-insensitive hint, checked in the order edge → chromium → chrome.
|
|
532
482
|
*
|
|
@@ -582,7 +532,7 @@ async function removeBrowserProfile(profile) {
|
|
|
582
532
|
if (!profile.temporary) return;
|
|
583
533
|
const path = resolve(profile.path);
|
|
584
534
|
const temporary = resolve(tmpdir());
|
|
585
|
-
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 });
|
|
586
536
|
await rm(path, {
|
|
587
537
|
recursive: true,
|
|
588
538
|
force: true,
|
|
@@ -772,18 +722,18 @@ function readBrowserEndpoint(stream, signal) {
|
|
|
772
722
|
stream.on("close", () => {
|
|
773
723
|
const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
|
|
774
724
|
if (endpoint !== void 0) read.resolve(endpoint);
|
|
775
|
-
else read.reject(new
|
|
725
|
+
else read.reject(new BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
|
|
776
726
|
});
|
|
777
727
|
return read.promise;
|
|
778
728
|
}
|
|
779
729
|
/**
|
|
780
730
|
* Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
|
|
781
|
-
* carrying either the targets or a coded `
|
|
731
|
+
* carrying either the targets or a coded `BrowserError`.
|
|
782
732
|
*
|
|
783
733
|
* @remarks
|
|
784
734
|
* The endpoint is a network boundary, so an unreachable host, a non-2xx
|
|
785
735
|
* response, and a body that is not a JSON array each come back as a failed
|
|
786
|
-
* `Result` carrying a coded `
|
|
736
|
+
* `Result` carrying a coded `BrowserError`. An entry missing a
|
|
787
737
|
* required string field is skipped rather than failing the whole list.
|
|
788
738
|
*
|
|
789
739
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
@@ -808,7 +758,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
808
758
|
const response = await fetch(url, { signal: controller.signal });
|
|
809
759
|
if (!response.ok) return {
|
|
810
760
|
success: false,
|
|
811
|
-
error: new
|
|
761
|
+
error: new BrowserError("CONNECTION", "CDP target list request was refused", {
|
|
812
762
|
url,
|
|
813
763
|
status: response.status
|
|
814
764
|
})
|
|
@@ -816,7 +766,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
816
766
|
const list = await response.json();
|
|
817
767
|
if (!isArray(list)) return {
|
|
818
768
|
success: false,
|
|
819
|
-
error: new
|
|
769
|
+
error: new BrowserError("CONNECTION", "CDP target list is not a JSON array", { url })
|
|
820
770
|
};
|
|
821
771
|
const targets = [];
|
|
822
772
|
for (const entry of list) {
|
|
@@ -836,9 +786,9 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
836
786
|
} catch (error) {
|
|
837
787
|
return {
|
|
838
788
|
success: false,
|
|
839
|
-
error: new
|
|
789
|
+
error: new BrowserError("CONNECTION", "CDP target list is unreachable", {
|
|
840
790
|
url,
|
|
841
|
-
error
|
|
791
|
+
error: isJSONValue(error) ? error : String(error)
|
|
842
792
|
})
|
|
843
793
|
};
|
|
844
794
|
} finally {
|
|
@@ -904,7 +854,7 @@ var WebSocketCDPTransport = class {
|
|
|
904
854
|
}
|
|
905
855
|
async send(data) {
|
|
906
856
|
const socket = this.#socket;
|
|
907
|
-
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 });
|
|
908
858
|
socket.send(data);
|
|
909
859
|
}
|
|
910
860
|
async close() {
|
|
@@ -912,12 +862,12 @@ var WebSocketCDPTransport = class {
|
|
|
912
862
|
}
|
|
913
863
|
async #start() {
|
|
914
864
|
const parsed = attempt(() => new URL(this.#url));
|
|
915
|
-
if (!parsed.success) throw new
|
|
865
|
+
if (!parsed.success) throw new BrowserError("CONNECTION", `WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
916
866
|
url: this.#url,
|
|
917
|
-
error: parsed.error
|
|
867
|
+
error: isJSONValue(parsed.error) ? parsed.error : String(parsed.error)
|
|
918
868
|
});
|
|
919
869
|
const url = parsed.value;
|
|
920
|
-
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 });
|
|
921
871
|
const key = randomBytes(16).toString("base64");
|
|
922
872
|
const deferred = Promise.withResolvers();
|
|
923
873
|
const options = {
|
|
@@ -949,7 +899,7 @@ var WebSocketCDPTransport = class {
|
|
|
949
899
|
async #close() {
|
|
950
900
|
const request = this.#request;
|
|
951
901
|
if (request !== void 0) {
|
|
952
|
-
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 }));
|
|
953
903
|
request.destroy();
|
|
954
904
|
}
|
|
955
905
|
await this.#starting.pending?.catch(() => void 0);
|
|
@@ -978,7 +928,7 @@ var WebSocketCDPTransport = class {
|
|
|
978
928
|
const accept = response.headers["sec-websocket-accept"];
|
|
979
929
|
if (!isString(accept) || accept !== computeWebSocketAccept(key)) {
|
|
980
930
|
socket.destroy();
|
|
981
|
-
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 }));
|
|
982
932
|
return;
|
|
983
933
|
}
|
|
984
934
|
try {
|
|
@@ -994,13 +944,13 @@ var WebSocketCDPTransport = class {
|
|
|
994
944
|
#decline(request, response) {
|
|
995
945
|
response.resume();
|
|
996
946
|
const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
|
|
997
|
-
this.#rejectRequest(request, new
|
|
947
|
+
this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
|
|
998
948
|
}
|
|
999
949
|
#fail(request, error) {
|
|
1000
|
-
this.#rejectRequest(request, new
|
|
950
|
+
this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
|
|
1001
951
|
}
|
|
1002
952
|
#expire(request) {
|
|
1003
|
-
this.#rejectRequest(request, new
|
|
953
|
+
this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
|
|
1004
954
|
url: this.#url,
|
|
1005
955
|
timeout: this.#timeout
|
|
1006
956
|
}));
|
|
@@ -1072,7 +1022,7 @@ var FileBrowserWriter = class {
|
|
|
1072
1022
|
};
|
|
1073
1023
|
//#endregion
|
|
1074
1024
|
//#region package.json
|
|
1075
|
-
var version = "0.0.
|
|
1025
|
+
var version = "0.0.28";
|
|
1076
1026
|
//#endregion
|
|
1077
1027
|
//#region src/server/BrowserMCPServer.ts
|
|
1078
1028
|
/**
|
|
@@ -1080,9 +1030,9 @@ var version = "0.0.26";
|
|
|
1080
1030
|
* the Model Context Protocol on stdio, and warms Chromium at server start.
|
|
1081
1031
|
*
|
|
1082
1032
|
* @remarks
|
|
1083
|
-
* The server's own manager holds one dispatcher per name of the vocabulary — `
|
|
1084
|
-
* `
|
|
1085
|
-
* `
|
|
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
|
|
1086
1036
|
* `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
|
|
1087
1037
|
* absent. After the launch, a tool the toolset's manager adds under another name, such as a page
|
|
1088
1038
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
@@ -1119,7 +1069,7 @@ var version = "0.0.26";
|
|
|
1119
1069
|
* ```ts
|
|
1120
1070
|
* import { BrowserMCPServer } from '@orkestrel/browser/server'
|
|
1121
1071
|
*
|
|
1122
|
-
* const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
|
|
1072
|
+
* const server = new BrowserMCPServer({ root: 'tmp/browsers', journeys: { readonly: true } })
|
|
1123
1073
|
* await server.start()
|
|
1124
1074
|
* await server.destroy()
|
|
1125
1075
|
* ```
|
|
@@ -1182,8 +1132,8 @@ var BrowserMCPServer = class {
|
|
|
1182
1132
|
constructor(options) {
|
|
1183
1133
|
const size = options?.pool?.size ?? 1;
|
|
1184
1134
|
const contexts = options?.pool?.contexts ?? 2;
|
|
1185
|
-
if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(`pool.size must be an integer from 1 through 3`,
|
|
1186
|
-
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" });
|
|
1187
1137
|
this.#admission = size * contexts;
|
|
1188
1138
|
this.#log = options?.log ?? process.stderr;
|
|
1189
1139
|
this.#reference = this.#issue.bind(this);
|
|
@@ -1198,11 +1148,11 @@ var BrowserMCPServer = class {
|
|
|
1198
1148
|
restarts: 1
|
|
1199
1149
|
});
|
|
1200
1150
|
this.#root = resolve(options?.root ?? "tmp/browsers");
|
|
1201
|
-
this.#headless = options?.headless ?? true;
|
|
1202
|
-
this.#executable = options?.executable;
|
|
1203
|
-
this.#viewport = options?.viewport;
|
|
1204
|
-
this.#readonly = options?.readonly ?? false;
|
|
1205
|
-
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;
|
|
1206
1156
|
this.#input = options?.stdio?.input ?? process.stdin;
|
|
1207
1157
|
this.#tools = createToolManager();
|
|
1208
1158
|
const names = [...BROWSER_TOOL_NAMES, ...BROWSER_JOURNEY_TOOL_NAMES];
|
|
@@ -1266,7 +1216,7 @@ var BrowserMCPServer = class {
|
|
|
1266
1216
|
warming.then(this.#exhaust.bind(this));
|
|
1267
1217
|
} catch (error) {
|
|
1268
1218
|
if (this.#closing !== void 0) return;
|
|
1269
|
-
if (isBrowserError(error) && error.code === "
|
|
1219
|
+
if (isBrowserError(error) && error.code === "SERVER_UNAVAILABLE") throw error;
|
|
1270
1220
|
throw this.#unavailable(isPoolError(error) ? error.cause ?? this.#failure : error);
|
|
1271
1221
|
}
|
|
1272
1222
|
}
|
|
@@ -1316,12 +1266,16 @@ var BrowserMCPServer = class {
|
|
|
1316
1266
|
signal: context.signal,
|
|
1317
1267
|
...context.caller === void 0 ? {} : { caller: context.caller }
|
|
1318
1268
|
});
|
|
1319
|
-
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;
|
|
1320
1274
|
return {
|
|
1321
1275
|
resultType: "complete",
|
|
1322
1276
|
content: [{
|
|
1323
1277
|
type: "text",
|
|
1324
|
-
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)
|
|
1325
1279
|
}]
|
|
1326
1280
|
};
|
|
1327
1281
|
}
|
|
@@ -1337,8 +1291,8 @@ var BrowserMCPServer = class {
|
|
|
1337
1291
|
context.signal.throwIfAborted();
|
|
1338
1292
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1339
1293
|
const purpose = args["purpose"];
|
|
1340
|
-
if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("purpose must describe the work", "
|
|
1341
|
-
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.`);
|
|
1342
1296
|
const holder = {
|
|
1343
1297
|
id: randomUUID(),
|
|
1344
1298
|
purpose,
|
|
@@ -1365,14 +1319,14 @@ var BrowserMCPServer = class {
|
|
|
1365
1319
|
}
|
|
1366
1320
|
#holder(value) {
|
|
1367
1321
|
const holder = isString(value) ? this.#holders.get(value) : void 0;
|
|
1368
|
-
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`);
|
|
1369
1323
|
return holder;
|
|
1370
1324
|
}
|
|
1371
1325
|
async #dispatch(args, context) {
|
|
1372
1326
|
const holder = this.#holder(args["holder"]);
|
|
1373
1327
|
const name = args["name"];
|
|
1374
1328
|
const parameters = args["arguments"];
|
|
1375
|
-
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" });
|
|
1376
1330
|
return this.#forward(holder, name, parameters, context);
|
|
1377
1331
|
}
|
|
1378
1332
|
async #catalog(args, context) {
|
|
@@ -1389,8 +1343,8 @@ var BrowserMCPServer = class {
|
|
|
1389
1343
|
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1390
1344
|
} catch (error) {
|
|
1391
1345
|
context.signal.throwIfAborted();
|
|
1392
|
-
if (holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call
|
|
1393
|
-
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}`));
|
|
1394
1348
|
throw error;
|
|
1395
1349
|
}
|
|
1396
1350
|
}
|
|
@@ -1427,7 +1381,7 @@ var BrowserMCPServer = class {
|
|
|
1427
1381
|
context.signal.throwIfAborted();
|
|
1428
1382
|
const journey = args["journey"];
|
|
1429
1383
|
if (name !== "replay" && name !== "forget" || !isString(journey)) return this.#perform(holder, name, args, context);
|
|
1430
|
-
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 });
|
|
1431
1385
|
const reader = Symbol(journey);
|
|
1432
1386
|
if (name === "forget") this.#writers.add(journey);
|
|
1433
1387
|
else {
|
|
@@ -1453,8 +1407,8 @@ var BrowserMCPServer = class {
|
|
|
1453
1407
|
lease = await this.#serve(holder, signal);
|
|
1454
1408
|
} catch (error) {
|
|
1455
1409
|
context.signal.throwIfAborted();
|
|
1456
|
-
if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call
|
|
1457
|
-
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}`));
|
|
1458
1412
|
throw error;
|
|
1459
1413
|
}
|
|
1460
1414
|
let result;
|
|
@@ -1479,7 +1433,7 @@ var BrowserMCPServer = class {
|
|
|
1479
1433
|
};
|
|
1480
1434
|
}
|
|
1481
1435
|
context.signal.throwIfAborted();
|
|
1482
|
-
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");
|
|
1483
1437
|
if (!result.success) {
|
|
1484
1438
|
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1485
1439
|
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
@@ -1487,9 +1441,9 @@ var BrowserMCPServer = class {
|
|
|
1487
1441
|
context.signal.throwIfAborted();
|
|
1488
1442
|
if (this.#leases.get(holder) !== lease) {
|
|
1489
1443
|
const loss = this.#departures.get(lease);
|
|
1490
|
-
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));
|
|
1491
1445
|
}
|
|
1492
|
-
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));
|
|
1493
1447
|
}
|
|
1494
1448
|
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1495
1449
|
}
|
|
@@ -1503,6 +1457,10 @@ var BrowserMCPServer = class {
|
|
|
1503
1457
|
}
|
|
1504
1458
|
return [...notes, value].join("\n");
|
|
1505
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
|
+
}
|
|
1506
1464
|
async #handshake(options) {
|
|
1507
1465
|
try {
|
|
1508
1466
|
await this.#race(this.#starting, options.signal);
|
|
@@ -1653,7 +1611,7 @@ var BrowserMCPServer = class {
|
|
|
1653
1611
|
if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
|
|
1654
1612
|
this.#departures.set(lease, {
|
|
1655
1613
|
cause,
|
|
1656
|
-
url: lease.generation.toolset.view.url
|
|
1614
|
+
url: lease.generation.toolset.redact(lease.generation.toolset.view.url)
|
|
1657
1615
|
});
|
|
1658
1616
|
if (holder === this.#shared) this.#unmirror();
|
|
1659
1617
|
this.#leases.delete(holder);
|
|
@@ -1795,6 +1753,7 @@ var BrowserMCPServer = class {
|
|
|
1795
1753
|
}
|
|
1796
1754
|
toolset = createBrowserToolset(page, {
|
|
1797
1755
|
context,
|
|
1756
|
+
notes: this.#notice.bind(this, context),
|
|
1798
1757
|
journeys: {
|
|
1799
1758
|
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1800
1759
|
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
@@ -1814,7 +1773,7 @@ var BrowserMCPServer = class {
|
|
|
1814
1773
|
} catch (error) {
|
|
1815
1774
|
await toolset?.destroy().catch(this.#fault.bind(this));
|
|
1816
1775
|
await context?.close().catch((failure) => {
|
|
1817
|
-
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);
|
|
1818
1777
|
});
|
|
1819
1778
|
await rm(directory, {
|
|
1820
1779
|
recursive: true,
|
|
@@ -1824,7 +1783,8 @@ var BrowserMCPServer = class {
|
|
|
1824
1783
|
}
|
|
1825
1784
|
}
|
|
1826
1785
|
async #destroyRecord(slot) {
|
|
1827
|
-
|
|
1786
|
+
const cause = this.#losses.get(slot)?.cause;
|
|
1787
|
+
if (isBrowserError(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
|
|
1828
1788
|
process.kill(slot.browser.pid, "SIGKILL");
|
|
1829
1789
|
} catch (error) {
|
|
1830
1790
|
if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
@@ -1849,7 +1809,7 @@ var BrowserMCPServer = class {
|
|
|
1849
1809
|
}
|
|
1850
1810
|
async #warm() {
|
|
1851
1811
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1852
|
-
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);
|
|
1853
1813
|
const profile = join(this.#root, ".profiles", formatBrowserLockEntry(process.pid, randomUUID()));
|
|
1854
1814
|
let browser;
|
|
1855
1815
|
let slot;
|
|
@@ -1922,7 +1882,7 @@ var BrowserMCPServer = class {
|
|
|
1922
1882
|
const record = parseBrowserProfileRecord(await readFile(join(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1923
1883
|
if (record === void 0) continue;
|
|
1924
1884
|
if (probeProcess(record.pid)) {
|
|
1925
|
-
const transport =
|
|
1885
|
+
const transport = createWebSocketCDPTransport({ url: record.endpoint });
|
|
1926
1886
|
const client = createCDPClient({ transport });
|
|
1927
1887
|
try {
|
|
1928
1888
|
this.#abort.signal.throwIfAborted();
|
|
@@ -1964,7 +1924,7 @@ var BrowserMCPServer = class {
|
|
|
1964
1924
|
}
|
|
1965
1925
|
#unavailable(cause) {
|
|
1966
1926
|
const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
|
|
1967
|
-
return new BrowserError(message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2)
|
|
1927
|
+
return new BrowserError(BROWSER_SERVER_UNAVAILABLE, message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2));
|
|
1968
1928
|
}
|
|
1969
1929
|
#issue() {
|
|
1970
1930
|
return `e${++this.#references}`;
|
|
@@ -1996,7 +1956,7 @@ var BrowserMCPServer = class {
|
|
|
1996
1956
|
tools.emitter.off("clear", mirrored.cleared);
|
|
1997
1957
|
}
|
|
1998
1958
|
#ended() {
|
|
1999
|
-
return new BrowserError("the browser session ended", "
|
|
1959
|
+
return new BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
|
|
2000
1960
|
}
|
|
2001
1961
|
};
|
|
2002
1962
|
//#endregion
|
|
@@ -2009,31 +1969,32 @@ var BrowserMCPServer = class {
|
|
|
2009
1969
|
* const files = new FileBrowserStore({ root: directory })
|
|
2010
1970
|
* files.validateName('check-ready')
|
|
2011
1971
|
*/
|
|
2012
|
-
var FileBrowserStore = class {
|
|
1972
|
+
var FileBrowserStore = class FileBrowserStore {
|
|
1973
|
+
static #locks = /* @__PURE__ */ new Map();
|
|
2013
1974
|
#root;
|
|
2014
1975
|
#limit;
|
|
2015
1976
|
constructor(options) {
|
|
2016
1977
|
this.#limit = options.limit ?? 100;
|
|
2017
|
-
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" });
|
|
2018
1979
|
try {
|
|
2019
1980
|
this.#root = realpathSync(options.root);
|
|
2020
1981
|
} catch (error) {
|
|
2021
|
-
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 });
|
|
2022
1983
|
throw this.translateError(options.root, error);
|
|
2023
1984
|
}
|
|
2024
1985
|
}
|
|
2025
1986
|
/** Checks a journey name before filesystem access. @param name - Journey name */
|
|
2026
1987
|
validateName(name) {
|
|
2027
|
-
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}`);
|
|
2028
1989
|
}
|
|
2029
1990
|
/** Checks a run id before filesystem access. @param id - Producer id */
|
|
2030
1991
|
validateId(id) {
|
|
2031
|
-
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}`);
|
|
2032
1993
|
}
|
|
2033
1994
|
/** Resolves a confined target. @param parts - Relative components @returns Absolute target */
|
|
2034
1995
|
resolvePath(...parts) {
|
|
2035
1996
|
const path = resolve(this.#root, ...parts);
|
|
2036
|
-
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}`);
|
|
2037
1998
|
return path;
|
|
2038
1999
|
}
|
|
2039
2000
|
/**
|
|
@@ -2051,8 +2012,8 @@ var FileBrowserStore = class {
|
|
|
2051
2012
|
try {
|
|
2052
2013
|
const status = await lstat(current);
|
|
2053
2014
|
options?.signal?.throwIfAborted();
|
|
2054
|
-
if (status.isSymbolicLink()) throw new BrowserError(`Refused symbolic link: ${current}
|
|
2055
|
-
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}`);
|
|
2056
2017
|
} catch (error) {
|
|
2057
2018
|
options?.signal?.throwIfAborted();
|
|
2058
2019
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
@@ -2066,7 +2027,7 @@ var FileBrowserStore = class {
|
|
|
2066
2027
|
/** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
|
|
2067
2028
|
async createDirectory(path, options) {
|
|
2068
2029
|
if (await this.check(path, options)) return;
|
|
2069
|
-
if (path === this.#root) throw new BrowserError(`Missing root: ${path}
|
|
2030
|
+
if (path === this.#root) throw new BrowserError("STORE_PATH", `Missing root: ${path}`);
|
|
2070
2031
|
await this.createDirectory(dirname(path), options);
|
|
2071
2032
|
options?.signal?.throwIfAborted();
|
|
2072
2033
|
try {
|
|
@@ -2116,7 +2077,7 @@ var FileBrowserStore = class {
|
|
|
2116
2077
|
await rename(temporary, path);
|
|
2117
2078
|
} catch (error) {
|
|
2118
2079
|
const target = await lstat(path).catch(() => void 0);
|
|
2119
|
-
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 });
|
|
2120
2081
|
throw error;
|
|
2121
2082
|
}
|
|
2122
2083
|
owned = false;
|
|
@@ -2147,6 +2108,20 @@ var FileBrowserStore = class {
|
|
|
2147
2108
|
* @returns The mutation result
|
|
2148
2109
|
*/
|
|
2149
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) {
|
|
2150
2125
|
await this.createDirectory(dirname(path), options);
|
|
2151
2126
|
const name = formatBrowserLockEntry(process.pid, randomUUID());
|
|
2152
2127
|
const entry = this.resolvePath(path, name);
|
|
@@ -2189,7 +2164,7 @@ var FileBrowserStore = class {
|
|
|
2189
2164
|
await this.#removeLock(path);
|
|
2190
2165
|
}
|
|
2191
2166
|
}
|
|
2192
|
-
throw new BrowserError(`Journey is locked: ${path}
|
|
2167
|
+
throw new BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2193
2168
|
}
|
|
2194
2169
|
/**
|
|
2195
2170
|
* Lists readable entries in sorted order with a bounded page and one extra readable probe.
|
|
@@ -2236,7 +2211,7 @@ var FileBrowserStore = class {
|
|
|
2236
2211
|
const reason = error instanceof BrowserError ? error.context?.["reason"] : void 0;
|
|
2237
2212
|
faults.push({
|
|
2238
2213
|
name,
|
|
2239
|
-
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"
|
|
2240
2215
|
});
|
|
2241
2216
|
}
|
|
2242
2217
|
}
|
|
@@ -2279,7 +2254,7 @@ var FileBrowserStore = class {
|
|
|
2279
2254
|
/** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
|
|
2280
2255
|
translateError(path, error) {
|
|
2281
2256
|
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
2282
|
-
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 });
|
|
2283
2258
|
}
|
|
2284
2259
|
async #readLock(path, options) {
|
|
2285
2260
|
await this.check(path, options);
|
|
@@ -2287,13 +2262,13 @@ var FileBrowserStore = class {
|
|
|
2287
2262
|
const entries = await readdir(path, { withFileTypes: true });
|
|
2288
2263
|
for (const entry of entries) {
|
|
2289
2264
|
await this.check(this.resolvePath(path, entry.name), options);
|
|
2290
|
-
if (!entry.isFile()) throw new BrowserError(`Journey is locked: ${path}
|
|
2265
|
+
if (!entry.isFile()) throw new BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2291
2266
|
}
|
|
2292
2267
|
return entries.map((entry) => entry.name);
|
|
2293
2268
|
} catch (error) {
|
|
2294
2269
|
if (error instanceof Error && "code" in error) {
|
|
2295
2270
|
if (error.code === "ENOENT") return void 0;
|
|
2296
|
-
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}`);
|
|
2297
2272
|
}
|
|
2298
2273
|
throw this.translateError(path, error);
|
|
2299
2274
|
}
|
|
@@ -2305,7 +2280,7 @@ var FileBrowserStore = class {
|
|
|
2305
2280
|
return true;
|
|
2306
2281
|
} catch (error) {
|
|
2307
2282
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
|
|
2308
|
-
throw new BrowserError(`Cannot remove lock entry: ${path}`,
|
|
2283
|
+
throw new BrowserError("STORE_ACCESS", `Cannot remove lock entry: ${path}`, { path });
|
|
2309
2284
|
}
|
|
2310
2285
|
}
|
|
2311
2286
|
async #removeLock(path) {
|
|
@@ -2321,7 +2296,7 @@ var FileBrowserStore = class {
|
|
|
2321
2296
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
|
|
2322
2297
|
}
|
|
2323
2298
|
}
|
|
2324
|
-
throw new BrowserError(`Cannot remove lock directory: ${path}`,
|
|
2299
|
+
throw new BrowserError("STORE_ACCESS", `Cannot remove lock directory: ${path}`, { path });
|
|
2325
2300
|
}
|
|
2326
2301
|
}
|
|
2327
2302
|
};
|
|
@@ -2347,24 +2322,29 @@ var FileBrowserJourneyStore = class {
|
|
|
2347
2322
|
if (source === void 0) return void 0;
|
|
2348
2323
|
try {
|
|
2349
2324
|
const value = parseJSON(source);
|
|
2350
|
-
if (!isRecord(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new BrowserError("Malformed journey revision"
|
|
2351
|
-
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");
|
|
2352
2327
|
validateBrowserJourney(value["journey"]);
|
|
2353
|
-
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");
|
|
2354
2329
|
return {
|
|
2355
2330
|
journey: value["journey"],
|
|
2356
2331
|
revision: value["revision"]
|
|
2357
2332
|
};
|
|
2358
2333
|
} catch (error) {
|
|
2359
2334
|
const translated = this.#files.translateError(path, error);
|
|
2360
|
-
throw new BrowserError(translated.
|
|
2335
|
+
throw new BrowserError(translated.code, translated.message, {
|
|
2361
2336
|
...translated.context,
|
|
2362
2337
|
reason: normalizeBrowserJourneyReason(error)
|
|
2363
2338
|
});
|
|
2364
2339
|
}
|
|
2365
2340
|
}
|
|
2366
|
-
async set(journey,
|
|
2341
|
+
async set(journey, options) {
|
|
2367
2342
|
options?.signal?.throwIfAborted();
|
|
2343
|
+
validateBrowserJourneyWriteOptions(options);
|
|
2344
|
+
const condition = {
|
|
2345
|
+
revision: options?.revision,
|
|
2346
|
+
exclusive: options?.exclusive
|
|
2347
|
+
};
|
|
2368
2348
|
this.#files.validateName(journey.name);
|
|
2369
2349
|
validateBrowserJourney(journey);
|
|
2370
2350
|
const owned = structuredClone(journey);
|
|
@@ -2372,12 +2352,12 @@ var FileBrowserJourneyStore = class {
|
|
|
2372
2352
|
const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
|
|
2373
2353
|
return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
|
|
2374
2354
|
const current = await this.get(owned.name, options);
|
|
2375
|
-
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`);
|
|
2376
2356
|
const source = await this.#files.read(counter, options);
|
|
2377
2357
|
const previous = source === void 0 ? 0 : Number(source);
|
|
2378
|
-
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}`);
|
|
2379
2359
|
const revision = Math.max(previous, current?.revision ?? 0) + 1;
|
|
2380
|
-
if (!Number.isSafeInteger(revision)) throw new BrowserError(`Exhausted revision: ${counter}
|
|
2360
|
+
if (!Number.isSafeInteger(revision)) throw new BrowserError("STORE_FILE", `Exhausted revision: ${counter}`);
|
|
2381
2361
|
const saved = {
|
|
2382
2362
|
journey: owned,
|
|
2383
2363
|
revision
|
|
@@ -2402,7 +2382,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2402
2382
|
const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
|
|
2403
2383
|
const source = await this.#files.read(counter, options);
|
|
2404
2384
|
const previous = source === void 0 ? 0 : Number(source);
|
|
2405
|
-
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}`);
|
|
2406
2386
|
await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
|
|
2407
2387
|
await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
|
|
2408
2388
|
}, options);
|
|
@@ -2424,7 +2404,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2424
2404
|
* still refuses conflicting mutations and allocations from another process.
|
|
2425
2405
|
* @example
|
|
2426
2406
|
* const store = new FileBrowserRunStore({ root: directory })
|
|
2427
|
-
* const slot = await store.
|
|
2407
|
+
* const slot = await store.create('check-ready')
|
|
2428
2408
|
*/
|
|
2429
2409
|
var FileBrowserRunStore = class FileBrowserRunStore {
|
|
2430
2410
|
static #allocations = /* @__PURE__ */ new Map();
|
|
@@ -2434,7 +2414,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2434
2414
|
constructor(options) {
|
|
2435
2415
|
this.#files = new FileBrowserStore(options);
|
|
2436
2416
|
}
|
|
2437
|
-
async
|
|
2417
|
+
async write(bytes, options) {
|
|
2438
2418
|
options?.signal?.throwIfAborted();
|
|
2439
2419
|
const owned = new Uint8Array(bytes);
|
|
2440
2420
|
const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
|
|
@@ -2452,7 +2432,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2452
2432
|
throw this.#files.translateError(path, error);
|
|
2453
2433
|
}
|
|
2454
2434
|
}
|
|
2455
|
-
async
|
|
2435
|
+
async create(name, options) {
|
|
2456
2436
|
options?.signal?.throwIfAborted();
|
|
2457
2437
|
this.#files.validateName(name);
|
|
2458
2438
|
const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
|
|
@@ -2486,10 +2466,10 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2486
2466
|
if (source === void 0) return void 0;
|
|
2487
2467
|
try {
|
|
2488
2468
|
const value = parseJSON(source);
|
|
2489
|
-
if (!isRecord(value) || !("format" in value)) throw new BrowserError("Missing run format"
|
|
2490
|
-
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");
|
|
2491
2471
|
validateBrowserRun(value);
|
|
2492
|
-
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");
|
|
2493
2473
|
return value;
|
|
2494
2474
|
} catch (error) {
|
|
2495
2475
|
throw this.#files.translateError(path, error);
|
|
@@ -2508,7 +2488,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2508
2488
|
async capture(slot, name, bytes, options) {
|
|
2509
2489
|
options?.signal?.throwIfAborted();
|
|
2510
2490
|
const directory = this.#slots.get(slot);
|
|
2511
|
-
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");
|
|
2512
2492
|
const owned = new Uint8Array(bytes);
|
|
2513
2493
|
await this.#requireDirectory(directory, options);
|
|
2514
2494
|
await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
|
|
@@ -2561,12 +2541,12 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2561
2541
|
return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
|
|
2562
2542
|
}
|
|
2563
2543
|
async #requireDirectory(directory, options) {
|
|
2564
|
-
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}`);
|
|
2565
2545
|
try {
|
|
2566
2546
|
options?.signal?.throwIfAborted();
|
|
2567
2547
|
const status = await lstat(directory);
|
|
2568
2548
|
options?.signal?.throwIfAborted();
|
|
2569
|
-
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}`);
|
|
2570
2550
|
} catch (error) {
|
|
2571
2551
|
options?.signal?.throwIfAborted();
|
|
2572
2552
|
throw this.#files.translateError(directory, error);
|
|
@@ -2603,7 +2583,7 @@ function createBrowser(options) {
|
|
|
2603
2583
|
* @param options - The CDP WebSocket debugger URL (and optional timeout)
|
|
2604
2584
|
* @returns A {@link CDPTransportInterface}
|
|
2605
2585
|
*/
|
|
2606
|
-
function
|
|
2586
|
+
function createWebSocketCDPTransport(options) {
|
|
2607
2587
|
return new WebSocketCDPTransport(options);
|
|
2608
2588
|
}
|
|
2609
2589
|
/**
|
|
@@ -2612,7 +2592,7 @@ function createCDPTransport(options) {
|
|
|
2612
2592
|
*
|
|
2613
2593
|
* @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
|
|
2614
2594
|
*/
|
|
2615
|
-
function
|
|
2595
|
+
function createFileBrowserWriter() {
|
|
2616
2596
|
return new FileBrowserWriter();
|
|
2617
2597
|
}
|
|
2618
2598
|
/**
|
|
@@ -2646,7 +2626,7 @@ function createFileBrowserRunStore(options) {
|
|
|
2646
2626
|
* ```ts
|
|
2647
2627
|
* import { createBrowserMCPServer } from '@orkestrel/browser/server'
|
|
2648
2628
|
*
|
|
2649
|
-
* const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
|
|
2629
|
+
* const server = createBrowserMCPServer({ root: 'tmp/browsers', browser: { headless: true } })
|
|
2650
2630
|
* await server.start() // resolves after leasing a connected browser
|
|
2651
2631
|
* await server.destroy()
|
|
2652
2632
|
* ```
|
|
@@ -2701,7 +2681,7 @@ var Browser = class {
|
|
|
2701
2681
|
...options?.error !== void 0 ? { error: options.error } : {}
|
|
2702
2682
|
});
|
|
2703
2683
|
this.#options = options ?? {};
|
|
2704
|
-
this.#engine = this.#options.
|
|
2684
|
+
this.#engine = this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : this.#options.browsers?.engine ?? "chromium";
|
|
2705
2685
|
this.#cdpPort = this.#options.cdp?.port ?? 9222;
|
|
2706
2686
|
this.#cdpHost = this.#options.cdp?.host ?? "127.0.0.1";
|
|
2707
2687
|
queueMicrotask(() => this.#emitter.emit("idle"));
|
|
@@ -2728,9 +2708,9 @@ var Browser = class {
|
|
|
2728
2708
|
return this.#endpoint;
|
|
2729
2709
|
}
|
|
2730
2710
|
async ping(options) {
|
|
2731
|
-
if (this.#destroyed) throw new
|
|
2711
|
+
if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2732
2712
|
const client = this.#client;
|
|
2733
|
-
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");
|
|
2734
2714
|
await client.send("Browser.getVersion", void 0, options);
|
|
2735
2715
|
}
|
|
2736
2716
|
async discover() {
|
|
@@ -2739,7 +2719,7 @@ var Browser = class {
|
|
|
2739
2719
|
return result;
|
|
2740
2720
|
}
|
|
2741
2721
|
async connect() {
|
|
2742
|
-
if (this.#destroyed) throw new
|
|
2722
|
+
if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2743
2723
|
const active = this.#connecting.pending;
|
|
2744
2724
|
if (active !== void 0) {
|
|
2745
2725
|
await active;
|
|
@@ -2749,8 +2729,8 @@ var Browser = class {
|
|
|
2749
2729
|
await this.#connecting.execute(() => this.#establish());
|
|
2750
2730
|
}
|
|
2751
2731
|
adopt() {
|
|
2752
|
-
if (this.#destroyed) throw new
|
|
2753
|
-
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");
|
|
2754
2734
|
this.#owned = true;
|
|
2755
2735
|
}
|
|
2756
2736
|
async disconnect() {
|
|
@@ -2781,12 +2761,15 @@ var Browser = class {
|
|
|
2781
2761
|
}
|
|
2782
2762
|
}
|
|
2783
2763
|
async create(options) {
|
|
2784
|
-
if (this.#destroyed) throw new
|
|
2764
|
+
if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2785
2765
|
const client = this.#client;
|
|
2786
|
-
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");
|
|
2787
2767
|
let context = this.#contexts[0];
|
|
2788
2768
|
if (context === void 0) {
|
|
2789
|
-
context = new BrowserContext(client,
|
|
2769
|
+
context = new BrowserContext(client, {
|
|
2770
|
+
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
2771
|
+
writer: createFileBrowserWriter()
|
|
2772
|
+
});
|
|
2790
2773
|
this.#registerContext(context);
|
|
2791
2774
|
}
|
|
2792
2775
|
const page = await context.create(options);
|
|
@@ -2814,10 +2797,11 @@ var Browser = class {
|
|
|
2814
2797
|
return shutdown;
|
|
2815
2798
|
}
|
|
2816
2799
|
async #isolate(options) {
|
|
2817
|
-
if (this.#destroyed) throw new
|
|
2800
|
+
if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2818
2801
|
const client = this.#client;
|
|
2819
|
-
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");
|
|
2820
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");
|
|
2821
2805
|
const params = { disposeOnDetach: false };
|
|
2822
2806
|
if (options?.proxy !== void 0) {
|
|
2823
2807
|
params["proxyServer"] = options.proxy.server;
|
|
@@ -2825,18 +2809,25 @@ var Browser = class {
|
|
|
2825
2809
|
}
|
|
2826
2810
|
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2827
2811
|
const result = await client.send("Target.createBrowserContext", params);
|
|
2828
|
-
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");
|
|
2829
2813
|
const id = result["browserContextId"];
|
|
2830
|
-
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
|
+
});
|
|
2831
2822
|
try {
|
|
2832
|
-
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" });
|
|
2833
2824
|
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
2834
2825
|
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
2835
2826
|
browserContextId: id,
|
|
2836
2827
|
downloadPath: options.downloads.path,
|
|
2837
2828
|
eventsEnabled: true
|
|
2838
2829
|
});
|
|
2839
|
-
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" });
|
|
2840
2831
|
} catch (error) {
|
|
2841
2832
|
await context.close().catch(() => void 0);
|
|
2842
2833
|
throw error;
|
|
@@ -2848,7 +2839,7 @@ var Browser = class {
|
|
|
2848
2839
|
const disconnecting = this.#disconnecting.pending;
|
|
2849
2840
|
if (disconnecting !== void 0) await disconnecting;
|
|
2850
2841
|
await this.#settleExit();
|
|
2851
|
-
if (this.#destroyed) throw new
|
|
2842
|
+
if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2852
2843
|
if (this.#status === "connected") return;
|
|
2853
2844
|
this.#assertNotAborted();
|
|
2854
2845
|
this.#status = "connecting";
|
|
@@ -2873,12 +2864,12 @@ var Browser = class {
|
|
|
2873
2864
|
this.#assertNotAborted();
|
|
2874
2865
|
await this.#launch();
|
|
2875
2866
|
} catch (error) {
|
|
2876
|
-
if (this.#destroyed) throw new
|
|
2867
|
+
if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2877
2868
|
this.#status = "error";
|
|
2878
2869
|
this.#emitter.emit("error", error);
|
|
2879
|
-
if (
|
|
2870
|
+
if (isBrowserError(error) && error.code === "CONNECTION") throw error;
|
|
2880
2871
|
const message = isError(error) ? error.message : String(error);
|
|
2881
|
-
throw new
|
|
2872
|
+
throw new BrowserError("CONNECTION", message, this.#options.executable === void 0 ? void 0 : { executable: this.#options.executable });
|
|
2882
2873
|
}
|
|
2883
2874
|
}
|
|
2884
2875
|
async #detach() {
|
|
@@ -2899,7 +2890,7 @@ var Browser = class {
|
|
|
2899
2890
|
this.#emitter.emit("idle");
|
|
2900
2891
|
}
|
|
2901
2892
|
#assertNotAborted() {
|
|
2902
|
-
if (this.#signal().aborted) throw new
|
|
2893
|
+
if (this.#signal().aborted) throw new BrowserError("CONNECTION", "Connection aborted");
|
|
2903
2894
|
}
|
|
2904
2895
|
#signal() {
|
|
2905
2896
|
const external = this.#options.signal;
|
|
@@ -2907,10 +2898,10 @@ var Browser = class {
|
|
|
2907
2898
|
}
|
|
2908
2899
|
async #raceAbort(promise) {
|
|
2909
2900
|
const signal = this.#signal();
|
|
2910
|
-
if (signal.aborted) throw new
|
|
2901
|
+
if (signal.aborted) throw new BrowserError("CONNECTION", "Connection aborted");
|
|
2911
2902
|
const aborted = Promise.withResolvers();
|
|
2912
2903
|
const listener = addAbortListener(signal, () => {
|
|
2913
|
-
aborted.reject(new
|
|
2904
|
+
aborted.reject(new BrowserError("CONNECTION", "Connection aborted"));
|
|
2914
2905
|
});
|
|
2915
2906
|
try {
|
|
2916
2907
|
return await Promise.race([promise, aborted.promise]);
|
|
@@ -2938,7 +2929,7 @@ var Browser = class {
|
|
|
2938
2929
|
this.#owned = void 0;
|
|
2939
2930
|
this.#endpoint = void 0;
|
|
2940
2931
|
if (connected) this.#status = "disconnected";
|
|
2941
|
-
this.#emitter.emit("error", new
|
|
2932
|
+
this.#emitter.emit("error", new BrowserError("CONNECTION", "The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
2942
2933
|
if (connected) {
|
|
2943
2934
|
this.#emitter.emit("disconnect");
|
|
2944
2935
|
this.#emitter.emit("idle");
|
|
@@ -2976,7 +2967,7 @@ var Browser = class {
|
|
|
2976
2967
|
}
|
|
2977
2968
|
this.#destroyContextList(contexts);
|
|
2978
2969
|
this.#closeClient(client);
|
|
2979
|
-
this.#emitter.emit("error", new
|
|
2970
|
+
this.#emitter.emit("error", new BrowserError("CONNECTION", "The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
|
|
2980
2971
|
this.#emitter.emit("disconnect");
|
|
2981
2972
|
this.#emitter.emit("idle");
|
|
2982
2973
|
}
|
|
@@ -3029,7 +3020,7 @@ var Browser = class {
|
|
|
3029
3020
|
};
|
|
3030
3021
|
}
|
|
3031
3022
|
async #assertPortFree() {
|
|
3032
|
-
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`, {
|
|
3033
3024
|
port: this.#cdpPort,
|
|
3034
3025
|
host: this.#cdpHost
|
|
3035
3026
|
});
|
|
@@ -3043,7 +3034,7 @@ var Browser = class {
|
|
|
3043
3034
|
}
|
|
3044
3035
|
}
|
|
3045
3036
|
async #connectCDP(endpoint) {
|
|
3046
|
-
const transport =
|
|
3037
|
+
const transport = createWebSocketCDPTransport({
|
|
3047
3038
|
url: endpoint,
|
|
3048
3039
|
timeout: this.#timeout()
|
|
3049
3040
|
});
|
|
@@ -3076,20 +3067,20 @@ var Browser = class {
|
|
|
3076
3067
|
}
|
|
3077
3068
|
}
|
|
3078
3069
|
async #launch() {
|
|
3079
|
-
if (this.#process !== void 0) throw new
|
|
3080
|
-
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;
|
|
3081
3072
|
let executable = this.#options.executable;
|
|
3082
3073
|
let resolvedEngine;
|
|
3083
3074
|
if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
|
|
3084
3075
|
else {
|
|
3085
|
-
const found =
|
|
3076
|
+
const found = findSystemBrowsers({
|
|
3086
3077
|
...this.#options.browsers,
|
|
3087
3078
|
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
3088
|
-
});
|
|
3079
|
+
})[0];
|
|
3089
3080
|
executable = found?.executable;
|
|
3090
3081
|
resolvedEngine = found?.engine;
|
|
3091
3082
|
}
|
|
3092
|
-
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 });
|
|
3093
3084
|
this.#engine = resolvedEngine ?? "chromium";
|
|
3094
3085
|
await this.#releaseProfile();
|
|
3095
3086
|
const profile = await createBrowserProfile(this.#options.profile);
|
|
@@ -3105,7 +3096,7 @@ var Browser = class {
|
|
|
3105
3096
|
let client;
|
|
3106
3097
|
try {
|
|
3107
3098
|
const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
|
|
3108
|
-
const transport =
|
|
3099
|
+
const transport = createWebSocketCDPTransport({
|
|
3109
3100
|
url: endpoint,
|
|
3110
3101
|
timeout: this.#timeout()
|
|
3111
3102
|
});
|
|
@@ -3169,9 +3160,9 @@ var Browser = class {
|
|
|
3169
3160
|
}
|
|
3170
3161
|
if (pid === void 0) {
|
|
3171
3162
|
await this.#closeRemote(client);
|
|
3172
|
-
throw new
|
|
3173
|
-
executable: this.#options.executable,
|
|
3174
|
-
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 }
|
|
3175
3166
|
});
|
|
3176
3167
|
}
|
|
3177
3168
|
this.#servingPid = pid;
|
|
@@ -3179,7 +3170,7 @@ var Browser = class {
|
|
|
3179
3170
|
async #waitForLaunch(process, executable, args) {
|
|
3180
3171
|
const context = {
|
|
3181
3172
|
executable,
|
|
3182
|
-
args
|
|
3173
|
+
...args === void 0 ? {} : { args }
|
|
3183
3174
|
};
|
|
3184
3175
|
const timeout = this.#timeout();
|
|
3185
3176
|
const controller = new AbortController();
|
|
@@ -3190,32 +3181,32 @@ var Browser = class {
|
|
|
3190
3181
|
deadline
|
|
3191
3182
|
]);
|
|
3192
3183
|
const stderr = process.stderr;
|
|
3193
|
-
if (stderr === null) throw new
|
|
3184
|
+
if (stderr === null) throw new BrowserError("CONNECTION", "The browser process has no standard error to read", context);
|
|
3194
3185
|
const exit = once(process, "exit", { signal });
|
|
3195
3186
|
exit.catch(() => void 0);
|
|
3196
3187
|
const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
|
|
3197
3188
|
if (signal.aborted) throw error;
|
|
3198
3189
|
const values = await exit;
|
|
3199
3190
|
if (values[0] === 0 && values[1] === null) throw error;
|
|
3200
|
-
throw new
|
|
3191
|
+
throw new BrowserError("CONNECTION", this.#formatLaunchExit(isInteger(values[0]) ? values[0] : null, isString(values[1]) ? values[1] : null), context);
|
|
3201
3192
|
});
|
|
3202
3193
|
const exited = exit.then((values) => {
|
|
3203
3194
|
const code = isInteger(values[0]) ? values[0] : null;
|
|
3204
3195
|
const exitSignal = isString(values[1]) ? values[1] : null;
|
|
3205
3196
|
if (code === 0 && exitSignal === null) return ready;
|
|
3206
|
-
throw new
|
|
3197
|
+
throw new BrowserError("CONNECTION", this.#formatLaunchExit(code, exitSignal), context);
|
|
3207
3198
|
});
|
|
3208
3199
|
try {
|
|
3209
3200
|
return await Promise.race([ready, exited]);
|
|
3210
3201
|
} catch (error) {
|
|
3211
|
-
if (
|
|
3212
|
-
if (this.#signal().aborted) throw new
|
|
3213
|
-
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`, {
|
|
3214
3205
|
...context,
|
|
3215
3206
|
timeout
|
|
3216
3207
|
});
|
|
3217
3208
|
const message = isError(error) ? error.message : String(error);
|
|
3218
|
-
throw new
|
|
3209
|
+
throw new BrowserError("CONNECTION", message, context);
|
|
3219
3210
|
} finally {
|
|
3220
3211
|
controller.abort();
|
|
3221
3212
|
ready.catch(() => void 0);
|
|
@@ -3252,7 +3243,10 @@ var Browser = class {
|
|
|
3252
3243
|
});
|
|
3253
3244
|
}
|
|
3254
3245
|
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
3255
|
-
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
|
+
});
|
|
3256
3250
|
await context.sync(pages);
|
|
3257
3251
|
this.#registerContext(context);
|
|
3258
3252
|
}
|
|
@@ -3332,7 +3326,7 @@ var Browser = class {
|
|
|
3332
3326
|
let remote = client;
|
|
3333
3327
|
let temporary = false;
|
|
3334
3328
|
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
3335
|
-
const transport =
|
|
3329
|
+
const transport = createWebSocketCDPTransport({
|
|
3336
3330
|
url: this.#endpoint,
|
|
3337
3331
|
timeout: this.#timeout()
|
|
3338
3332
|
});
|
|
@@ -3435,10 +3429,11 @@ var Browser = class {
|
|
|
3435
3429
|
const code = isError(error) && "code" in error && isString(error.code) ? error.code : void 0;
|
|
3436
3430
|
if (code === "ESRCH") return false;
|
|
3437
3431
|
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
3438
|
-
|
|
3439
|
-
|
|
3432
|
+
const target = serving ?? pid;
|
|
3433
|
+
throw new BrowserError("CONNECTION", "Failed to signal the browser process", {
|
|
3434
|
+
...target === void 0 ? {} : { pid: target },
|
|
3440
3435
|
signal,
|
|
3441
|
-
cause: error
|
|
3436
|
+
cause: isJSONValue(error) ? error : String(error)
|
|
3442
3437
|
});
|
|
3443
3438
|
}
|
|
3444
3439
|
}
|
|
@@ -3477,7 +3472,8 @@ var Browser = class {
|
|
|
3477
3472
|
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
3478
3473
|
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
3479
3474
|
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
3480
|
-
|
|
3475
|
+
const target = this.#servingPid ?? process.pid;
|
|
3476
|
+
throw new BrowserError("CONNECTION", "Browser process did not exit after SIGKILL", { ...target === void 0 ? {} : { pid: target } });
|
|
3481
3477
|
} finally {
|
|
3482
3478
|
await this.#closeProcessPipe(process);
|
|
3483
3479
|
}
|
|
@@ -3506,6 +3502,6 @@ var Browser = class {
|
|
|
3506
3502
|
}
|
|
3507
3503
|
};
|
|
3508
3504
|
//#endregion
|
|
3509
|
-
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 };
|
|
3510
3506
|
|
|
3511
3507
|
//# sourceMappingURL=index.js.map
|