@orkestrel/browser 0.0.5 → 0.0.6

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.
@@ -1,17 +1,18 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _src_core = require("../core/index.cjs");
3
+ let _orkestrel_contract = require("@orkestrel/contract");
3
4
  let node_fs = require("node:fs");
5
+ let node_fs_promises = require("node:fs/promises");
6
+ let node_os = require("node:os");
4
7
  let node_path = require("node:path");
5
8
  let node_child_process = require("node:child_process");
6
9
  let node_timers_promises = require("node:timers/promises");
7
- let _orkestrel_contract = require("@orkestrel/contract");
8
10
  let node_crypto = require("node:crypto");
9
11
  let node_http = require("node:http");
10
12
  let node_https = require("node:https");
11
13
  let _orkestrel_emitter = require("@orkestrel/emitter");
12
14
  let _orkestrel_websocket = require("@orkestrel/websocket");
13
15
  let node_events = require("node:events");
14
- let node_fs_promises = require("node:fs/promises");
15
16
  //#region src/server/constants.ts
16
17
  /** Default CDP port probed for an existing browser and used for launches. */
17
18
  var BROWSER_DEFAULT_CDP_PORT = 9222;
@@ -27,6 +28,8 @@ var BROWSER_CDP_LIST_PATH = "/json/list";
27
28
  var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
28
29
  /** Flag enabling headless mode on a launched browser process. */
29
30
  var BROWSER_HEADLESS_ARG = "--headless=new";
31
+ /** Prefix for isolated browser profiles created beneath the operating-system temp directory. */
32
+ var BROWSER_PROFILE_PREFIX = "orkestrel-browser-";
30
33
  /** Grace period after SIGTERM before a launched process is escalated to SIGKILL during teardown. */
31
34
  var BROWSER_KILL_GRACE_MS = 3e3;
32
35
  /** Bound for the `discover: false` port-occupancy probe before launching — short, since it only needs to detect an already-listening CDP endpoint, not perform full discovery. */
@@ -158,7 +161,7 @@ var BrowserDestroyedError = class extends _src_core.BrowserError {
158
161
  * @returns True when value is a BrowserConnectionError instance
159
162
  */
160
163
  function isBrowserConnectionError(value) {
161
- return value instanceof BrowserConnectionError;
164
+ return (0, _orkestrel_contract.isInstance)(value, BrowserConnectionError);
162
165
  }
163
166
  /**
164
167
  * Narrow an unknown value to BrowserNotConnectedError.
@@ -167,7 +170,7 @@ function isBrowserConnectionError(value) {
167
170
  * @returns True when value is a BrowserNotConnectedError instance
168
171
  */
169
172
  function isBrowserNotConnectedError(value) {
170
- return value instanceof BrowserNotConnectedError;
173
+ return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
171
174
  }
172
175
  /**
173
176
  * Narrow an unknown value to BrowserDestroyedError.
@@ -176,7 +179,7 @@ function isBrowserNotConnectedError(value) {
176
179
  * @returns True when value is a BrowserDestroyedError instance
177
180
  */
178
181
  function isBrowserDestroyedError(value) {
179
- return value instanceof BrowserDestroyedError;
182
+ return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
180
183
  }
181
184
  //#endregion
182
185
  //#region src/server/helpers.ts
@@ -264,6 +267,44 @@ function browserToEngine(browser) {
264
267
  if (browser?.includes("Chrome/") === true) return "chrome";
265
268
  return "chromium";
266
269
  }
270
+ /**
271
+ * Resolve a persistent caller profile or create an isolated temporary one.
272
+ *
273
+ * @param path - Optional caller-owned persistent user-data directory
274
+ * @returns The resolved profile and whether the library owns its lifecycle
275
+ */
276
+ async function createBrowserProfile(path) {
277
+ if (path !== void 0) return {
278
+ path,
279
+ temporary: false
280
+ };
281
+ return {
282
+ path: await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), BROWSER_PROFILE_PREFIX)),
283
+ temporary: true
284
+ };
285
+ }
286
+ /**
287
+ * Remove a library-owned isolated browser profile.
288
+ *
289
+ * @remarks
290
+ * The path must remain a direct child of the operating-system temp directory
291
+ * with {@link BROWSER_PROFILE_PREFIX}; this defensive check prevents a
292
+ * malformed profile value from widening recursive deletion.
293
+ *
294
+ * @param profile - Resolved profile lifecycle result
295
+ */
296
+ async function removeBrowserProfile(profile) {
297
+ if (!profile.temporary) return;
298
+ const path = (0, node_path.resolve)(profile.path);
299
+ const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
300
+ if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new Error("Refusing to remove an unsafe browser profile path");
301
+ await (0, node_fs_promises.rm)(path, {
302
+ recursive: true,
303
+ force: true,
304
+ maxRetries: 3,
305
+ retryDelay: 100
306
+ });
307
+ }
267
308
  /** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order for an existing file. */
