@hasna/browser 0.4.5 → 0.4.7

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.
@@ -3,7 +3,6 @@ var __create = Object.create;
3
3
  var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
7
  function __accessProp(key) {
9
8
  return this[key];
@@ -30,23 +29,6 @@ var __toESM = (mod, isNodeMode, target) => {
30
29
  cache.set(mod, to);
31
30
  return to;
32
31
  };
33
- var __toCommonJS = (from) => {
34
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
35
- if (entry)
36
- return entry;
37
- entry = __defProp({}, "__esModule", { value: true });
38
- if (from && typeof from === "object" || typeof from === "function") {
39
- for (var key of __getOwnPropNames(from))
40
- if (!__hasOwnProp.call(entry, key))
41
- __defProp(entry, key, {
42
- get: __accessProp.bind(from, key),
43
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
44
- });
45
- }
46
- __moduleCache.set(from, entry);
47
- return entry;
48
- };
49
- var __moduleCache;
50
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
51
33
  var __returnValue = (v) => v;
52
34
  function __exportSetter(name, newValue) {
@@ -10834,6 +10816,7 @@ function watchPage(page, opts) {
10834
10816
  const changes = [];
10835
10817
  const intervalMs = opts?.intervalMs ?? 500;
10836
10818
  const maxChanges = opts?.maxChanges ?? 50;
10819
+ const sessionId = opts?.sessionId;
10837
10820
  const interval = setInterval(async () => {
10838
10821
  if (changes.length >= maxChanges)
10839
10822
  return;
@@ -10847,7 +10830,7 @@ function watchPage(page, opts) {
10847
10830
  }
10848
10831
  } catch {}
10849
10832
  }, intervalMs);
10850
- activeWatches.set(id, { interval, changes });
10833
+ activeWatches.set(id, { interval, changes, sessionId });
10851
10834
  return {
10852
10835
  id,
10853
10836
  stop: () => {
@@ -10866,10 +10849,12 @@ function stopWatch(watchId) {
10866
10849
  activeWatches.delete(watchId);
10867
10850
  }
10868
10851
  }
10869
- function stopAllWatchesForSession(_sessionId) {
10870
- for (const [id, w] of activeWatches) {
10871
- clearInterval(w.interval);
10872
- activeWatches.delete(id);
10852
+ function stopAllWatchesForSession(sessionId) {
10853
+ for (const [id, w] of [...activeWatches]) {
10854
+ if (!sessionId || w.sessionId === sessionId) {
10855
+ clearInterval(w.interval);
10856
+ activeWatches.delete(id);
10857
+ }
10873
10858
  }
10874
10859
  }
10875
10860
  async function clickRef(page, sessionId, ref, opts) {
@@ -14606,7 +14591,7 @@ var require_operation = __commonJS((exports, module) => {
14606
14591
  // node_modules/@img/colour/color.cjs
14607
14592
  var require_color = __commonJS((exports, module) => {
14608
14593
  var __defProp3 = Object.defineProperty;
14609
- var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
14594
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
14610
14595
  var __getOwnPropNames3 = Object.getOwnPropertyNames;
14611
14596
  var __hasOwnProp3 = Object.prototype.hasOwnProperty;
14612
14597
  var __export3 = (target, all) => {
@@ -14617,16 +14602,16 @@ var require_color = __commonJS((exports, module) => {
14617
14602
  if (from && typeof from === "object" || typeof from === "function") {
14618
14603
  for (let key of __getOwnPropNames3(from))
14619
14604
  if (!__hasOwnProp3.call(to, key) && key !== except)
14620
- __defProp3(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
14605
+ __defProp3(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14621
14606
  }
14622
14607
  return to;
14623
14608
  };
14624
- var __toCommonJS2 = (mod) => __copyProps(__defProp3({}, "__esModule", { value: true }), mod);
14609
+ var __toCommonJS = (mod) => __copyProps(__defProp3({}, "__esModule", { value: true }), mod);
14625
14610
  var index_exports = {};
14626
14611
  __export3(index_exports, {
14627
14612
  default: () => index_default
14628
14613
  });
14629
- module.exports = __toCommonJS2(index_exports);
14614
+ module.exports = __toCommonJS(index_exports);
14630
14615
  var colors = {
14631
14616
  aliceblue: [240, 248, 255],
14632
14617
  antiquewhite: [250, 235, 215],
@@ -18232,6 +18217,207 @@ var THEMES = {
18232
18217
  brightWhite: "#ffffff"
18233
18218
  }
18234
18219
  };
18220
+ async function configureDomRenderer(page, options) {
18221
+ await page.evaluate((opts) => {
18222
+ const runtimeKey = "__takumiTuiDomRenderer";
18223
+ const rootId = "takumi-tui-dom-root";
18224
+ const styleId = "takumi-tui-dom-style";
18225
+ const win = window;
18226
+ const ensureStyle = () => {
18227
+ let style = document.getElementById(styleId);
18228
+ if (!style) {
18229
+ style = document.createElement("style");
18230
+ style.id = styleId;
18231
+ document.head.appendChild(style);
18232
+ }
18233
+ style.textContent = `
18234
+ #${rootId} {
18235
+ position: absolute;
18236
+ inset: 0;
18237
+ overflow: hidden;
18238
+ display: flex;
18239
+ flex-direction: column;
18240
+ white-space: pre;
18241
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
18242
+ line-height: 1.2;
18243
+ background: var(--takumi-tui-bg, #1e1e1e);
18244
+ color: var(--takumi-tui-fg, #d4d4d4);
18245
+ z-index: 4;
18246
+ pointer-events: none;
18247
+ user-select: text;
18248
+ }
18249
+ #${rootId}[data-active="0"] {
18250
+ display: none;
18251
+ }
18252
+ #${rootId} .takumi-tui-dom-row {
18253
+ display: flex;
18254
+ min-height: 1.2em;
18255
+ }
18256
+ #${rootId} .takumi-tui-dom-cell {
18257
+ display: inline-flex;
18258
+ align-items: center;
18259
+ justify-content: center;
18260
+ min-width: 0.62em;
18261
+ height: 1.2em;
18262
+ }
18263
+ #${rootId} .takumi-tui-dom-cell[data-cursor="true"] {
18264
+ outline: 1px solid currentColor;
18265
+ outline-offset: -1px;
18266
+ }
18267
+ body[data-takumi-dom-render="1"] .xterm-rows,
18268
+ body[data-takumi-dom-render="1"] .xterm-text-layer,
18269
+ body[data-takumi-dom-render="1"] .xterm-cursor-layer,
18270
+ body[data-takumi-dom-render="1"] .xterm-selection-layer {
18271
+ opacity: 0 !important;
18272
+ }
18273
+ `;
18274
+ };
18275
+ const ensureRoot = () => {
18276
+ let root = document.getElementById(rootId);
18277
+ if (!root) {
18278
+ root = document.createElement("div");
18279
+ root.id = rootId;
18280
+ root.setAttribute("role", "grid");
18281
+ root.setAttribute("aria-label", "Terminal DOM renderer");
18282
+ const host = document.getElementById("terminal-container") ?? document.querySelector(".xterm") ?? document.body;
18283
+ if (getComputedStyle(host).position === "static") {
18284
+ host.style.position = "relative";
18285
+ }
18286
+ host.appendChild(root);
18287
+ }
18288
+ root.style.setProperty("--takumi-tui-bg", opts.theme === "light" ? "#ffffff" : "#1e1e1e");
18289
+ root.style.setProperty("--takumi-tui-fg", opts.theme === "light" ? "#1e1e1e" : "#d4d4d4");
18290
+ root.style.fontSize = `${opts.fontSize ?? 14}px`;
18291
+ root.dataset.active = opts.active ? "1" : "0";
18292
+ if (opts.active)
18293
+ root.removeAttribute("aria-hidden");
18294
+ else
18295
+ root.setAttribute("aria-hidden", "true");
18296
+ document.body.dataset.takumiDomRender = opts.active ? "1" : "0";
18297
+ return root;
18298
+ };
18299
+ const readCellChars = (line, col) => {
18300
+ try {
18301
+ const cell = typeof line?.getCell === "function" ? line.getCell(col) : null;
18302
+ const chars = typeof cell?.getChars === "function" ? cell.getChars() : "";
18303
+ if (chars)
18304
+ return chars;
18305
+ } catch {}
18306
+ try {
18307
+ const text = typeof line?.translateToString === "function" ? line.translateToString(false, col, col + 1) : "";
18308
+ if (text)
18309
+ return text;
18310
+ } catch {}
18311
+ return " ";
18312
+ };
18313
+ const buildState = (activeOnly) => {
18314
+ const term = win.term ?? win.terminal;
18315
+ if (!term?.buffer?.active) {
18316
+ return {
18317
+ text: "",
18318
+ rows: [],
18319
+ row_count: 0,
18320
+ cols: null,
18321
+ total_rows: 0,
18322
+ buffer_length: null,
18323
+ cursor_row: -1,
18324
+ cursor_col: -1,
18325
+ font_size: null,
18326
+ theme: opts.theme
18327
+ };
18328
+ }
18329
+ const buf = term.buffer.active;
18330
+ const rows = [];
18331
+ const root = ensureRoot();
18332
+ const fragment = document.createDocumentFragment();
18333
+ for (let row = 0;row < buf.length; row++) {
18334
+ const line = buf.getLine(row);
18335
+ if (!line)
18336
+ continue;
18337
+ const rowEl = document.createElement("div");
18338
+ rowEl.className = "takumi-tui-dom-row";
18339
+ rowEl.setAttribute("role", "row");
18340
+ rowEl.dataset.row = String(row);
18341
+ rowEl.setAttribute("aria-rowindex", String(row + 1));
18342
+ let rowText = "";
18343
+ for (let col = 0;col < term.cols; col++) {
18344
+ const char = readCellChars(line, col) || " ";
18345
+ rowText += char;
18346
+ const cellEl = document.createElement("span");
18347
+ cellEl.className = "takumi-tui-dom-cell";
18348
+ cellEl.setAttribute("role", "gridcell");
18349
+ cellEl.dataset.row = String(row);
18350
+ cellEl.dataset.col = String(col);
18351
+ cellEl.setAttribute("aria-colindex", String(col + 1));
18352
+ cellEl.textContent = char;
18353
+ if (buf.cursorY === row && buf.cursorX === col) {
18354
+ cellEl.dataset.cursor = "true";
18355
+ }
18356
+ rowEl.appendChild(cellEl);
18357
+ }
18358
+ rows.push(rowText.replace(/\s+$/g, ""));
18359
+ rowEl.setAttribute("aria-label", rows[rows.length - 1] || " ");
18360
+ fragment.appendChild(rowEl);
18361
+ }
18362
+ root.replaceChildren(fragment);
18363
+ root.setAttribute("aria-rowcount", String(rows.length));
18364
+ root.dataset.method = "dom";
18365
+ return {
18366
+ text: rows.join(`
18367
+ `).trimEnd(),
18368
+ rows,
18369
+ row_count: rows.length,
18370
+ cols: term.cols,
18371
+ total_rows: term.rows,
18372
+ buffer_length: buf.length,
18373
+ cursor_row: buf.cursorY,
18374
+ cursor_col: buf.cursorX,
18375
+ font_size: term.options?.fontSize ?? null,
18376
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
18377
+ };
18378
+ };
18379
+ ensureStyle();
18380
+ ensureRoot();
18381
+ if (!win[runtimeKey]) {
18382
+ win[runtimeKey] = {
18383
+ sync: () => buildState(false),
18384
+ activate: (active) => {
18385
+ const root = ensureRoot();
18386
+ root.dataset.active = active ? "1" : "0";
18387
+ if (active)
18388
+ root.removeAttribute("aria-hidden");
18389
+ else
18390
+ root.setAttribute("aria-hidden", "true");
18391
+ document.body.dataset.takumiDomRender = active ? "1" : "0";
18392
+ }
18393
+ };
18394
+ const intervalId = window.setInterval(() => {
18395
+ try {
18396
+ win[runtimeKey]?.sync?.();
18397
+ } catch {}
18398
+ }, 50);
18399
+ win[runtimeKey].intervalId = intervalId;
18400
+ }
18401
+ win[runtimeKey].activate(opts.active);
18402
+ win[runtimeKey].sync();
18403
+ }, options);
18404
+ }
18405
+ async function destroyDomRenderer(page) {
18406
+ await page.evaluate(() => {
18407
+ const runtimeKey = "__takumiTuiDomRenderer";
18408
+ const win = window;
18409
+ if (win[runtimeKey]?.intervalId) {
18410
+ clearInterval(win[runtimeKey].intervalId);
18411
+ }
18412
+ delete win[runtimeKey];
18413
+ document.getElementById("takumi-tui-dom-root")?.remove();
18414
+ document.getElementById("takumi-tui-dom-style")?.remove();
18415
+ delete document.body.dataset.takumiDomRender;
18416
+ }).catch(() => {});
18417
+ }
18418
+ function isDomMethod(method) {
18419
+ return method === "dom";
18420
+ }
18235
18421
  function isTuiAvailable() {
18236
18422
  try {
18237
18423
  execSync2("which ttyd", { stdio: "ignore" });
@@ -18244,7 +18430,7 @@ async function findAvailablePort(startPort) {
18244
18430
  let port = startPort;
18245
18431
  for (let i = 0;i < 100; i++) {
18246
18432
  try {
18247
- const resp = await fetch(`http://localhost:${port}`);
18433
+ await fetch(`http://localhost:${port}`);
18248
18434
  port++;
18249
18435
  } catch {
18250
18436
  return port;
@@ -18270,24 +18456,16 @@ async function launchTui(command, options = {}) {
18270
18456
  }
18271
18457
  const port = await findAvailablePort(nextPort);
18272
18458
  nextPort = port + 1;
18273
- const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], {
18274
- stdio: "ignore",
18275
- detached: false
18276
- });
18459
+ const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
18277
18460
  ttydProcess.on("error", (err) => {
18278
18461
  console.error(`[tui] ttyd process error: ${err.message}`);
18279
18462
  });
18280
18463
  try {
18281
18464
  await waitForTtyd(port);
18282
18465
  const viewport = options.viewport ?? { width: 1280, height: 720 };
18283
- const browser = await launchPlaywright({
18284
- headless: options.headless ?? true,
18285
- viewport
18286
- });
18466
+ const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
18287
18467
  const page = await getPage(browser, { viewport });
18288
- await page.goto(`http://localhost:${port}`, {
18289
- waitUntil: "domcontentloaded"
18290
- });
18468
+ await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
18291
18469
  await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
18292
18470
  let resolvedTheme = "dark";
18293
18471
  const requestedTheme = options.theme ?? "system";
@@ -18306,16 +18484,15 @@ async function launchTui(command, options = {}) {
18306
18484
  const themeColors = THEMES[resolvedTheme];
18307
18485
  await page.evaluate((theme) => {
18308
18486
  const term = window.term ?? window.terminal;
18309
- if (term?.options) {
18487
+ if (term?.options)
18310
18488
  term.options.theme = theme;
18311
- }
18312
18489
  document.body.style.backgroundColor = theme.background;
18313
18490
  const container = document.getElementById("terminal-container");
18314
18491
  if (container)
18315
18492
  container.style.backgroundColor = theme.background;
18316
- const viewport2 = document.querySelector(".xterm-viewport");
18317
- if (viewport2)
18318
- viewport2.style.backgroundColor = theme.background;
18493
+ const vp = document.querySelector(".xterm-viewport");
18494
+ if (vp)
18495
+ vp.style.backgroundColor = theme.background;
18319
18496
  }, themeColors);
18320
18497
  if (options.fontSize) {
18321
18498
  await page.evaluate((size) => {
@@ -18324,13 +18501,31 @@ async function launchTui(command, options = {}) {
18324
18501
  term.options.fontSize = size;
18325
18502
  }, options.fontSize);
18326
18503
  }
18327
- return { ttydProcess, port, browser, page, theme: resolvedTheme };
18504
+ const method = options.method ?? "buffer";
18505
+ await configureDomRenderer(page, {
18506
+ active: isDomMethod(method),
18507
+ theme: resolvedTheme,
18508
+ fontSize: options.fontSize
18509
+ });
18510
+ return {
18511
+ ttydProcess,
18512
+ port,
18513
+ browser,
18514
+ page,
18515
+ theme: resolvedTheme,
18516
+ method,
18517
+ lastHealthCheck: Date.now(),
18518
+ reconnectCount: 0
18519
+ };
18328
18520
  } catch (err) {
18329
- ttydProcess.kill();
18521
+ try {
18522
+ ttydProcess.kill("SIGTERM");
18523
+ } catch {}
18330
18524
  throw err;
18331
18525
  }
18332
18526
  }
18333
18527
  async function closeTui(session) {
18528
+ await destroyDomRenderer(session.page);
18334
18529
  try {
18335
18530
  await session.page.close();
18336
18531
  } catch {}
@@ -18340,6 +18535,9 @@ async function closeTui(session) {
18340
18535
  try {
18341
18536
  session.ttydProcess.kill("SIGTERM");
18342
18537
  } catch {}
18538
+ try {
18539
+ session.ttydProcess.kill("SIGKILL");
18540
+ } catch {}
18343
18541
  }
18344
18542
 
18345
18543
  // src/engines/selector.ts
@@ -18551,19 +18749,13 @@ Object.defineProperty(navigator, 'plugins', {
18551
18749
  { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
18552
18750
  { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
18553
18751
  ];
18554
- // Mimic PluginArray interface
18555
- const pluginArray = Object.create(PluginArray.prototype);
18752
+ // Mimic PluginArray interface \u2014 guard against removed prototypes
18753
+ const pluginArray = {};
18556
18754
  plugins.forEach((p, i) => {
18557
- const plugin = Object.create(Plugin.prototype);
18558
- Object.defineProperties(plugin, {
18559
- name: { value: p.name, enumerable: true },
18560
- filename: { value: p.filename, enumerable: true },
18561
- description: { value: p.description, enumerable: true },
18562
- length: { value: p.length, enumerable: true },
18563
- });
18755
+ const plugin = { ...p, item: () => null };
18564
18756
  pluginArray[i] = plugin;
18565
18757
  });
18566
- Object.defineProperty(pluginArray, 'length', { value: plugins.length });
18758
+ pluginArray.length = plugins.length;
18567
18759
  pluginArray.item = (i) => pluginArray[i] || null;
18568
18760
  pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
18569
18761
  pluginArray.refresh = () => {};
@@ -18618,14 +18810,16 @@ if (ttlInterval.unref)
18618
18810
  var DB_PRUNE_INTERVAL_MS = 30 * 60000;
18619
18811
  var DB_RETENTION_HOURS = 24;
18620
18812
  var dbPruneInterval = setInterval(() => {
18621
- try {
18622
- const { getDatabase: getDatabase2 } = (init_schema(), __toCommonJS(exports_schema));
18623
- const db = getDatabase2();
18624
- const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
18625
- db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
18626
- db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
18627
- db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
18628
- } catch {}
18813
+ (async () => {
18814
+ try {
18815
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
18816
+ const db = getDatabase2();
18817
+ const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
18818
+ db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
18819
+ db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
18820
+ db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
18821
+ } catch {}
18822
+ })();
18629
18823
  }, DB_PRUNE_INTERVAL_MS);
18630
18824
  if (dbPruneInterval.unref)
18631
18825
  dbPruneInterval.unref();
@@ -18661,7 +18855,7 @@ async function createSession2(opts = {}) {
18661
18855
  try {
18662
18856
  cleanups2.push(setupDialogHandler(page2, session2.id));
18663
18857
  } catch {}
18664
- handles.set(session2.id, { browser: cdpBrowser, bunView: null, tuiSession: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
18858
+ handles.set(session2.id, { browser: cdpBrowser, bunView: null, tuiSession: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "" });
18665
18859
  return { session: session2, page: page2 };
18666
18860
  }
18667
18861
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
@@ -18675,13 +18869,29 @@ async function createSession2(opts = {}) {
18675
18869
  browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
18676
18870
  page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
18677
18871
  } else {
18678
- bunView = new BunWebViewSession({
18872
+ const testView = new BunWebViewSession({
18679
18873
  width: opts.viewport?.width ?? 1280,
18680
18874
  height: opts.viewport?.height ?? 720,
18681
18875
  profile: opts.name ?? undefined
18682
18876
  });
18683
- if (opts.stealth) {}
18684
- page = createBunProxy(bunView);
18877
+ let bunWorks = true;
18878
+ try {
18879
+ await testView.goto("data:text/html,<html></html>");
18880
+ } catch {
18881
+ bunWorks = false;
18882
+ try {
18883
+ await testView.close();
18884
+ } catch {}
18885
+ }
18886
+ if (!bunWorks) {
18887
+ console.warn("[browser] Bun.WebView exists but Chrome not available \u2014 falling back to playwright");
18888
+ browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
18889
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
18890
+ } else {
18891
+ bunView = testView;
18892
+ if (opts.stealth) {}
18893
+ page = createBunProxy(bunView);
18894
+ }
18685
18895
  }
18686
18896
  } else if (resolvedEngine === "lightpanda") {
18687
18897
  browser = await connectLightpanda();
@@ -18693,7 +18903,8 @@ async function createSession2(opts = {}) {
18693
18903
  headless: opts.headless ?? true,
18694
18904
  viewport: opts.viewport,
18695
18905
  theme: opts.tuiTheme ?? "system",
18696
- fontSize: opts.tuiFontSize
18906
+ fontSize: opts.tuiFontSize,
18907
+ method: opts.tuiMethod ?? "buffer"
18697
18908
  });
18698
18909
  browser = tuiSess.browser;
18699
18910
  page = tuiSess.page;
@@ -18719,7 +18930,7 @@ async function createSession2(opts = {}) {
18719
18930
  try {
18720
18931
  cleanups2.push(setupDialogHandler(page, session2.id));
18721
18932
  } catch {}
18722
- handles.set(session2.id, { browser, bunView: null, tuiSession: tuiSess, page, engine: "tui", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
18933
+ handles.set(session2.id, { browser, bunView: null, tuiSession: tuiSess, page, engine: "tui", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "bash" });
18723
18934
  return { session: session2, page };
18724
18935
  } else {
18725
18936
  browser = await pool.acquire(opts.headless ?? true);
@@ -18791,7 +19002,7 @@ async function createSession2(opts = {}) {
18791
19002
  } catch {}
18792
19003
  }
18793
19004
  }
18794
- handles.set(session.id, { browser, bunView, tuiSession: null, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
19005
+ handles.set(session.id, { browser, bunView, tuiSession: null, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "" });
18795
19006
  if (opts.startUrl) {
18796
19007
  try {
18797
19008
  if (bunView) {
@@ -18822,38 +19033,43 @@ function getSessionPage(sessionId) {
18822
19033
  }
18823
19034
  async function closeSession2(sessionId) {
18824
19035
  const handle = handles.get(sessionId);
18825
- if (handle) {
18826
- for (const cleanup of handle.cleanups) {
18827
- try {
18828
- cleanup();
18829
- } catch {}
18830
- }
18831
- if (handle.bunView) {
18832
- try {
18833
- await handle.bunView.close();
18834
- } catch {}
18835
- } else if (handle.tuiSession) {} else {
18836
- try {
18837
- await handle.page.context().close();
18838
- } catch {}
18839
- if (handle.browser)
18840
- pool.release(handle.browser);
19036
+ try {
19037
+ if (handle) {
19038
+ for (const cleanup of handle.cleanups) {
19039
+ try {
19040
+ cleanup();
19041
+ } catch {}
19042
+ }
19043
+ if (handle.bunView) {
19044
+ try {
19045
+ await handle.bunView.close();
19046
+ } catch {}
19047
+ } else if (handle.tuiSession) {} else {
19048
+ try {
19049
+ await handle.page.context().close();
19050
+ } catch {}
19051
+ try {
19052
+ if (handle.browser)
19053
+ pool.release(handle.browser);
19054
+ } catch {}
19055
+ }
18841
19056
  }
19057
+ try {
19058
+ const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
19059
+ clearLastSnapshot2(sessionId);
19060
+ clearSessionRefs2(sessionId);
19061
+ } catch {}
19062
+ try {
19063
+ const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
19064
+ stopAllWatchesForSession2(sessionId);
19065
+ } catch {}
19066
+ try {
19067
+ const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
19068
+ clearDialogs2(sessionId);
19069
+ } catch {}
19070
+ } finally {
18842
19071
  handles.delete(sessionId);
18843
19072
  }
18844
- try {
18845
- const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
18846
- clearLastSnapshot2(sessionId);
18847
- clearSessionRefs2(sessionId);
18848
- } catch {}
18849
- try {
18850
- const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
18851
- stopAllWatchesForSession2(sessionId);
18852
- } catch {}
18853
- try {
18854
- const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
18855
- clearDialogs2(sessionId);
18856
- } catch {}
18857
19073
  return closeSession(sessionId);
18858
19074
  }
18859
19075
  function listSessions2(filter) {
@@ -18952,6 +19168,12 @@ import { mkdirSync as mkdirSync7 } from "fs";
18952
19168
  // src/db/gallery.ts
18953
19169
  init_schema();
18954
19170
  import { randomUUID as randomUUID4 } from "crypto";
19171
+ function validateDataPath(filePath) {
19172
+ if (filePath.includes("..")) {
19173
+ throw new Error(`File path must not contain '..': ${filePath}`);
19174
+ }
19175
+ return filePath;
19176
+ }
18955
19177
  function deserialize(row) {
18956
19178
  return {
18957
19179
  id: row.id,
@@ -18976,6 +19198,9 @@ function deserialize(row) {
18976
19198
  function createEntry(data) {
18977
19199
  const db = getDatabase();
18978
19200
  const id = randomUUID4();
19201
+ validateDataPath(data.path);
19202
+ if (data.thumbnail_path)
19203
+ validateDataPath(data.thumbnail_path);
18979
19204
  db.prepare(`
18980
19205
  INSERT INTO gallery_entries
18981
19206
  (id, session_id, project_id, url, title, path, thumbnail_path, format,
@@ -19591,38 +19816,74 @@ async function diffImages(path1, path2) {
19591
19816
 
19592
19817
  // src/server/index.ts
19593
19818
  var PORT = parseInt(process.env["BROWSER_SERVER_PORT"] ?? "7030");
19819
+ var API_KEY = process.env["BROWSER_API_KEY"] ?? null;
19820
+ var ALLOWED_ORIGIN = process.env["BROWSER_ALLOWED_ORIGIN"] ?? (API_KEY ? null : "http://localhost:3000");
19594
19821
  var startTime = Date.now();
19595
- var CORS_HEADERS = {
19596
- "Access-Control-Allow-Origin": "*",
19597
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
19598
- "Access-Control-Allow-Headers": "Content-Type"
19599
- };
19822
+ function corsHeaders(origin) {
19823
+ const headers = {
19824
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
19825
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
19826
+ };
19827
+ if (origin) {
19828
+ if (!API_KEY && !origin.startsWith("http://localhost") && !origin.startsWith("http://127.0.0.1")) {
19829
+ headers["Access-Control-Allow-Origin"] = ALLOWED_ORIGIN ?? "http://localhost:3000";
19830
+ } else {
19831
+ headers["Access-Control-Allow-Origin"] = origin;
19832
+ }
19833
+ }
19834
+ return headers;
19835
+ }
19836
+ function authenticate(req) {
19837
+ if (!API_KEY)
19838
+ return null;
19839
+ const header = req.headers.get("Authorization") ?? "";
19840
+ const token = header.startsWith("Bearer ") ? header.slice(7) : "";
19841
+ if (token !== API_KEY) {
19842
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
19843
+ status: 401,
19844
+ headers: { "Content-Type": "application/json" }
19845
+ });
19846
+ }
19847
+ return null;
19848
+ }
19600
19849
  var networkCleanup = new Map;
19601
19850
  var consoleCleanup = new Map;
19602
19851
  var harCaptures = new Map;
19603
- function ok(data, status = 200) {
19852
+ async function safeJson(req) {
19853
+ try {
19854
+ const contentType = req.headers.get("content-type") ?? "";
19855
+ if (!contentType.includes("application/json")) {
19856
+ return { error: badRequest("Content-Type must be application/json") };
19857
+ }
19858
+ const body = await req.json();
19859
+ return { body };
19860
+ } catch {
19861
+ return { error: badRequest("Invalid or missing JSON body") };
19862
+ }
19863
+ }
19864
+ function ok(data, status = 200, extraHeaders) {
19604
19865
  return new Response(JSON.stringify(data), {
19605
19866
  status,
19606
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
19867
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
19607
19868
  });
19608
19869
  }
19609
- function notFound(msg) {
19870
+ function notFound(msg, extraHeaders) {
19610
19871
  return new Response(JSON.stringify({ error: msg }), {
19611
19872
  status: 404,
19612
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
19873
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
19613
19874
  });
19614
19875
  }
19615
- function badRequest(msg) {
19876
+ function badRequest(msg, extraHeaders) {
19616
19877
  return new Response(JSON.stringify({ error: msg }), {
19617
19878
  status: 400,
19618
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
19879
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
19619
19880
  });
19620
19881
  }
19621
- function serverError(e) {
19882
+ function serverError(e, extraHeaders) {
19622
19883
  const msg = e instanceof Error ? e.message : String(e);
19623
19884
  return new Response(JSON.stringify({ error: msg }), {
19624
19885
  status: 500,
19625
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
19886
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
19626
19887
  });
19627
19888
  }
19628
19889
  var server = Bun.serve({
@@ -19631,8 +19892,15 @@ var server = Bun.serve({
19631
19892
  const url = new URL(req.url);
19632
19893
  const path = url.pathname;
19633
19894
  const method = req.method;
19895
+ const origin = req.headers.get("Origin") ?? undefined;
19896
+ const headers = corsHeaders(origin ?? null);
19634
19897
  if (method === "OPTIONS") {
19635
- return new Response(null, { status: 204, headers: CORS_HEADERS });
19898
+ return new Response(null, { status: 204, headers });
19899
+ }
19900
+ if (!path.startsWith("/dashboard") && path !== "/health") {
19901
+ const authError = authenticate(req);
19902
+ if (authError)
19903
+ return authError;
19636
19904
  }
19637
19905
  try {
19638
19906
  if (path === "/health" && method === "GET") {
@@ -19649,7 +19917,10 @@ var server = Bun.serve({
19649
19917
  return ok({ sessions: listSessions2(status ? { status, projectId } : { projectId }) });
19650
19918
  }
19651
19919
  if (path === "/api/sessions" && method === "POST") {
19652
- const body = await req.json();
19920
+ const parsed = await safeJson(req);
19921
+ if ("error" in parsed)
19922
+ return parsed.error;
19923
+ const body = parsed.body;
19653
19924
  const { session } = await createSession2({
19654
19925
  engine: body.engine ?? "auto",
19655
19926
  projectId: body.project_id,
@@ -19671,25 +19942,36 @@ var server = Bun.serve({
19671
19942
  return ok({ session });
19672
19943
  }
19673
19944
  if (path === "/api/navigate" && method === "POST") {
19674
- const body = await req.json();
19945
+ const parsed = await safeJson(req);
19946
+ if ("error" in parsed)
19947
+ return parsed.error;
19948
+ const body = parsed.body;
19675
19949
  if (!body.session_id || !body.url)
19676
- return badRequest("session_id and url required");
19677
- const page = getSessionPage(body.session_id);
19678
- await navigate(page, body.url);
19679
- return ok({ url: body.url, title: await page.title(), current_url: page.url() });
19950
+ return badRequest("session_id and url required", headers);
19951
+ const sessionId = body.session_id;
19952
+ const url2 = body.url;
19953
+ const page = getSessionPage(sessionId);
19954
+ await navigate(page, url2);
19955
+ return ok({ url: url2, title: await page.title(), current_url: page.url() });
19680
19956
  }
19681
19957
  if (path === "/api/extract" && method === "POST") {
19682
- const body = await req.json();
19958
+ const parsed = await safeJson(req);
19959
+ if ("error" in parsed)
19960
+ return parsed.error;
19961
+ const body = parsed.body;
19683
19962
  if (!body.session_id)
19684
- return badRequest("session_id required");
19963
+ return badRequest("session_id required", headers);
19685
19964
  const page = getSessionPage(body.session_id);
19686
19965
  const result = await extract(page, { format: body.format, selector: body.selector });
19687
19966
  return ok(result);
19688
19967
  }
19689
19968
  if (path === "/api/screenshot" && method === "POST") {
19690
- const body = await req.json();
19969
+ const parsed = await safeJson(req);
19970
+ if ("error" in parsed)
19971
+ return parsed.error;
19972
+ const body = parsed.body;
19691
19973
  if (!body.session_id)
19692
- return badRequest("session_id required");
19974
+ return badRequest("session_id required", headers);
19693
19975
  const page = getSessionPage(body.session_id);
19694
19976
  const result = await takeScreenshot(page, { selector: body.selector, fullPage: body.full_page });
19695
19977
  return ok(result);
@@ -19726,16 +20008,22 @@ var server = Bun.serve({
19726
20008
  return ok({ metrics: await getPerformanceMetrics(page) });
19727
20009
  }
19728
20010
  if (path === "/api/har/start" && method === "POST") {
19729
- const body = await req.json();
20011
+ const parsed = await safeJson(req);
20012
+ if ("error" in parsed)
20013
+ return parsed.error;
20014
+ const body = parsed.body;
19730
20015
  const page = getSessionPage(body.session_id);
19731
20016
  harCaptures.set(body.session_id, startHAR(page));
19732
20017
  return ok({ started: true });
19733
20018
  }
19734
20019
  if (path === "/api/har/stop" && method === "POST") {
19735
- const body = await req.json();
20020
+ const parsed = await safeJson(req);
20021
+ if ("error" in parsed)
20022
+ return parsed.error;
20023
+ const body = parsed.body;
19736
20024
  const capture = harCaptures.get(body.session_id);
19737
20025
  if (!capture)
19738
- return notFound("No active HAR capture");
20026
+ return notFound("No active HAR capture", headers);
19739
20027
  const har = capture.stop();
19740
20028
  harCaptures.delete(body.session_id);
19741
20029
  return ok({ har });
@@ -19744,8 +20032,11 @@ var server = Bun.serve({
19744
20032
  return ok({ recordings: listRecordings(url.searchParams.get("project_id") ?? undefined) });
19745
20033
  }
19746
20034
  if (path.match(/^\/api\/recordings\/([^/]+)\/replay$/) && method === "POST") {
20035
+ const parsed = await safeJson(req);
20036
+ if ("error" in parsed)
20037
+ return parsed.error;
20038
+ const body = parsed.body;
19747
20039
  const id = path.split("/")[3];
19748
- const body = await req.json();
19749
20040
  const page = getSessionPage(body.session_id);
19750
20041
  const result = await replayRecording(id, page);
19751
20042
  return ok(result);
@@ -19757,9 +20048,12 @@ var server = Bun.serve({
19757
20048
  return ok({ deleted: id });
19758
20049
  }
19759
20050
  if (path === "/api/crawl" && method === "POST") {
19760
- const body = await req.json();
20051
+ const parsed = await safeJson(req);
20052
+ if ("error" in parsed)
20053
+ return parsed.error;
20054
+ const body = parsed.body;
19761
20055
  if (!body.url)
19762
- return badRequest("url required");
20056
+ return badRequest("url required", headers);
19763
20057
  const result = await crawl(body.url, {
19764
20058
  maxDepth: body.max_depth ?? 2,
19765
20059
  maxPages: body.max_pages ?? 50,
@@ -19771,9 +20065,12 @@ var server = Bun.serve({
19771
20065
  return ok({ agents: listAgents(url.searchParams.get("project_id") ?? undefined) });
19772
20066
  }
19773
20067
  if (path === "/api/agents" && method === "POST") {
19774
- const body = await req.json();
20068
+ const parsed = await safeJson(req);
20069
+ if ("error" in parsed)
20070
+ return parsed.error;
20071
+ const body = parsed.body;
19775
20072
  if (!body.name)
19776
- return badRequest("name required");
20073
+ return badRequest("name required", headers);
19777
20074
  const agent = registerAgent2(body.name, { description: body.description, projectId: body.project_id, sessionId: body.session_id, workingDir: body.working_dir });
19778
20075
  return ok({ agent }, 201);
19779
20076
  }
@@ -19792,9 +20089,12 @@ var server = Bun.serve({
19792
20089
  return ok({ projects: listProjects() });
19793
20090
  }
19794
20091
  if (path === "/api/projects" && method === "POST") {
19795
- const body = await req.json();
20092
+ const parsed = await safeJson(req);
20093
+ if ("error" in parsed)
20094
+ return parsed.error;
20095
+ const body = parsed.body;
19796
20096
  if (!body.name || !body.path)
19797
- return badRequest("name and path required");
20097
+ return badRequest("name and path required", headers);
19798
20098
  const project = ensureProject(body.name, body.path, body.description);
19799
20099
  return ok({ project }, 201);
19800
20100
  }
@@ -19810,21 +20110,30 @@ var server = Bun.serve({
19810
20110
  return ok(getGalleryStats(url.searchParams.get("project_id") ?? undefined));
19811
20111
  }
19812
20112
  if (path === "/api/gallery/diff" && method === "POST") {
19813
- const body = await req.json();
20113
+ const parsed = await safeJson(req);
20114
+ if ("error" in parsed)
20115
+ return parsed.error;
20116
+ const body = parsed.body;
19814
20117
  const e1 = getEntry(body.id1);
19815
20118
  const e2 = getEntry(body.id2);
19816
20119
  if (!e1 || !e2)
19817
- return notFound("Gallery entry not found");
20120
+ return notFound("Gallery entry not found", headers);
19818
20121
  return ok(await diffImages(e1.path, e2.path));
19819
20122
  }
19820
20123
  if (path.match(/^\/api\/gallery\/([^/]+)\/tag$/) && method === "POST") {
20124
+ const parsed = await safeJson(req);
20125
+ if ("error" in parsed)
20126
+ return parsed.error;
20127
+ const body = parsed.body;
19821
20128
  const id = path.split("/")[3];
19822
- const body = await req.json();
19823
20129
  return ok({ entry: tagEntry(id, body.tag) });
19824
20130
  }
19825
20131
  if (path.match(/^\/api\/gallery\/([^/]+)\/favorite$/) && method === "PUT") {
20132
+ const parsed = await safeJson(req);
20133
+ if ("error" in parsed)
20134
+ return parsed.error;
20135
+ const body = parsed.body;
19826
20136
  const id = path.split("/")[3];
19827
- const body = await req.json();
19828
20137
  return ok({ entry: favoriteEntry(id, body.favorited) });
19829
20138
  }
19830
20139
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
@@ -19832,14 +20141,14 @@ var server = Bun.serve({
19832
20141
  const entry = getEntry(id);
19833
20142
  if (!entry?.thumbnail_path || !existsSync9(entry.thumbnail_path))
19834
20143
  return notFound("Thumbnail not found");
19835
- return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
20144
+ return new Response(Bun.file(entry.thumbnail_path), { headers: { ...headers } });
19836
20145
  }
19837
20146
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
19838
20147
  const id = path.split("/")[3];
19839
20148
  const entry = getEntry(id);
19840
20149
  if (!entry?.path || !existsSync9(entry.path))
19841
20150
  return notFound("Image not found");
19842
- return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
20151
+ return new Response(Bun.file(entry.path), { headers: { ...headers } });
19843
20152
  }
19844
20153
  if (path.match(/^\/api\/gallery\/([^/]+)$/) && method === "DELETE") {
19845
20154
  const id = path.split("/")[3];
@@ -19867,7 +20176,7 @@ var server = Bun.serve({
19867
20176
  const file = getDownload(id);
19868
20177
  if (!file || !existsSync9(file.path))
19869
20178
  return notFound("Download not found");
19870
- return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
20179
+ return new Response(Bun.file(file.path), { headers: { ...headers } });
19871
20180
  }
19872
20181
  if (path.match(/^\/api\/downloads\/([^/]+)$/) && method === "DELETE") {
19873
20182
  const id = path.split("/")[3];
@@ -19875,15 +20184,21 @@ var server = Bun.serve({
19875
20184
  }
19876
20185
  const dashboardDist = join12(import.meta.dir, "../../dashboard/dist");
19877
20186
  if (existsSync9(dashboardDist)) {
19878
- const filePath = path === "/" ? join12(dashboardDist, "index.html") : join12(dashboardDist, path);
20187
+ const cleanPath = path.replace(/^\//, "");
20188
+ if (cleanPath.includes("..") || cleanPath.startsWith("/"))
20189
+ return notFound("Not found", headers);
20190
+ const filePath = path === "/" ? join12(dashboardDist, "index.html") : join12(dashboardDist, cleanPath);
20191
+ const resolved = await Bun.file(filePath).arrayBuffer().then(() => join12(dashboardDist, cleanPath)) || "";
20192
+ if (!resolved.startsWith(dashboardDist))
20193
+ return notFound("Not found", headers);
19879
20194
  if (existsSync9(filePath)) {
19880
- return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
20195
+ return new Response(Bun.file(filePath), { headers });
19881
20196
  }
19882
- return new Response(Bun.file(join12(dashboardDist, "index.html")), { headers: CORS_HEADERS });
20197
+ return new Response(Bun.file(join12(dashboardDist, "index.html")), { headers });
19883
20198
  }
19884
20199
  if (path === "/" || path === "") {
19885
20200
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
19886
- headers: { "Content-Type": "text/plain", ...CORS_HEADERS }
20201
+ headers: { "Content-Type": "text/plain", ...headers }
19887
20202
  });
19888
20203
  }
19889
20204
  return notFound(`Route not found: ${method} ${path}`);