@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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let _src_core = require("../core/index.cjs");
|
|
3
|
-
let _orkestrel_contract = require("@orkestrel/contract");
|
|
4
2
|
let node_fs = require("node:fs");
|
|
5
3
|
let node_fs_promises = require("node:fs/promises");
|
|
6
4
|
let node_os = require("node:os");
|
|
7
5
|
let node_path = require("node:path");
|
|
8
6
|
let node_child_process = require("node:child_process");
|
|
7
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
8
|
+
let _src_core = require("../core/index.cjs");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
10
10
|
let node_http = require("node:http");
|
|
11
11
|
let node_https = require("node:https");
|
|
@@ -227,9 +227,9 @@ var BROWSER_RUN_DIRECTORY = "runs";
|
|
|
227
227
|
/** Bounds a file-store listing page by default. */
|
|
228
228
|
var BROWSER_FILE_STORE_LIMIT = 100;
|
|
229
229
|
/** Names the refusal when no browser can serve a call. */
|
|
230
|
-
var BROWSER_SERVER_UNAVAILABLE = "
|
|
230
|
+
var BROWSER_SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
|
|
231
231
|
/** Names a failed attempt to warm a browser. */
|
|
232
|
-
var BROWSER_SERVER_LAUNCH = "
|
|
232
|
+
var BROWSER_SERVER_LAUNCH = "SERVER_LAUNCH";
|
|
233
233
|
/** Sets the default number of warm browsers to 1. */
|
|
234
234
|
var BROWSER_SERVER_POOL_SIZE = 1;
|
|
235
235
|
/** Sets the default context capacity per browser, including the shared holder. */
|
|
@@ -243,21 +243,21 @@ var BROWSER_SERVER_RESTARTS = 1;
|
|
|
243
243
|
/** Names the browser process record in each profile. */
|
|
244
244
|
var BROWSER_SERVER_RECORD = "browse.json";
|
|
245
245
|
/** Names the diagnostic when the warm floor spends its restart bound. */
|
|
246
|
-
var BROWSER_SERVER_EXHAUSTED = "
|
|
246
|
+
var BROWSER_SERVER_EXHAUSTED = "SERVER_EXHAUSTED";
|
|
247
247
|
/** Names a failed browser server teardown. */
|
|
248
|
-
var BROWSER_SERVER_TEARDOWN = "
|
|
248
|
+
var BROWSER_SERVER_TEARDOWN = "SERVER_TEARDOWN";
|
|
249
249
|
/** Names a failed profile sweep. */
|
|
250
|
-
var BROWSER_SERVER_SWEEP = "
|
|
250
|
+
var BROWSER_SERVER_SWEEP = "SERVER_SWEEP";
|
|
251
251
|
/** Names a refused browser server option. */
|
|
252
|
-
var BROWSER_SERVER_OPTIONS = "
|
|
252
|
+
var BROWSER_SERVER_OPTIONS = "ARGUMENT";
|
|
253
253
|
/** Names the notice that a browser and its session state were lost. */
|
|
254
|
-
var BROWSER_SERVER_CRASH = "
|
|
254
|
+
var BROWSER_SERVER_CRASH = "SERVER_CRASH";
|
|
255
255
|
/** Names an interrupted call whose outcome is unknown. */
|
|
256
|
-
var BROWSER_SERVER_UNRESOLVED = "
|
|
256
|
+
var BROWSER_SERVER_UNRESOLVED = "SERVER_UNRESOLVED";
|
|
257
257
|
/** Names the refusal when every browser is admitted to a holder. */
|
|
258
|
-
var BROWSER_SERVER_BUSY = "
|
|
258
|
+
var BROWSER_SERVER_BUSY = "SERVER_BUSY";
|
|
259
259
|
/** Names the refusal for an unknown or ended holder. */
|
|
260
|
-
var BROWSER_SERVER_HOLDER = "
|
|
260
|
+
var BROWSER_SERVER_HOLDER = "SERVER_HOLDER";
|
|
261
261
|
/** Defines the holder tools independently of the shared browser's vocabulary. */
|
|
262
262
|
var BROWSER_SERVER_COPY = Object.freeze({
|
|
263
263
|
acquire: Object.freeze({
|
|
@@ -333,46 +333,6 @@ var BROWSER_SERVER_COPY = Object.freeze({
|
|
|
333
333
|
})
|
|
334
334
|
});
|
|
335
335
|
//#endregion
|
|
336
|
-
//#region src/server/errors.ts
|
|
337
|
-
/**
|
|
338
|
-
* Reports that an operation requiring an active connection was attempted while disconnected,
|
|
339
|
-
* under the code `BROWSER_NOT_CONNECTED_ERROR`.
|
|
340
|
-
*/
|
|
341
|
-
var BrowserNotConnectedError = class extends _src_core.BrowserError {
|
|
342
|
-
constructor(context) {
|
|
343
|
-
super("Browser is not connected", "BROWSER_NOT_CONNECTED_ERROR", context);
|
|
344
|
-
this.name = "BrowserNotConnectedError";
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
/**
|
|
348
|
-
* Reports that an operation was attempted after the browser wrapper was destroyed, under the
|
|
349
|
-
* code `BROWSER_DESTROYED_ERROR`.
|
|
350
|
-
*/
|
|
351
|
-
var BrowserDestroyedError = class extends _src_core.BrowserError {
|
|
352
|
-
constructor(context) {
|
|
353
|
-
super("Browser has been destroyed", "BROWSER_DESTROYED_ERROR", context);
|
|
354
|
-
this.name = "BrowserDestroyedError";
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
/**
|
|
358
|
-
* Narrows an unknown value to a `BrowserNotConnectedError`.
|
|
359
|
-
*
|
|
360
|
-
* @param value - Value to check
|
|
361
|
-
* @returns True if value is a BrowserNotConnectedError instance; false otherwise
|
|
362
|
-
*/
|
|
363
|
-
function isBrowserNotConnectedError(value) {
|
|
364
|
-
return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Narrows an unknown value to a `BrowserDestroyedError`.
|
|
368
|
-
*
|
|
369
|
-
* @param value - Value to check
|
|
370
|
-
* @returns True if value is a BrowserDestroyedError instance; false otherwise
|
|
371
|
-
*/
|
|
372
|
-
function isBrowserDestroyedError(value) {
|
|
373
|
-
return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
|
|
374
|
-
}
|
|
375
|
-
//#endregion
|
|
376
336
|
//#region src/server/helpers.ts
|
|
377
337
|
/**
|
|
378
338
|
* Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
|
|
@@ -435,15 +395,15 @@ function parseBrowserProfileRecord(text) {
|
|
|
435
395
|
* @param url - The lost page's last URL, when known
|
|
436
396
|
* @returns The coded diagnostic and any operation-lifecycle guidance
|
|
437
397
|
* @example
|
|
438
|
-
* describeBrowserServerLoss('
|
|
398
|
+
* describeBrowserServerLoss('SERVER_UNAVAILABLE', new Error('Browser exited'))
|
|
439
399
|
*/
|
|
440
400
|
function describeBrowserServerLoss(code, cause, url) {
|
|
441
401
|
const pid = (0, _src_core.isBrowserError)(cause) ? cause.context?.["pid"] : void 0;
|
|
442
402
|
const message = `${code}: ${(cause === void 0 ? "Browser session lost" : (0, _orkestrel_contract.isError)(cause) ? cause.message : String(cause)).replace(/\.+$/u, "")}${(0, _orkestrel_contract.isNumber)(pid) ? ` (pid ${pid})` : ""}.`;
|
|
443
|
-
if (code !== "
|
|
444
|
-
const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the
|
|
403
|
+
if (code !== "SERVER_CRASH" && code !== "SERVER_UNRESOLVED") return message;
|
|
404
|
+
const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the last projection, dialogs, holds, an unsaved recording, the active replay, and the isolated context's cookies.`;
|
|
445
405
|
const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
|
|
446
|
-
return code === "
|
|
406
|
+
return code === "SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
|
|
447
407
|
}
|
|
448
408
|
/**
|
|
449
409
|
* Formats a process identifier and UUID token as a lock entry name.
|
|
@@ -518,16 +478,6 @@ function findSystemBrowsers(options) {
|
|
|
518
478
|
return browsers;
|
|
519
479
|
}
|
|
520
480
|
/**
|
|
521
|
-
* Locates a Chrome/Chromium/Edge executable on this machine — the first entry
|
|
522
|
-
* of {@link findSystemBrowsers}.
|
|
523
|
-
*
|
|
524
|
-
* @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
|
|
525
|
-
* @returns The first discovered browser, or undefined
|
|
526
|
-
*/
|
|
527
|
-
function findSystemBrowser(options) {
|
|
528
|
-
return findSystemBrowsers(options)[0];
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
481
|
* Classifies an executable path/name into a {@link BrowserEngine} by
|
|
532
482
|
* case-insensitive hint, checked in the order edge → chromium → chrome.
|
|
533
483
|
*
|
|
@@ -583,7 +533,7 @@ async function removeBrowserProfile(profile) {
|
|
|
583
533
|
if (!profile.temporary) return;
|
|
584
534
|
const path = (0, node_path.resolve)(profile.path);
|
|
585
535
|
const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
|
|
586
|
-
if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("Refusing to remove an unsafe browser profile path",
|
|
536
|
+
if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("ARGUMENT", "Refusing to remove an unsafe browser profile path", { path });
|
|
587
537
|
await (0, node_fs_promises.rm)(path, {
|
|
588
538
|
recursive: true,
|
|
589
539
|
force: true,
|
|
@@ -773,18 +723,18 @@ function readBrowserEndpoint(stream, signal) {
|
|
|
773
723
|
stream.on("close", () => {
|
|
774
724
|
const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
|
|
775
725
|
if (endpoint !== void 0) read.resolve(endpoint);
|
|
776
|
-
else read.reject(new _src_core.
|
|
726
|
+
else read.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
|
|
777
727
|
});
|
|
778
728
|
return read.promise;
|
|
779
729
|
}
|
|
780
730
|
/**
|
|
781
731
|
* Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
|
|
782
|
-
* carrying either the targets or a coded `
|
|
732
|
+
* carrying either the targets or a coded `BrowserError`.
|
|
783
733
|
*
|
|
784
734
|
* @remarks
|
|
785
735
|
* The endpoint is a network boundary, so an unreachable host, a non-2xx
|
|
786
736
|
* response, and a body that is not a JSON array each come back as a failed
|
|
787
|
-
* `Result` carrying a coded `
|
|
737
|
+
* `Result` carrying a coded `BrowserError`. An entry missing a
|
|
788
738
|
* required string field is skipped rather than failing the whole list.
|
|
789
739
|
*
|
|
790
740
|
* @param port - Port the browser exposes its CDP endpoint on
|
|
@@ -809,7 +759,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
809
759
|
const response = await fetch(url, { signal: controller.signal });
|
|
810
760
|
if (!response.ok) return {
|
|
811
761
|
success: false,
|
|
812
|
-
error: new _src_core.
|
|
762
|
+
error: new _src_core.BrowserError("CONNECTION", "CDP target list request was refused", {
|
|
813
763
|
url,
|
|
814
764
|
status: response.status
|
|
815
765
|
})
|
|
@@ -817,7 +767,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
817
767
|
const list = await response.json();
|
|
818
768
|
if (!(0, _orkestrel_contract.isArray)(list)) return {
|
|
819
769
|
success: false,
|
|
820
|
-
error: new _src_core.
|
|
770
|
+
error: new _src_core.BrowserError("CONNECTION", "CDP target list is not a JSON array", { url })
|
|
821
771
|
};
|
|
822
772
|
const targets = [];
|
|
823
773
|
for (const entry of list) {
|
|
@@ -837,9 +787,9 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
837
787
|
} catch (error) {
|
|
838
788
|
return {
|
|
839
789
|
success: false,
|
|
840
|
-
error: new _src_core.
|
|
790
|
+
error: new _src_core.BrowserError("CONNECTION", "CDP target list is unreachable", {
|
|
841
791
|
url,
|
|
842
|
-
error
|
|
792
|
+
error: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
|
|
843
793
|
})
|
|
844
794
|
};
|
|
845
795
|
} finally {
|
|
@@ -905,7 +855,7 @@ var WebSocketCDPTransport = class {
|
|
|
905
855
|
}
|
|
906
856
|
async send(data) {
|
|
907
857
|
const socket = this.#socket;
|
|
908
|
-
if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new _src_core.
|
|
858
|
+
if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new _src_core.BrowserError("CONNECTION", "WebSocket CDP transport is not open", { url: this.#url });
|
|
909
859
|
socket.send(data);
|
|
910
860
|
}
|
|
911
861
|
async close() {
|
|
@@ -913,12 +863,12 @@ var WebSocketCDPTransport = class {
|
|
|
913
863
|
}
|
|
914
864
|
async #start() {
|
|
915
865
|
const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
|
|
916
|
-
if (!parsed.success) throw new _src_core.
|
|
866
|
+
if (!parsed.success) throw new _src_core.BrowserError("CONNECTION", `WebSocket CDP URL is invalid: ${this.#url}`, {
|
|
917
867
|
url: this.#url,
|
|
918
|
-
error: parsed.error
|
|
868
|
+
error: (0, _orkestrel_contract.isJSONValue)(parsed.error) ? parsed.error : String(parsed.error)
|
|
919
869
|
});
|
|
920
870
|
const url = parsed.value;
|
|
921
|
-
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new _src_core.
|
|
871
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
|
|
922
872
|
const key = (0, node_crypto.randomBytes)(16).toString("base64");
|
|
923
873
|
const deferred = Promise.withResolvers();
|
|
924
874
|
const options = {
|
|
@@ -950,7 +900,7 @@ var WebSocketCDPTransport = class {
|
|
|
950
900
|
async #close() {
|
|
951
901
|
const request = this.#request;
|
|
952
902
|
if (request !== void 0) {
|
|
953
|
-
this.#rejectRequest(request, new _src_core.
|
|
903
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
954
904
|
request.destroy();
|
|
955
905
|
}
|
|
956
906
|
await this.#starting.pending?.catch(() => void 0);
|
|
@@ -979,7 +929,7 @@ var WebSocketCDPTransport = class {
|
|
|
979
929
|
const accept = response.headers["sec-websocket-accept"];
|
|
980
930
|
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
981
931
|
socket.destroy();
|
|
982
|
-
this.#rejectRequest(request, new _src_core.
|
|
932
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
|
|
983
933
|
return;
|
|
984
934
|
}
|
|
985
935
|
try {
|
|
@@ -995,13 +945,13 @@ var WebSocketCDPTransport = class {
|
|
|
995
945
|
#decline(request, response) {
|
|
996
946
|
response.resume();
|
|
997
947
|
const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
|
|
998
|
-
this.#rejectRequest(request, new _src_core.
|
|
948
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
|
|
999
949
|
}
|
|
1000
950
|
#fail(request, error) {
|
|
1001
|
-
this.#rejectRequest(request, new _src_core.
|
|
951
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
|
|
1002
952
|
}
|
|
1003
953
|
#expire(request) {
|
|
1004
|
-
this.#rejectRequest(request, new _src_core.
|
|
954
|
+
this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
|
|
1005
955
|
url: this.#url,
|
|
1006
956
|
timeout: this.#timeout
|
|
1007
957
|
}));
|
|
@@ -1073,7 +1023,7 @@ var FileBrowserWriter = class {
|
|
|
1073
1023
|
};
|
|
1074
1024
|
//#endregion
|
|
1075
1025
|
//#region package.json
|
|
1076
|
-
var version = "0.0.
|
|
1026
|
+
var version = "0.0.28";
|
|
1077
1027
|
//#endregion
|
|
1078
1028
|
//#region src/server/BrowserMCPServer.ts
|
|
1079
1029
|
/**
|
|
@@ -1081,9 +1031,9 @@ var version = "0.0.26";
|
|
|
1081
1031
|
* the Model Context Protocol on stdio, and warms Chromium at server start.
|
|
1082
1032
|
*
|
|
1083
1033
|
* @remarks
|
|
1084
|
-
* The server's own manager holds one dispatcher per name of the vocabulary — `
|
|
1085
|
-
* `
|
|
1086
|
-
* `
|
|
1034
|
+
* The server's own manager holds one dispatcher per name of the vocabulary — `read`, `click`,
|
|
1035
|
+
* `type`, `press`, `navigate`, `wait`, `dialog`, `switch`, `record`, `save`, `journeys`, `edit`,
|
|
1036
|
+
* `replay`, `forget`, and `capture` — each carrying the description, parameters, and annotations
|
|
1087
1037
|
* `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
|
|
1088
1038
|
* absent. After the launch, a tool the toolset's manager adds under another name, such as a page
|
|
1089
1039
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
@@ -1120,7 +1070,7 @@ var version = "0.0.26";
|
|
|
1120
1070
|
* ```ts
|
|
1121
1071
|
* import { BrowserMCPServer } from '@orkestrel/browser/server'
|
|
1122
1072
|
*
|
|
1123
|
-
* const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
|
|
1073
|
+
* const server = new BrowserMCPServer({ root: 'tmp/browsers', journeys: { readonly: true } })
|
|
1124
1074
|
* await server.start()
|
|
1125
1075
|
* await server.destroy()
|
|
1126
1076
|
* ```
|
|
@@ -1183,8 +1133,8 @@ var BrowserMCPServer = class {
|
|
|
1183
1133
|
constructor(options) {
|
|
1184
1134
|
const size = options?.pool?.size ?? 1;
|
|
1185
1135
|
const contexts = options?.pool?.contexts ?? 2;
|
|
1186
|
-
if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`,
|
|
1187
|
-
if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(`pool.contexts must be an integer from 1 through 4`,
|
|
1136
|
+
if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(BROWSER_SERVER_OPTIONS, `pool.size must be an integer from 1 through 3`, { subject: "server" });
|
|
1137
|
+
if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(BROWSER_SERVER_OPTIONS, `pool.contexts must be an integer from 1 through 4`, { subject: "server" });
|
|
1188
1138
|
this.#admission = size * contexts;
|
|
1189
1139
|
this.#log = options?.log ?? process.stderr;
|
|
1190
1140
|
this.#reference = this.#issue.bind(this);
|
|
@@ -1199,11 +1149,11 @@ var BrowserMCPServer = class {
|
|
|
1199
1149
|
restarts: 1
|
|
1200
1150
|
});
|
|
1201
1151
|
this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
|
|
1202
|
-
this.#headless = options?.headless ?? true;
|
|
1203
|
-
this.#executable = options?.executable;
|
|
1204
|
-
this.#viewport = options?.viewport;
|
|
1205
|
-
this.#readonly = options?.readonly ?? false;
|
|
1206
|
-
this.#launcher = options?.launch ?? createBrowser;
|
|
1152
|
+
this.#headless = options?.browser?.headless ?? true;
|
|
1153
|
+
this.#executable = options?.browser?.executable;
|
|
1154
|
+
this.#viewport = options?.browser?.viewport;
|
|
1155
|
+
this.#readonly = options?.journeys?.readonly ?? false;
|
|
1156
|
+
this.#launcher = options?.pool?.launch ?? createBrowser;
|
|
1207
1157
|
this.#input = options?.stdio?.input ?? process.stdin;
|
|
1208
1158
|
this.#tools = (0, _orkestrel_tool.createToolManager)();
|
|
1209
1159
|
const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
|
|
@@ -1267,7 +1217,7 @@ var BrowserMCPServer = class {
|
|
|
1267
1217
|
warming.then(this.#exhaust.bind(this));
|
|
1268
1218
|
} catch (error) {
|
|
1269
1219
|
if (this.#closing !== void 0) return;
|
|
1270
|
-
if ((0, _src_core.isBrowserError)(error) && error.code === "
|
|
1220
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "SERVER_UNAVAILABLE") throw error;
|
|
1271
1221
|
throw this.#unavailable((0, _orkestrel_pool.isPoolError)(error) ? error.cause ?? this.#failure : error);
|
|
1272
1222
|
}
|
|
1273
1223
|
}
|
|
@@ -1317,12 +1267,16 @@ var BrowserMCPServer = class {
|
|
|
1317
1267
|
signal: context.signal,
|
|
1318
1268
|
...context.caller === void 0 ? {} : { caller: context.caller }
|
|
1319
1269
|
});
|
|
1320
|
-
if (!result.success
|
|
1270
|
+
if (!result.success) return {
|
|
1271
|
+
...result,
|
|
1272
|
+
error: (0, _src_core.boundBrowserText)(result.error, _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER)
|
|
1273
|
+
};
|
|
1274
|
+
if (typeof result.value !== "string") return result;
|
|
1321
1275
|
return {
|
|
1322
1276
|
resultType: "complete",
|
|
1323
1277
|
content: [{
|
|
1324
1278
|
type: "text",
|
|
1325
|
-
text: result.value
|
|
1279
|
+
text: context.call.name === "acquire" || context.call.name === "tools" ? result.value : (0, _src_core.boundBrowserText)(result.value, _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER)
|
|
1326
1280
|
}]
|
|
1327
1281
|
};
|
|
1328
1282
|
}
|
|
@@ -1338,8 +1292,8 @@ var BrowserMCPServer = class {
|
|
|
1338
1292
|
context.signal.throwIfAborted();
|
|
1339
1293
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1340
1294
|
const purpose = args["purpose"];
|
|
1341
|
-
if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "
|
|
1342
|
-
if (this.#holders.size >= this.#admission) throw new _src_core.BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser
|
|
1295
|
+
if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("ARGUMENT", "purpose must describe the work", { subject: "toolset" });
|
|
1296
|
+
if (this.#holders.size >= this.#admission) throw new _src_core.BrowserError(BROWSER_SERVER_BUSY, `${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`);
|
|
1343
1297
|
const holder = {
|
|
1344
1298
|
id: (0, node_crypto.randomUUID)(),
|
|
1345
1299
|
purpose,
|
|
@@ -1366,14 +1320,14 @@ var BrowserMCPServer = class {
|
|
|
1366
1320
|
}
|
|
1367
1321
|
#holder(value) {
|
|
1368
1322
|
const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
|
|
1369
|
-
if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder
|
|
1323
|
+
if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_HOLDER, `${BROWSER_SERVER_HOLDER}: unknown or ended holder`);
|
|
1370
1324
|
return holder;
|
|
1371
1325
|
}
|
|
1372
1326
|
async #dispatch(args, context) {
|
|
1373
1327
|
const holder = this.#holder(args["holder"]);
|
|
1374
1328
|
const name = args["name"];
|
|
1375
1329
|
const parameters = args["arguments"];
|
|
1376
|
-
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "
|
|
1330
|
+
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("ARGUMENT", "execute requires name and arguments", { subject: "toolset" });
|
|
1377
1331
|
return this.#forward(holder, name, parameters, context);
|
|
1378
1332
|
}
|
|
1379
1333
|
async #catalog(args, context) {
|
|
@@ -1390,8 +1344,8 @@ var BrowserMCPServer = class {
|
|
|
1390
1344
|
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1391
1345
|
} catch (error) {
|
|
1392
1346
|
context.signal.throwIfAborted();
|
|
1393
|
-
if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call
|
|
1394
|
-
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`)
|
|
1347
|
+
if (holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
|
|
1348
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
|
|
1395
1349
|
throw error;
|
|
1396
1350
|
}
|
|
1397
1351
|
}
|
|
@@ -1428,7 +1382,7 @@ var BrowserMCPServer = class {
|
|
|
1428
1382
|
context.signal.throwIfAborted();
|
|
1429
1383
|
const journey = args["journey"];
|
|
1430
1384
|
if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
|
|
1431
|
-
if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError(`
|
|
1385
|
+
if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError("STORE_LOCKED", `STORE_LOCKED: Journey ${journey} is locked; call ${name} again.`, { name: journey });
|
|
1432
1386
|
const reader = Symbol(journey);
|
|
1433
1387
|
if (name === "forget") this.#writers.add(journey);
|
|
1434
1388
|
else {
|
|
@@ -1454,8 +1408,8 @@ var BrowserMCPServer = class {
|
|
|
1454
1408
|
lease = await this.#serve(holder, signal);
|
|
1455
1409
|
} catch (error) {
|
|
1456
1410
|
context.signal.throwIfAborted();
|
|
1457
|
-
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call
|
|
1458
|
-
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`)
|
|
1411
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
|
|
1412
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
|
|
1459
1413
|
throw error;
|
|
1460
1414
|
}
|
|
1461
1415
|
let result;
|
|
@@ -1480,7 +1434,7 @@ var BrowserMCPServer = class {
|
|
|
1480
1434
|
};
|
|
1481
1435
|
}
|
|
1482
1436
|
context.signal.throwIfAborted();
|
|
1483
|
-
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call"
|
|
1437
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
|
|
1484
1438
|
if (!result.success) {
|
|
1485
1439
|
if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
|
|
1486
1440
|
await this.#race(this.#ping(lease.token.value), context.signal);
|
|
@@ -1488,9 +1442,9 @@ var BrowserMCPServer = class {
|
|
|
1488
1442
|
context.signal.throwIfAborted();
|
|
1489
1443
|
if (this.#leases.get(holder) !== lease) {
|
|
1490
1444
|
const loss = this.#departures.get(lease);
|
|
1491
|
-
throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease)
|
|
1445
|
+
throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
|
|
1492
1446
|
}
|
|
1493
|
-
throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
|
|
1447
|
+
throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
|
|
1494
1448
|
}
|
|
1495
1449
|
return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
|
|
1496
1450
|
}
|
|
@@ -1504,6 +1458,10 @@ var BrowserMCPServer = class {
|
|
|
1504
1458
|
}
|
|
1505
1459
|
return [...notes, value].join("\n");
|
|
1506
1460
|
}
|
|
1461
|
+
#notice(context) {
|
|
1462
|
+
const entry = [...this.#leases].find(([, lease]) => lease.generation.context === context);
|
|
1463
|
+
return entry === void 0 ? "" : this.#annotate(entry[0], "");
|
|
1464
|
+
}
|
|
1507
1465
|
async #handshake(options) {
|
|
1508
1466
|
try {
|
|
1509
1467
|
await this.#race(this.#starting, options.signal);
|
|
@@ -1654,7 +1612,7 @@ var BrowserMCPServer = class {
|
|
|
1654
1612
|
if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
|
|
1655
1613
|
this.#departures.set(lease, {
|
|
1656
1614
|
cause,
|
|
1657
|
-
url: lease.generation.toolset.view.url
|
|
1615
|
+
url: lease.generation.toolset.redact(lease.generation.toolset.view.url)
|
|
1658
1616
|
});
|
|
1659
1617
|
if (holder === this.#shared) this.#unmirror();
|
|
1660
1618
|
this.#leases.delete(holder);
|
|
@@ -1796,6 +1754,7 @@ var BrowserMCPServer = class {
|
|
|
1796
1754
|
}
|
|
1797
1755
|
toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1798
1756
|
context,
|
|
1757
|
+
notes: this.#notice.bind(this, context),
|
|
1799
1758
|
journeys: {
|
|
1800
1759
|
store: createFileBrowserJourneyStore({ root: this.#root }),
|
|
1801
1760
|
runs: createFileBrowserRunStore({ root: this.#root }),
|
|
@@ -1815,7 +1774,7 @@ var BrowserMCPServer = class {
|
|
|
1815
1774
|
} catch (error) {
|
|
1816
1775
|
await toolset?.destroy().catch(this.#fault.bind(this));
|
|
1817
1776
|
await context?.close().catch((failure) => {
|
|
1818
|
-
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !(0, _src_core.
|
|
1777
|
+
if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
|
|
1819
1778
|
});
|
|
1820
1779
|
await (0, node_fs_promises.rm)(directory, {
|
|
1821
1780
|
recursive: true,
|
|
@@ -1825,7 +1784,8 @@ var BrowserMCPServer = class {
|
|
|
1825
1784
|
}
|
|
1826
1785
|
}
|
|
1827
1786
|
async #destroyRecord(slot) {
|
|
1828
|
-
|
|
1787
|
+
const cause = this.#losses.get(slot)?.cause;
|
|
1788
|
+
if ((0, _src_core.isBrowserError)(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
|
|
1829
1789
|
process.kill(slot.browser.pid, "SIGKILL");
|
|
1830
1790
|
} catch (error) {
|
|
1831
1791
|
if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
|
|
@@ -1850,7 +1810,7 @@ var BrowserMCPServer = class {
|
|
|
1850
1810
|
}
|
|
1851
1811
|
async #warm() {
|
|
1852
1812
|
if (this.#closing !== void 0) throw this.#ended();
|
|
1853
|
-
if (this.#stranded !== void 0) throw new _src_core.BrowserError((0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded),
|
|
1813
|
+
if (this.#stranded !== void 0) throw new _src_core.BrowserError(BROWSER_SERVER_TEARDOWN, (0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
|
|
1854
1814
|
const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
|
|
1855
1815
|
let browser;
|
|
1856
1816
|
let slot;
|
|
@@ -1923,7 +1883,7 @@ var BrowserMCPServer = class {
|
|
|
1923
1883
|
const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
|
|
1924
1884
|
if (record === void 0) continue;
|
|
1925
1885
|
if (probeProcess(record.pid)) {
|
|
1926
|
-
const transport =
|
|
1886
|
+
const transport = createWebSocketCDPTransport({ url: record.endpoint });
|
|
1927
1887
|
const client = (0, _src_core.createCDPClient)({ transport });
|
|
1928
1888
|
try {
|
|
1929
1889
|
this.#abort.signal.throwIfAborted();
|
|
@@ -1965,7 +1925,7 @@ var BrowserMCPServer = class {
|
|
|
1965
1925
|
}
|
|
1966
1926
|
#unavailable(cause) {
|
|
1967
1927
|
const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
|
|
1968
|
-
return new _src_core.BrowserError(message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2)
|
|
1928
|
+
return new _src_core.BrowserError(BROWSER_SERVER_UNAVAILABLE, message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2));
|
|
1969
1929
|
}
|
|
1970
1930
|
#issue() {
|
|
1971
1931
|
return `e${++this.#references}`;
|
|
@@ -1997,7 +1957,7 @@ var BrowserMCPServer = class {
|
|
|
1997
1957
|
tools.emitter.off("clear", mirrored.cleared);
|
|
1998
1958
|
}
|
|
1999
1959
|
#ended() {
|
|
2000
|
-
return new _src_core.BrowserError("the browser session ended", "
|
|
1960
|
+
return new _src_core.BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
|
|
2001
1961
|
}
|
|
2002
1962
|
};
|
|
2003
1963
|
//#endregion
|
|
@@ -2010,31 +1970,32 @@ var BrowserMCPServer = class {
|
|
|
2010
1970
|
* const files = new FileBrowserStore({ root: directory })
|
|
2011
1971
|
* files.validateName('check-ready')
|
|
2012
1972
|
*/
|
|
2013
|
-
var FileBrowserStore = class {
|
|
1973
|
+
var FileBrowserStore = class FileBrowserStore {
|
|
1974
|
+
static #locks = /* @__PURE__ */ new Map();
|
|
2014
1975
|
#root;
|
|
2015
1976
|
#limit;
|
|
2016
1977
|
constructor(options) {
|
|
2017
1978
|
this.#limit = options.limit ?? 100;
|
|
2018
|
-
if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("The listing cap must be a positive integer", "
|
|
1979
|
+
if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("ARGUMENT", "The listing cap must be a positive integer", { subject: "store" });
|
|
2019
1980
|
try {
|
|
2020
1981
|
this.#root = (0, node_fs.realpathSync)(options.root);
|
|
2021
1982
|
} catch (error) {
|
|
2022
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new _src_core.BrowserError(`Missing root: ${options.root}`,
|
|
1983
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new _src_core.BrowserError("STORE_PATH", `Missing root: ${options.root}`, { path: options.root });
|
|
2023
1984
|
throw this.translateError(options.root, error);
|
|
2024
1985
|
}
|
|
2025
1986
|
}
|
|
2026
1987
|
/** Checks a journey name before filesystem access. @param name - Journey name */
|
|
2027
1988
|
validateName(name) {
|
|
2028
|
-
if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError(`Refused journey name: ${name}
|
|
1989
|
+
if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError("STORE_PATH", `Refused journey name: ${name}`);
|
|
2029
1990
|
}
|
|
2030
1991
|
/** Checks a run id before filesystem access. @param id - Producer id */
|
|
2031
1992
|
validateId(id) {
|
|
2032
|
-
if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError(`Refused run id: ${id}
|
|
1993
|
+
if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError("STORE_PATH", `Refused run id: ${id}`);
|
|
2033
1994
|
}
|
|
2034
1995
|
/** Resolves a confined target. @param parts - Relative components @returns Absolute target */
|
|
2035
1996
|
resolvePath(...parts) {
|
|
2036
1997
|
const path = (0, node_path.resolve)(this.#root, ...parts);
|
|
2037
|
-
if (path !== this.#root && !path.startsWith(this.#root.endsWith(node_path.sep) ? this.#root : this.#root + node_path.sep)) throw new _src_core.BrowserError(`Refused path: ${path}
|
|
1998
|
+
if (path !== this.#root && !path.startsWith(this.#root.endsWith(node_path.sep) ? this.#root : this.#root + node_path.sep)) throw new _src_core.BrowserError("STORE_PATH", `Refused path: ${path}`);
|
|
2038
1999
|
return path;
|
|
2039
2000
|
}
|
|
2040
2001
|
/**
|
|
@@ -2052,8 +2013,8 @@ var FileBrowserStore = class {
|
|
|
2052
2013
|
try {
|
|
2053
2014
|
const status = await (0, node_fs_promises.lstat)(current);
|
|
2054
2015
|
options?.signal?.throwIfAborted();
|
|
2055
|
-
if (status.isSymbolicLink()) throw new _src_core.BrowserError(`Refused symbolic link: ${current}
|
|
2056
|
-
if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError(`Not a directory: ${current}
|
|
2016
|
+
if (status.isSymbolicLink()) throw new _src_core.BrowserError("STORE_PATH", `Refused symbolic link: ${current}`);
|
|
2017
|
+
if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Not a directory: ${current}`);
|
|
2057
2018
|
} catch (error) {
|
|
2058
2019
|
options?.signal?.throwIfAborted();
|
|
2059
2020
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
@@ -2067,7 +2028,7 @@ var FileBrowserStore = class {
|
|
|
2067
2028
|
/** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
|
|
2068
2029
|
async createDirectory(path, options) {
|
|
2069
2030
|
if (await this.check(path, options)) return;
|
|
2070
|
-
if (path === this.#root) throw new _src_core.BrowserError(`Missing root: ${path}
|
|
2031
|
+
if (path === this.#root) throw new _src_core.BrowserError("STORE_PATH", `Missing root: ${path}`);
|
|
2071
2032
|
await this.createDirectory((0, node_path.dirname)(path), options);
|
|
2072
2033
|
options?.signal?.throwIfAborted();
|
|
2073
2034
|
try {
|
|
@@ -2117,7 +2078,7 @@ var FileBrowserStore = class {
|
|
|
2117
2078
|
await (0, node_fs_promises.rename)(temporary, path);
|
|
2118
2079
|
} catch (error) {
|
|
2119
2080
|
const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
|
|
2120
|
-
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError(`Cannot replace directory: ${path}`,
|
|
2081
|
+
if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
|
|
2121
2082
|
throw error;
|
|
2122
2083
|
}
|
|
2123
2084
|
owned = false;
|
|
@@ -2148,6 +2109,20 @@ var FileBrowserStore = class {
|
|
|
2148
2109
|
* @returns The mutation result
|
|
2149
2110
|
*/
|
|
2150
2111
|
async lock(path, action, options) {
|
|
2112
|
+
const key = this.resolvePath(path);
|
|
2113
|
+
const previous = FileBrowserStore.#locks.get(key);
|
|
2114
|
+
const released = Promise.withResolvers();
|
|
2115
|
+
FileBrowserStore.#locks.set(key, released.promise);
|
|
2116
|
+
try {
|
|
2117
|
+
await previous;
|
|
2118
|
+
options?.signal?.throwIfAborted();
|
|
2119
|
+
return await this.#lockFile(key, action, options);
|
|
2120
|
+
} finally {
|
|
2121
|
+
released.resolve();
|
|
2122
|
+
if (FileBrowserStore.#locks.get(key) === released.promise) FileBrowserStore.#locks.delete(key);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async #lockFile(path, action, options) {
|
|
2151
2126
|
await this.createDirectory((0, node_path.dirname)(path), options);
|
|
2152
2127
|
const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
|
|
2153
2128
|
const entry = this.resolvePath(path, name);
|
|
@@ -2190,7 +2165,7 @@ var FileBrowserStore = class {
|
|
|
2190
2165
|
await this.#removeLock(path);
|
|
2191
2166
|
}
|
|
2192
2167
|
}
|
|
2193
|
-
throw new _src_core.BrowserError(`Journey is locked: ${path}
|
|
2168
|
+
throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2194
2169
|
}
|
|
2195
2170
|
/**
|
|
2196
2171
|
* Lists readable entries in sorted order with a bounded page and one extra readable probe.
|
|
@@ -2237,7 +2212,7 @@ var FileBrowserStore = class {
|
|
|
2237
2212
|
const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
|
|
2238
2213
|
faults.push({
|
|
2239
2214
|
name,
|
|
2240
|
-
reason: typeof reason === "string" ? reason : error instanceof _src_core.BrowserError && error.code === "
|
|
2215
|
+
reason: typeof reason === "string" ? reason : error instanceof _src_core.BrowserError && error.code === "STORE_PATH" ? "The entry path is refused" : error instanceof _src_core.BrowserError && error.code === "STORE_ACCESS" ? "Permission denied" : error instanceof _src_core.BrowserError && error.code === "STORE_FORMAT" ? "Unknown file format" : "The stored entry is malformed or unreadable"
|
|
2241
2216
|
});
|
|
2242
2217
|
}
|
|
2243
2218
|
}
|
|
@@ -2280,7 +2255,7 @@ var FileBrowserStore = class {
|
|
|
2280
2255
|
/** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
|
|
2281
2256
|
translateError(path, error) {
|
|
2282
2257
|
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
2283
|
-
return new _src_core.BrowserError(
|
|
2258
|
+
return new _src_core.BrowserError(code === "EACCES" || code === "EPERM" ? "STORE_ACCESS" : error instanceof _src_core.BrowserError && code !== "JOURNEY_INVALID" ? error.code : "STORE_FILE", `${path}: ${error instanceof Error ? error.message : String(error)}`, { path });
|
|
2284
2259
|
}
|
|
2285
2260
|
async #readLock(path, options) {
|
|
2286
2261
|
await this.check(path, options);
|
|
@@ -2288,13 +2263,13 @@ var FileBrowserStore = class {
|
|
|
2288
2263
|
const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true });
|
|
2289
2264
|
for (const entry of entries) {
|
|
2290
2265
|
await this.check(this.resolvePath(path, entry.name), options);
|
|
2291
|
-
if (!entry.isFile()) throw new _src_core.BrowserError(`Journey is locked: ${path}
|
|
2266
|
+
if (!entry.isFile()) throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2292
2267
|
}
|
|
2293
2268
|
return entries.map((entry) => entry.name);
|
|
2294
2269
|
} catch (error) {
|
|
2295
2270
|
if (error instanceof Error && "code" in error) {
|
|
2296
2271
|
if (error.code === "ENOENT") return void 0;
|
|
2297
|
-
if (error.code === "ENOTDIR") throw new _src_core.BrowserError(`Journey is locked: ${path}
|
|
2272
|
+
if (error.code === "ENOTDIR") throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
|
|
2298
2273
|
}
|
|
2299
2274
|
throw this.translateError(path, error);
|
|
2300
2275
|
}
|
|
@@ -2306,7 +2281,7 @@ var FileBrowserStore = class {
|
|
|
2306
2281
|
return true;
|
|
2307
2282
|
} catch (error) {
|
|
2308
2283
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
|
|
2309
|
-
throw new _src_core.BrowserError(`Cannot remove lock entry: ${path}`,
|
|
2284
|
+
throw new _src_core.BrowserError("STORE_ACCESS", `Cannot remove lock entry: ${path}`, { path });
|
|
2310
2285
|
}
|
|
2311
2286
|
}
|
|
2312
2287
|
async #removeLock(path) {
|
|
@@ -2322,7 +2297,7 @@ var FileBrowserStore = class {
|
|
|
2322
2297
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
|
|
2323
2298
|
}
|
|
2324
2299
|
}
|
|
2325
|
-
throw new _src_core.BrowserError(`Cannot remove lock directory: ${path}`,
|
|
2300
|
+
throw new _src_core.BrowserError("STORE_ACCESS", `Cannot remove lock directory: ${path}`, { path });
|
|
2326
2301
|
}
|
|
2327
2302
|
}
|
|
2328
2303
|
};
|
|
@@ -2348,24 +2323,29 @@ var FileBrowserJourneyStore = class {
|
|
|
2348
2323
|
if (source === void 0) return void 0;
|
|
2349
2324
|
try {
|
|
2350
2325
|
const value = (0, _orkestrel_contract.parseJSON)(source);
|
|
2351
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new _src_core.BrowserError("Malformed journey revision"
|
|
2352
|
-
if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("Missing journey format"
|
|
2326
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new _src_core.BrowserError("STORE_FILE", "Malformed journey revision");
|
|
2327
|
+
if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("STORE_FILE", "Missing journey format");
|
|
2353
2328
|
(0, _src_core.validateBrowserJourney)(value["journey"]);
|
|
2354
|
-
if (value["journey"].name !== name) throw new _src_core.BrowserError("Journey name differs from its directory"
|
|
2329
|
+
if (value["journey"].name !== name) throw new _src_core.BrowserError("STORE_FILE", "Journey name differs from its directory");
|
|
2355
2330
|
return {
|
|
2356
2331
|
journey: value["journey"],
|
|
2357
2332
|
revision: value["revision"]
|
|
2358
2333
|
};
|
|
2359
2334
|
} catch (error) {
|
|
2360
2335
|
const translated = this.#files.translateError(path, error);
|
|
2361
|
-
throw new _src_core.BrowserError(translated.
|
|
2336
|
+
throw new _src_core.BrowserError(translated.code, translated.message, {
|
|
2362
2337
|
...translated.context,
|
|
2363
2338
|
reason: (0, _src_core.normalizeBrowserJourneyReason)(error)
|
|
2364
2339
|
});
|
|
2365
2340
|
}
|
|
2366
2341
|
}
|
|
2367
|
-
async set(journey,
|
|
2342
|
+
async set(journey, options) {
|
|
2368
2343
|
options?.signal?.throwIfAborted();
|
|
2344
|
+
(0, _src_core.validateBrowserJourneyWriteOptions)(options);
|
|
2345
|
+
const condition = {
|
|
2346
|
+
revision: options?.revision,
|
|
2347
|
+
exclusive: options?.exclusive
|
|
2348
|
+
};
|
|
2369
2349
|
this.#files.validateName(journey.name);
|
|
2370
2350
|
(0, _src_core.validateBrowserJourney)(journey);
|
|
2371
2351
|
const owned = structuredClone(journey);
|
|
@@ -2373,12 +2353,12 @@ var FileBrowserJourneyStore = class {
|
|
|
2373
2353
|
const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
|
|
2374
2354
|
return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
|
|
2375
2355
|
const current = await this.get(owned.name, options);
|
|
2376
|
-
if (
|
|
2356
|
+
if (condition.exclusive === true && current !== void 0 || condition.revision !== void 0 && condition.revision !== current?.revision) throw new _src_core.BrowserError("JOURNEY_STALE", `Journey ${owned.name} changed since you read it`);
|
|
2377
2357
|
const source = await this.#files.read(counter, options);
|
|
2378
2358
|
const previous = source === void 0 ? 0 : Number(source);
|
|
2379
|
-
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}
|
|
2359
|
+
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
|
|
2380
2360
|
const revision = Math.max(previous, current?.revision ?? 0) + 1;
|
|
2381
|
-
if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError(`Exhausted revision: ${counter}
|
|
2361
|
+
if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError("STORE_FILE", `Exhausted revision: ${counter}`);
|
|
2382
2362
|
const saved = {
|
|
2383
2363
|
journey: owned,
|
|
2384
2364
|
revision
|
|
@@ -2403,7 +2383,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2403
2383
|
const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
|
|
2404
2384
|
const source = await this.#files.read(counter, options);
|
|
2405
2385
|
const previous = source === void 0 ? 0 : Number(source);
|
|
2406
|
-
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}
|
|
2386
|
+
if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
|
|
2407
2387
|
await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
|
|
2408
2388
|
await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
|
|
2409
2389
|
}, options);
|
|
@@ -2425,7 +2405,7 @@ var FileBrowserJourneyStore = class {
|
|
|
2425
2405
|
* still refuses conflicting mutations and allocations from another process.
|
|
2426
2406
|
* @example
|
|
2427
2407
|
* const store = new FileBrowserRunStore({ root: directory })
|
|
2428
|
-
* const slot = await store.
|
|
2408
|
+
* const slot = await store.create('check-ready')
|
|
2429
2409
|
*/
|
|
2430
2410
|
var FileBrowserRunStore = class FileBrowserRunStore {
|
|
2431
2411
|
static #allocations = /* @__PURE__ */ new Map();
|
|
@@ -2435,7 +2415,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2435
2415
|
constructor(options) {
|
|
2436
2416
|
this.#files = new FileBrowserStore(options);
|
|
2437
2417
|
}
|
|
2438
|
-
async
|
|
2418
|
+
async write(bytes, options) {
|
|
2439
2419
|
options?.signal?.throwIfAborted();
|
|
2440
2420
|
const owned = new Uint8Array(bytes);
|
|
2441
2421
|
const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
|
|
@@ -2453,7 +2433,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2453
2433
|
throw this.#files.translateError(path, error);
|
|
2454
2434
|
}
|
|
2455
2435
|
}
|
|
2456
|
-
async
|
|
2436
|
+
async create(name, options) {
|
|
2457
2437
|
options?.signal?.throwIfAborted();
|
|
2458
2438
|
this.#files.validateName(name);
|
|
2459
2439
|
const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
|
|
@@ -2487,10 +2467,10 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2487
2467
|
if (source === void 0) return void 0;
|
|
2488
2468
|
try {
|
|
2489
2469
|
const value = (0, _orkestrel_contract.parseJSON)(source);
|
|
2490
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("Missing run format"
|
|
2491
|
-
if (value["format"] === _src_core.BROWSER_JOURNEY_FORMAT_VERSION && (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"]))) throw new _src_core.BrowserError("Missing run journey format"
|
|
2470
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("STORE_FILE", "Missing run format");
|
|
2471
|
+
if (value["format"] === _src_core.BROWSER_JOURNEY_FORMAT_VERSION && (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"]))) throw new _src_core.BrowserError("STORE_FILE", "Missing run journey format");
|
|
2492
2472
|
(0, _src_core.validateBrowserRun)(value);
|
|
2493
|
-
if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("Run identity differs from its directory"
|
|
2473
|
+
if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("STORE_FILE", "Run identity differs from its directory");
|
|
2494
2474
|
return value;
|
|
2495
2475
|
} catch (error) {
|
|
2496
2476
|
throw this.#files.translateError(path, error);
|
|
@@ -2509,7 +2489,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2509
2489
|
async capture(slot, name, bytes, options) {
|
|
2510
2490
|
options?.signal?.throwIfAborted();
|
|
2511
2491
|
const directory = this.#slots.get(slot);
|
|
2512
|
-
if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new _src_core.BrowserError("Capture requires an opened slot and an sN.png name"
|
|
2492
|
+
if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new _src_core.BrowserError("STORE_PATH", "Capture requires an opened slot and an sN.png name");
|
|
2513
2493
|
const owned = new Uint8Array(bytes);
|
|
2514
2494
|
await this.#requireDirectory(directory, options);
|
|
2515
2495
|
await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
|
|
@@ -2562,12 +2542,12 @@ var FileBrowserRunStore = class FileBrowserRunStore {
|
|
|
2562
2542
|
return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
|
|
2563
2543
|
}
|
|
2564
2544
|
async #requireDirectory(directory, options) {
|
|
2565
|
-
if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new _src_core.BrowserError(`Run directory was not opened or is missing: ${directory}
|
|
2545
|
+
if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new _src_core.BrowserError("STORE_PATH", `Run directory was not opened or is missing: ${directory}`);
|
|
2566
2546
|
try {
|
|
2567
2547
|
options?.signal?.throwIfAborted();
|
|
2568
2548
|
const status = await (0, node_fs_promises.lstat)(directory);
|
|
2569
2549
|
options?.signal?.throwIfAborted();
|
|
2570
|
-
if (!status.isDirectory()) throw new _src_core.BrowserError(`Run directory is not a directory: ${directory}
|
|
2550
|
+
if (!status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Run directory is not a directory: ${directory}`);
|
|
2571
2551
|
} catch (error) {
|
|
2572
2552
|
options?.signal?.throwIfAborted();
|
|
2573
2553
|
throw this.#files.translateError(directory, error);
|
|
@@ -2604,7 +2584,7 @@ function createBrowser(options) {
|
|
|
2604
2584
|
* @param options - The CDP WebSocket debugger URL (and optional timeout)
|
|
2605
2585
|
* @returns A {@link CDPTransportInterface}
|
|
2606
2586
|
*/
|
|
2607
|
-
function
|
|
2587
|
+
function createWebSocketCDPTransport(options) {
|
|
2608
2588
|
return new WebSocketCDPTransport(options);
|
|
2609
2589
|
}
|
|
2610
2590
|
/**
|
|
@@ -2613,7 +2593,7 @@ function createCDPTransport(options) {
|
|
|
2613
2593
|
*
|
|
2614
2594
|
* @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
|
|
2615
2595
|
*/
|
|
2616
|
-
function
|
|
2596
|
+
function createFileBrowserWriter() {
|
|
2617
2597
|
return new FileBrowserWriter();
|
|
2618
2598
|
}
|
|
2619
2599
|
/**
|
|
@@ -2647,7 +2627,7 @@ function createFileBrowserRunStore(options) {
|
|
|
2647
2627
|
* ```ts
|
|
2648
2628
|
* import { createBrowserMCPServer } from '@orkestrel/browser/server'
|
|
2649
2629
|
*
|
|
2650
|
-
* const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
|
|
2630
|
+
* const server = createBrowserMCPServer({ root: 'tmp/browsers', browser: { headless: true } })
|
|
2651
2631
|
* await server.start() // resolves after leasing a connected browser
|
|
2652
2632
|
* await server.destroy()
|
|
2653
2633
|
* ```
|
|
@@ -2702,7 +2682,7 @@ var Browser = class {
|
|
|
2702
2682
|
...options?.error !== void 0 ? { error: options.error } : {}
|
|
2703
2683
|
});
|
|
2704
2684
|
this.#options = options ?? {};
|
|
2705
|
-
this.#engine = this.#options.
|
|
2685
|
+
this.#engine = this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : this.#options.browsers?.engine ?? "chromium";
|
|
2706
2686
|
this.#cdpPort = this.#options.cdp?.port ?? 9222;
|
|
2707
2687
|
this.#cdpHost = this.#options.cdp?.host ?? "127.0.0.1";
|
|
2708
2688
|
queueMicrotask(() => this.#emitter.emit("idle"));
|
|
@@ -2729,9 +2709,9 @@ var Browser = class {
|
|
|
2729
2709
|
return this.#endpoint;
|
|
2730
2710
|
}
|
|
2731
2711
|
async ping(options) {
|
|
2732
|
-
if (this.#destroyed) throw new
|
|
2712
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2733
2713
|
const client = this.#client;
|
|
2734
|
-
if (this.#status !== "connected" || client === void 0) throw new
|
|
2714
|
+
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2735
2715
|
await client.send("Browser.getVersion", void 0, options);
|
|
2736
2716
|
}
|
|
2737
2717
|
async discover() {
|
|
@@ -2740,7 +2720,7 @@ var Browser = class {
|
|
|
2740
2720
|
return result;
|
|
2741
2721
|
}
|
|
2742
2722
|
async connect() {
|
|
2743
|
-
if (this.#destroyed) throw new
|
|
2723
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2744
2724
|
const active = this.#connecting.pending;
|
|
2745
2725
|
if (active !== void 0) {
|
|
2746
2726
|
await active;
|
|
@@ -2750,8 +2730,8 @@ var Browser = class {
|
|
|
2750
2730
|
await this.#connecting.execute(() => this.#establish());
|
|
2751
2731
|
}
|
|
2752
2732
|
adopt() {
|
|
2753
|
-
if (this.#destroyed) throw new
|
|
2754
|
-
if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new
|
|
2733
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2734
|
+
if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2755
2735
|
this.#owned = true;
|
|
2756
2736
|
}
|
|
2757
2737
|
async disconnect() {
|
|
@@ -2782,12 +2762,15 @@ var Browser = class {
|
|
|
2782
2762
|
}
|
|
2783
2763
|
}
|
|
2784
2764
|
async create(options) {
|
|
2785
|
-
if (this.#destroyed) throw new
|
|
2765
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2786
2766
|
const client = this.#client;
|
|
2787
|
-
if (this.#status !== "connected" || client === void 0) throw new
|
|
2767
|
+
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2788
2768
|
let context = this.#contexts[0];
|
|
2789
2769
|
if (context === void 0) {
|
|
2790
|
-
context = new _src_core.BrowserContext(client,
|
|
2770
|
+
context = new _src_core.BrowserContext(client, {
|
|
2771
|
+
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
2772
|
+
writer: createFileBrowserWriter()
|
|
2773
|
+
});
|
|
2791
2774
|
this.#registerContext(context);
|
|
2792
2775
|
}
|
|
2793
2776
|
const page = await context.create(options);
|
|
@@ -2815,10 +2798,11 @@ var Browser = class {
|
|
|
2815
2798
|
return shutdown;
|
|
2816
2799
|
}
|
|
2817
2800
|
async #isolate(options) {
|
|
2818
|
-
if (this.#destroyed) throw new
|
|
2801
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2819
2802
|
const client = this.#client;
|
|
2820
|
-
if (this.#status !== "connected" || client === void 0) throw new
|
|
2803
|
+
if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
|
|
2821
2804
|
(0, _src_core.validateBrowserContextOptions)(options);
|
|
2805
|
+
if (options !== void 0 && "id" in options && options.id !== void 0) throw new _src_core.BrowserError("ARGUMENT", "An isolated context receives its id from the browser");
|
|
2822
2806
|
const params = { disposeOnDetach: false };
|
|
2823
2807
|
if (options?.proxy !== void 0) {
|
|
2824
2808
|
params["proxyServer"] = options.proxy.server;
|
|
@@ -2826,18 +2810,25 @@ var Browser = class {
|
|
|
2826
2810
|
}
|
|
2827
2811
|
if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
|
|
2828
2812
|
const result = await client.send("Target.createBrowserContext", params);
|
|
2829
|
-
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.
|
|
2813
|
+
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserError("CONNECTION", "Failed to create isolated browser context");
|
|
2830
2814
|
const id = result["browserContextId"];
|
|
2831
|
-
const
|
|
2815
|
+
const { proxy: _proxy, origins: _origins, ...defaults } = options ?? {};
|
|
2816
|
+
const viewport = options?.viewport ?? options?.emulation?.viewport ?? this.#options.viewport;
|
|
2817
|
+
const context = new _src_core.BrowserContext(client, {
|
|
2818
|
+
...defaults,
|
|
2819
|
+
id,
|
|
2820
|
+
...viewport === void 0 ? {} : { viewport },
|
|
2821
|
+
writer: options?.writer ?? createFileBrowserWriter()
|
|
2822
|
+
});
|
|
2832
2823
|
try {
|
|
2833
|
-
if (this.#destroyed || this.#client !== client) throw new
|
|
2824
|
+
if (this.#destroyed || this.#client !== client) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2834
2825
|
if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
|
|
2835
2826
|
behavior: options.downloads.named === true ? "allowAndName" : "allow",
|
|
2836
2827
|
browserContextId: id,
|
|
2837
2828
|
downloadPath: options.downloads.path,
|
|
2838
2829
|
eventsEnabled: true
|
|
2839
2830
|
});
|
|
2840
|
-
if (this.#destroyed || this.#client !== client) throw new
|
|
2831
|
+
if (this.#destroyed || this.#client !== client) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2841
2832
|
} catch (error) {
|
|
2842
2833
|
await context.close().catch(() => void 0);
|
|
2843
2834
|
throw error;
|
|
@@ -2849,7 +2840,7 @@ var Browser = class {
|
|
|
2849
2840
|
const disconnecting = this.#disconnecting.pending;
|
|
2850
2841
|
if (disconnecting !== void 0) await disconnecting;
|
|
2851
2842
|
await this.#settleExit();
|
|
2852
|
-
if (this.#destroyed) throw new
|
|
2843
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2853
2844
|
if (this.#status === "connected") return;
|
|
2854
2845
|
this.#assertNotAborted();
|
|
2855
2846
|
this.#status = "connecting";
|
|
@@ -2874,12 +2865,12 @@ var Browser = class {
|
|
|
2874
2865
|
this.#assertNotAborted();
|
|
2875
2866
|
await this.#launch();
|
|
2876
2867
|
} catch (error) {
|
|
2877
|
-
if (this.#destroyed) throw new
|
|
2868
|
+
if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
|
|
2878
2869
|
this.#status = "error";
|
|
2879
2870
|
this.#emitter.emit("error", error);
|
|
2880
|
-
if ((0, _src_core.
|
|
2871
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
|
|
2881
2872
|
const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
|
|
2882
|
-
throw new _src_core.
|
|
2873
|
+
throw new _src_core.BrowserError("CONNECTION", message, this.#options.executable === void 0 ? void 0 : { executable: this.#options.executable });
|
|
2883
2874
|
}
|
|
2884
2875
|
}
|
|
2885
2876
|
async #detach() {
|
|
@@ -2900,7 +2891,7 @@ var Browser = class {
|
|
|
2900
2891
|
this.#emitter.emit("idle");
|
|
2901
2892
|
}
|
|
2902
2893
|
#assertNotAborted() {
|
|
2903
|
-
if (this.#signal().aborted) throw new _src_core.
|
|
2894
|
+
if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
|
|
2904
2895
|
}
|
|
2905
2896
|
#signal() {
|
|
2906
2897
|
const external = this.#options.signal;
|
|
@@ -2908,10 +2899,10 @@ var Browser = class {
|
|
|
2908
2899
|
}
|
|
2909
2900
|
async #raceAbort(promise) {
|
|
2910
2901
|
const signal = this.#signal();
|
|
2911
|
-
if (signal.aborted) throw new _src_core.
|
|
2902
|
+
if (signal.aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
|
|
2912
2903
|
const aborted = Promise.withResolvers();
|
|
2913
2904
|
const listener = (0, node_events.addAbortListener)(signal, () => {
|
|
2914
|
-
aborted.reject(new _src_core.
|
|
2905
|
+
aborted.reject(new _src_core.BrowserError("CONNECTION", "Connection aborted"));
|
|
2915
2906
|
});
|
|
2916
2907
|
try {
|
|
2917
2908
|
return await Promise.race([promise, aborted.promise]);
|
|
@@ -2939,7 +2930,7 @@ var Browser = class {
|
|
|
2939
2930
|
this.#owned = void 0;
|
|
2940
2931
|
this.#endpoint = void 0;
|
|
2941
2932
|
if (connected) this.#status = "disconnected";
|
|
2942
|
-
this.#emitter.emit("error", new _src_core.
|
|
2933
|
+
this.#emitter.emit("error", new _src_core.BrowserError("CONNECTION", "The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
2943
2934
|
if (connected) {
|
|
2944
2935
|
this.#emitter.emit("disconnect");
|
|
2945
2936
|
this.#emitter.emit("idle");
|
|
@@ -2977,7 +2968,7 @@ var Browser = class {
|
|
|
2977
2968
|
}
|
|
2978
2969
|
this.#destroyContextList(contexts);
|
|
2979
2970
|
this.#closeClient(client);
|
|
2980
|
-
this.#emitter.emit("error", new _src_core.
|
|
2971
|
+
this.#emitter.emit("error", new _src_core.BrowserError("CONNECTION", "The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
|
|
2981
2972
|
this.#emitter.emit("disconnect");
|
|
2982
2973
|
this.#emitter.emit("idle");
|
|
2983
2974
|
}
|
|
@@ -3030,7 +3021,7 @@ var Browser = class {
|
|
|
3030
3021
|
};
|
|
3031
3022
|
}
|
|
3032
3023
|
async #assertPortFree() {
|
|
3033
|
-
if (await this.#probePort()) throw new _src_core.
|
|
3024
|
+
if (await this.#probePort()) throw new _src_core.BrowserError("CONNECTION", `Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
|
|
3034
3025
|
port: this.#cdpPort,
|
|
3035
3026
|
host: this.#cdpHost
|
|
3036
3027
|
});
|
|
@@ -3044,7 +3035,7 @@ var Browser = class {
|
|
|
3044
3035
|
}
|
|
3045
3036
|
}
|
|
3046
3037
|
async #connectCDP(endpoint) {
|
|
3047
|
-
const transport =
|
|
3038
|
+
const transport = createWebSocketCDPTransport({
|
|
3048
3039
|
url: endpoint,
|
|
3049
3040
|
timeout: this.#timeout()
|
|
3050
3041
|
});
|
|
@@ -3077,20 +3068,20 @@ var Browser = class {
|
|
|
3077
3068
|
}
|
|
3078
3069
|
}
|
|
3079
3070
|
async #launch() {
|
|
3080
|
-
if (this.#process !== void 0) throw new _src_core.
|
|
3081
|
-
const requestedEngine = this.#options.
|
|
3071
|
+
if (this.#process !== void 0) throw new _src_core.BrowserError("CONNECTION", "A browser process is already active on this instance");
|
|
3072
|
+
const requestedEngine = this.#options.browsers?.engine;
|
|
3082
3073
|
let executable = this.#options.executable;
|
|
3083
3074
|
let resolvedEngine;
|
|
3084
3075
|
if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
|
|
3085
3076
|
else {
|
|
3086
|
-
const found =
|
|
3077
|
+
const found = findSystemBrowsers({
|
|
3087
3078
|
...this.#options.browsers,
|
|
3088
3079
|
...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
|
|
3089
|
-
});
|
|
3080
|
+
})[0];
|
|
3090
3081
|
executable = found?.executable;
|
|
3091
3082
|
resolvedEngine = found?.engine;
|
|
3092
3083
|
}
|
|
3093
|
-
if (executable === void 0) throw new _src_core.
|
|
3084
|
+
if (executable === void 0) throw new _src_core.BrowserError("CONNECTION", "No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
3094
3085
|
this.#engine = resolvedEngine ?? "chromium";
|
|
3095
3086
|
await this.#releaseProfile();
|
|
3096
3087
|
const profile = await createBrowserProfile(this.#options.profile);
|
|
@@ -3106,7 +3097,7 @@ var Browser = class {
|
|
|
3106
3097
|
let client;
|
|
3107
3098
|
try {
|
|
3108
3099
|
const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
|
|
3109
|
-
const transport =
|
|
3100
|
+
const transport = createWebSocketCDPTransport({
|
|
3110
3101
|
url: endpoint,
|
|
3111
3102
|
timeout: this.#timeout()
|
|
3112
3103
|
});
|
|
@@ -3170,9 +3161,9 @@ var Browser = class {
|
|
|
3170
3161
|
}
|
|
3171
3162
|
if (pid === void 0) {
|
|
3172
3163
|
await this.#closeRemote(client);
|
|
3173
|
-
throw new _src_core.
|
|
3174
|
-
executable: this.#options.executable,
|
|
3175
|
-
pid: process.pid
|
|
3164
|
+
throw new _src_core.BrowserError("CONNECTION", "The browser launcher exited without naming the process serving its CDP endpoint", {
|
|
3165
|
+
...this.#options.executable === void 0 ? {} : { executable: this.#options.executable },
|
|
3166
|
+
...process.pid === void 0 ? {} : { pid: process.pid }
|
|
3176
3167
|
});
|
|
3177
3168
|
}
|
|
3178
3169
|
this.#servingPid = pid;
|
|
@@ -3180,7 +3171,7 @@ var Browser = class {
|
|
|
3180
3171
|
async #waitForLaunch(process, executable, args) {
|
|
3181
3172
|
const context = {
|
|
3182
3173
|
executable,
|
|
3183
|
-
args
|
|
3174
|
+
...args === void 0 ? {} : { args }
|
|
3184
3175
|
};
|
|
3185
3176
|
const timeout = this.#timeout();
|
|
3186
3177
|
const controller = new AbortController();
|
|
@@ -3191,32 +3182,32 @@ var Browser = class {
|
|
|
3191
3182
|
deadline
|
|
3192
3183
|
]);
|
|
3193
3184
|
const stderr = process.stderr;
|
|
3194
|
-
if (stderr === null) throw new _src_core.
|
|
3185
|
+
if (stderr === null) throw new _src_core.BrowserError("CONNECTION", "The browser process has no standard error to read", context);
|
|
3195
3186
|
const exit = (0, node_events.once)(process, "exit", { signal });
|
|
3196
3187
|
exit.catch(() => void 0);
|
|
3197
3188
|
const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
|
|
3198
3189
|
if (signal.aborted) throw error;
|
|
3199
3190
|
const values = await exit;
|
|
3200
3191
|
if (values[0] === 0 && values[1] === null) throw error;
|
|
3201
|
-
throw new _src_core.
|
|
3192
|
+
throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit((0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null, (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null), context);
|
|
3202
3193
|
});
|
|
3203
3194
|
const exited = exit.then((values) => {
|
|
3204
3195
|
const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
|
|
3205
3196
|
const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
|
|
3206
3197
|
if (code === 0 && exitSignal === null) return ready;
|
|
3207
|
-
throw new _src_core.
|
|
3198
|
+
throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit(code, exitSignal), context);
|
|
3208
3199
|
});
|
|
3209
3200
|
try {
|
|
3210
3201
|
return await Promise.race([ready, exited]);
|
|
3211
3202
|
} catch (error) {
|
|
3212
|
-
if ((0, _src_core.
|
|
3213
|
-
if (this.#signal().aborted) throw new _src_core.
|
|
3214
|
-
if (deadline.aborted) throw new _src_core.
|
|
3203
|
+
if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
|
|
3204
|
+
if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted", context);
|
|
3205
|
+
if (deadline.aborted) throw new _src_core.BrowserError("CONNECTION", `Browser did not report a CDP endpoint within ${timeout}ms`, {
|
|
3215
3206
|
...context,
|
|
3216
3207
|
timeout
|
|
3217
3208
|
});
|
|
3218
3209
|
const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
|
|
3219
|
-
throw new _src_core.
|
|
3210
|
+
throw new _src_core.BrowserError("CONNECTION", message, context);
|
|
3220
3211
|
} finally {
|
|
3221
3212
|
controller.abort();
|
|
3222
3213
|
ready.catch(() => void 0);
|
|
@@ -3253,7 +3244,10 @@ var Browser = class {
|
|
|
3253
3244
|
});
|
|
3254
3245
|
}
|
|
3255
3246
|
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
3256
|
-
const context = new _src_core.BrowserContext(client,
|
|
3247
|
+
const context = new _src_core.BrowserContext(client, {
|
|
3248
|
+
...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
|
|
3249
|
+
writer: createFileBrowserWriter()
|
|
3250
|
+
});
|
|
3257
3251
|
await context.sync(pages);
|
|
3258
3252
|
this.#registerContext(context);
|
|
3259
3253
|
}
|
|
@@ -3333,7 +3327,7 @@ var Browser = class {
|
|
|
3333
3327
|
let remote = client;
|
|
3334
3328
|
let temporary = false;
|
|
3335
3329
|
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
3336
|
-
const transport =
|
|
3330
|
+
const transport = createWebSocketCDPTransport({
|
|
3337
3331
|
url: this.#endpoint,
|
|
3338
3332
|
timeout: this.#timeout()
|
|
3339
3333
|
});
|
|
@@ -3436,10 +3430,11 @@ var Browser = class {
|
|
|
3436
3430
|
const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
|
|
3437
3431
|
if (code === "ESRCH") return false;
|
|
3438
3432
|
if (signal === 0) return code === "EPERM" ? true : void 0;
|
|
3439
|
-
|
|
3440
|
-
|
|
3433
|
+
const target = serving ?? pid;
|
|
3434
|
+
throw new _src_core.BrowserError("CONNECTION", "Failed to signal the browser process", {
|
|
3435
|
+
...target === void 0 ? {} : { pid: target },
|
|
3441
3436
|
signal,
|
|
3442
|
-
cause: error
|
|
3437
|
+
cause: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
|
|
3443
3438
|
});
|
|
3444
3439
|
}
|
|
3445
3440
|
}
|
|
@@ -3478,7 +3473,8 @@ var Browser = class {
|
|
|
3478
3473
|
if (await this.#waitForTerminationWithin(process, 3e3)) return;
|
|
3479
3474
|
if (this.#signalProcess(process, "SIGKILL") === false) return;
|
|
3480
3475
|
if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
|
|
3481
|
-
|
|
3476
|
+
const target = this.#servingPid ?? process.pid;
|
|
3477
|
+
throw new _src_core.BrowserError("CONNECTION", "Browser process did not exit after SIGKILL", { ...target === void 0 ? {} : { pid: target } });
|
|
3482
3478
|
} finally {
|
|
3483
3479
|
await this.#closeProcessPipe(process);
|
|
3484
3480
|
}
|
|
@@ -3558,9 +3554,7 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
|
|
|
3558
3554
|
exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
|
|
3559
3555
|
exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
|
|
3560
3556
|
exports.Browser = Browser;
|
|
3561
|
-
exports.BrowserDestroyedError = BrowserDestroyedError;
|
|
3562
3557
|
exports.BrowserMCPServer = BrowserMCPServer;
|
|
3563
|
-
exports.BrowserNotConnectedError = BrowserNotConnectedError;
|
|
3564
3558
|
exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
|
|
3565
3559
|
exports.FileBrowserRunStore = FileBrowserRunStore;
|
|
3566
3560
|
exports.FileBrowserWriter = FileBrowserWriter;
|
|
@@ -3572,20 +3566,17 @@ exports.buildWindowsRoots = buildWindowsRoots;
|
|
|
3572
3566
|
exports.createBrowser = createBrowser;
|
|
3573
3567
|
exports.createBrowserMCPServer = createBrowserMCPServer;
|
|
3574
3568
|
exports.createBrowserProfile = createBrowserProfile;
|
|
3575
|
-
exports.createBrowserWriter = createBrowserWriter;
|
|
3576
|
-
exports.createCDPTransport = createCDPTransport;
|
|
3577
3569
|
exports.createFileBrowserJourneyStore = createFileBrowserJourneyStore;
|
|
3578
3570
|
exports.createFileBrowserRunStore = createFileBrowserRunStore;
|
|
3571
|
+
exports.createFileBrowserWriter = createFileBrowserWriter;
|
|
3572
|
+
exports.createWebSocketCDPTransport = createWebSocketCDPTransport;
|
|
3579
3573
|
exports.describeBrowserServerLoss = describeBrowserServerLoss;
|
|
3580
3574
|
exports.fetchCDPTargets = fetchCDPTargets;
|
|
3581
3575
|
exports.findEnvOverrides = findEnvOverrides;
|
|
3582
3576
|
exports.findInstallPaths = findInstallPaths;
|
|
3583
3577
|
exports.findStorePaths = findStorePaths;
|
|
3584
|
-
exports.findSystemBrowser = findSystemBrowser;
|
|
3585
3578
|
exports.findSystemBrowsers = findSystemBrowsers;
|
|
3586
3579
|
exports.formatBrowserLockEntry = formatBrowserLockEntry;
|
|
3587
|
-
exports.isBrowserDestroyedError = isBrowserDestroyedError;
|
|
3588
|
-
exports.isBrowserNotConnectedError = isBrowserNotConnectedError;
|
|
3589
3580
|
exports.launchBrowserProcess = launchBrowserProcess;
|
|
3590
3581
|
exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
3591
3582
|
exports.parseBrowserEngine = parseBrowserEngine;
|