268
309
  function findEnvOverride(env) {
269
310
  return findAllEnvOverrides(env)[0];
@@ -416,7 +457,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
416
457
  const response = await fetch(url, { signal: controller.signal });
417
458
  if (!response.ok) return [];
418
459
  const list = await response.json();
419
- if (!Array.isArray(list)) return [];
460
+ if (!(0, _orkestrel_contract.isArray)(list)) return [];
420
461
  const targets = [];
421
462
  for (const entry of list) {
422
463
  if (!(0, _orkestrel_contract.isRecord)(entry)) continue;
@@ -480,12 +521,12 @@ var WebSocketCDPTransport = class {
480
521
  }
481
522
  this.#socket?.destroy();
482
523
  this.#socket = void 0;
483
- const attempt = this.#open();
484
- this.#starting = attempt;
524
+ const transition = this.#open();
525
+ this.#starting = transition;
485
526
  try {
486
- await attempt;
527
+ await transition;
487
528
  } finally {
488
- if (this.#starting === attempt) this.#starting = void 0;
529
+ if (this.#starting === transition) this.#starting = void 0;
489
530
  }
490
531
  }
491
532
  async send(data) {
@@ -499,16 +540,21 @@ var WebSocketCDPTransport = class {
499
540
  await active;
500
541
  return;
501
542
  }
502
- const attempt = this.#stop();
503
- this.#closing = attempt;
543
+ const transition = this.#stop();
544
+ this.#closing = transition;
504
545
  try {
505
- await attempt;
546
+ await transition;
506
547
  } finally {
507
- if (this.#closing === attempt) this.#closing = void 0;
548
+ if (this.#closing === transition) this.#closing = void 0;
508
549
  }
509
550
  }
510
551
  async #open() {
511
- const url = new URL(this.#url);
552
+ const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
553
+ if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
554
+ url: this.#url,
555
+ error: parsed.error
556
+ });
557
+ const url = parsed.value;
512
558
  if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
513
559
  const key = (0, node_crypto.randomBytes)(16).toString("base64");
514
560
  const deferred = Promise.withResolvers();
@@ -670,6 +716,9 @@ function createScreenshotWriter() {
670
716
  }
671
717
  //#endregion
672
718
  //#region src/server/Browser.ts
719
+ /**
720
+ * Discovers, launches, connects to, and owns Chromium-family browser sessions.
721
+ */
673
722
  var Browser = class {
674
723
  #emitter;
675
724
  #options;
@@ -683,6 +732,7 @@ var Browser = class {
683
732
  #endpoint;
684
733
  #client;
685
734
  #process;
735
+ #profile;
686
736
  #contexts = [];
687
737
  #destroyed = false;
688
738
  #connecting;
@@ -780,6 +830,35 @@ var Browser = class {
780
830
  contexts() {
781
831
  return [...this.#contexts];
782
832
  }
833
+ async isolate(options) {
834
+ if (this.#destroyed) throw new BrowserDestroyedError();
835
+ const client = this.#client;
836
+ if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
837
+ (0, _src_core.validateBrowserContextOptions)(options);
838
+ const params = { disposeOnDetach: false };
839
+ if (options?.proxy !== void 0) {
840
+ params["proxyServer"] = options.proxy.server;
841
+ if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
842
+ }
843
+ if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
844
+ const result = await client.send("Target.createBrowserContext", params);
845
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
846
+ const id = result["browserContextId"];
847
+ const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createScreenshotWriter(), options?.emulation, options?.downloads);
848
+ try {
849
+ if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
850
+ behavior: options.downloads.named === true ? "allowAndName" : "allow",
851
+ browserContextId: id,
852
+ downloadPath: options.downloads.path,
853
+ eventsEnabled: true
854
+ });
855
+ } catch (error) {
856
+ await context.close().catch(() => void 0);
857
+ throw error;
858
+ }
859
+ this.#registerContext(context);
860
+ return context;
861
+ }
783
862
  async create(options) {
784
863
  if (this.#destroyed) throw new BrowserDestroyedError();
785
864
  const client = this.#client;
@@ -787,7 +866,7 @@ var Browser = class {
787
866
  let context = this.#contexts[0];
788
867
  if (context === void 0) {
789
868
  context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
790
- this.#contexts.push(context);
869
+ this.#registerContext(context);
791
870
  }
792
871
  const page = await context.create(options);
793
872
  this.#emitter.emit("page", page);
@@ -840,8 +919,8 @@ var Browser = class {
840
919
  if (this.#destroyed) throw new BrowserDestroyedError();
841
920
  this.#status = "error";
842
921
  this.#emitter.emit("error", error);
843
- if (error instanceof BrowserConnectionError) throw error;
844
- throw new BrowserConnectionError(error instanceof Error ? error.message : String(error), { executable: this.#options.executable });
922
+ if (isBrowserConnectionError(error)) throw error;
923
+ throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), { executable: this.#options.executable });
845
924
  }
846
925
  }
847
926
  async #detach() {
@@ -890,6 +969,7 @@ var Browser = class {
890
969
  this.#unbindTransport();
891
970
  this.#unbindProcess();
892
971
  this.#process = void 0;
972
+ this.#releaseProfile().catch((error) => this.#emitter.emit("error", error));
893
973
  this.#client = void 0;
894
974
  this.#contexts = [];
895
975
  this.#connection = void 0;
@@ -1064,8 +1144,16 @@ var Browser = class {
1064
1144
  }
1065
1145
  if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
1066
1146
  this.#engine = resolvedEngine ?? "chromium";
1067
- const profile = this.#options.profile;
1068
- const process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile, this.#options.args);
1147
+ await this.#releaseProfile();
1148
+ const profile = await createBrowserProfile(this.#options.profile);
1149
+ this.#profile = profile;
1150
+ let process;
1151
+ try {
1152
+ process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile.path, this.#options.args);
1153
+ } catch (error) {
1154
+ await this.#releaseProfile();
1155
+ throw error;
1156
+ }
1069
1157
  this.#process = process;
1070
1158
  let client;
1071
1159
  try {
@@ -1079,7 +1167,7 @@ var Browser = class {
1079
1167
  timeout: this.#timeout()
1080
1168
  });
1081
1169
  await this.#raceAbort(client.connect());
1082
- const connection = profile === void 0 ? "launch" : "persistent";
1170
+ const connection = this.#options.profile === void 0 ? "launch" : "persistent";
1083
1171
  this.#client = client;
1084
1172
  this.#endpoint = endpoint;
1085
1173
  this.#connection = connection;
@@ -1095,6 +1183,7 @@ var Browser = class {
1095
1183
  this.#unbindProcess();
1096
1184
  await this.#closeClient(client);
1097
1185
  await this.#terminate(process);
1186
+ await this.#releaseProfile();
1098
1187
  this.#process = void 0;
1099
1188
  this.#client = void 0;
1100
1189
  this.#endpoint = void 0;
@@ -1111,16 +1200,16 @@ var Browser = class {
1111
1200
  const signal = AbortSignal.any([controller.signal, this.#signal()]);
1112
1201
  const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
1113
1202
  const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
1114
- const code = typeof values[0] === "number" ? values[0] : null;
1115
- const exitSignal = typeof values[1] === "string" ? values[1] : null;
1203
+ const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
1204
+ const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
1116
1205
  throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
1117
1206
  });
1118
1207
  try {
1119
1208
  return await Promise.race([ready, exited]);
1120
1209
  } catch (error) {
1121
- if (error instanceof BrowserConnectionError) throw error;
1210
+ if (isBrowserConnectionError(error)) throw error;
1122
1211
  if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
1123
- throw new BrowserConnectionError(error instanceof Error ? error.message : String(error), context);
1212
+ throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), context);
1124
1213
  } finally {
1125
1214
  controller.abort();
1126
1215
  ready.catch(() => void 0);
@@ -1145,7 +1234,7 @@ var Browser = class {
1145
1234
  } catch {
1146
1235
  return;
1147
1236
  }
1148
- if (!(0, _orkestrel_contract.isRecord)(result) || !Array.isArray(result["targetInfos"])) return;
1237
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["targetInfos"])) return;
1149
1238
  const pages = [];
1150
1239
  for (const target of result["targetInfos"]) {
1151
1240
  if (!(0, _orkestrel_contract.isRecord)(target) || !(0, _orkestrel_contract.isString)(target["targetId"]) || target["type"] !== "page" || !(0, _orkestrel_contract.isString)(target["title"]) || !(0, _orkestrel_contract.isString)(target["url"])) continue;
@@ -1159,7 +1248,7 @@ var Browser = class {
1159
1248
  if (pages.length === 0 || this.#contexts.length > 0) return;
1160
1249
  const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
1161
1250
  await context.sync(pages);
1162
- this.#contexts.push(context);
1251
+ this.#registerContext(context);
1163
1252
  }
1164
1253
  async #destroyResources() {
1165
1254
  try {
@@ -1177,7 +1266,11 @@ var Browser = class {
1177
1266
  await this.#closeClient(client);
1178
1267
  this.#client = void 0;
1179
1268
  } finally {
1180
- this.#finish();
1269
+ try {
1270
+ await this.#releaseProfile();
1271
+ } finally {
1272
+ this.#finish();
1273
+ }
1181
1274
  }
1182
1275
  }
1183
1276
  async #closeResources() {
@@ -1197,7 +1290,11 @@ var Browser = class {
1197
1290
  await this.#closeClient(client);
1198
1291
  this.#client = void 0;
1199
1292
  } finally {
1200
- this.#finish();
1293
+ try {
1294
+ await this.#releaseProfile();
1295
+ } finally {
1296
+ this.#finish();
1297
+ }
1201
1298
  }
1202
1299
  }
1203
1300
  async #settle() {
@@ -1229,12 +1326,20 @@ var Browser = class {
1229
1326
  if (temporary) await this.#closeClient(remote);
1230
1327
  }
1231
1328
  async #closeContexts() {
1232
- for (const context of this.#contexts) try {
1329
+ for (const context of [...this.#contexts]) try {
1233
1330
  await context.close();
1234
1331
  } catch {}
1235
1332
  }
1236
1333
  async #destroyContexts() {
1237
- await this.#destroyContextList(this.#contexts);
1334
+ await this.#destroyContextList([...this.#contexts]);
1335
+ }
1336
+ #registerContext(context) {
1337
+ this.#contexts.push(context);
1338
+ context.emitter.on("close", () => {
1339
+ const index = this.#contexts.indexOf(context);
1340
+ if (index >= 0) this.#contexts.splice(index, 1);
1341
+ });
1342
+ this.#emitter.emit("context", context);
1238
1343
  }
1239
1344
  async #destroyContextList(contexts) {
1240
1345
  for (const context of contexts) try {
@@ -1247,6 +1352,11 @@ var Browser = class {
1247
1352
  await client.close();
1248
1353
  } catch {}
1249
1354
  }
1355
+ async #releaseProfile() {
1356
+ const profile = this.#profile;
1357
+ this.#profile = void 0;
1358
+ if (profile !== void 0) await removeBrowserProfile(profile);
1359
+ }
1250
1360
  async #waitForProcess(process) {
1251
1361
  if (process.exitCode !== null || process.signalCode !== null) return;
1252
1362
  await (0, node_events.once)(process, "exit");
@@ -1306,6 +1416,7 @@ exports.BROWSER_KILL_GRACE_MS = BROWSER_KILL_GRACE_MS;
1306
1416
  exports.BROWSER_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS;
1307
1417
  exports.BROWSER_PORT_PROBE_TIMEOUT_MS = BROWSER_PORT_PROBE_TIMEOUT_MS;
1308
1418
  exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
1419
+ exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
1309
1420
  exports.BROWSER_STORE_CACHE_DIRS = BROWSER_STORE_CACHE_DIRS;
1310
1421
  exports.BROWSER_STORE_DEFAULT_DIRS = BROWSER_STORE_DEFAULT_DIRS;
1311
1422
  exports.BROWSER_STORE_ENV_KEY = BROWSER_STORE_ENV_KEY;
@@ -1322,6 +1433,7 @@ exports.BrowserNotConnectedError = BrowserNotConnectedError;
1322
1433
  exports.WebSocketCDPTransport = WebSocketCDPTransport;
1323
1434
  exports.browserToEngine = browserToEngine;
1324
1435
  exports.createBrowser = createBrowser;
1436
+ exports.createBrowserProfile = createBrowserProfile;
1325
1437
  exports.createCDPTransport = createCDPTransport;
1326
1438
  exports.createScreenshotWriter = createScreenshotWriter;
1327
1439
  exports.defaultInstallPaths = defaultInstallPaths;
@@ -1343,6 +1455,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
1343
1455
  exports.parseBrowserEngine = parseBrowserEngine;
1344
1456
  exports.probeAllPathNames = probeAllPathNames;
1345
1457
  exports.probePathNames = probePathNames;
1458
+ exports.removeBrowserProfile = removeBrowserProfile;
1346
1459
  exports.waitForCDPReady = waitForCDPReady;
1347
1460
  exports.windowsRoots = windowsRoots;
1348
1461