@btraut/browser-bridge 0.12.1 → 0.13.1

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,3 +1,6 @@
1
+ // packages/shared/src/contract-version.ts
2
+ var DRIVE_WS_PROTOCOL_VERSION = "2026-02-17.1";
3
+
1
4
  // packages/extension/src/error-sanitizer.ts
2
5
  var TRAILING_PUNCTUATION_RE = /[.,;:!?]+$/;
3
6
  var stripTrailingPunctuation = (value) => {
@@ -97,7 +100,9 @@ var SITE_ALLOWLIST_KEY = "siteAllowlist";
97
100
  var PERMISSION_PROMPT_WAIT_MS_KEY = "permissionPromptWaitMs";
98
101
  var DEFAULT_PERMISSION_PROMPT_WAIT_MS = 3e4;
99
102
  var SITE_PERMISSIONS_MODE_KEY = "sitePermissionsMode";
103
+ var DEBUGGER_CAPABILITY_ENABLED_KEY = "debuggerCapabilityEnabled";
100
104
  var DEFAULT_SITE_PERMISSIONS_MODE = "granular";
105
+ var DEFAULT_DEBUGGER_CAPABILITY_ENABLED = false;
101
106
  var siteKeyFromUrl = (rawUrl) => {
102
107
  if (!rawUrl || typeof rawUrl !== "string") {
103
108
  return null;
@@ -199,6 +204,27 @@ var readPermissionPromptWaitMs = async () => {
199
204
  );
200
205
  });
201
206
  };
207
+ var readDebuggerCapabilityEnabled = async () => {
208
+ return await new Promise((resolve) => {
209
+ chrome.storage.local.get(
210
+ [DEBUGGER_CAPABILITY_ENABLED_KEY],
211
+ (result) => {
212
+ const raw = result?.[DEBUGGER_CAPABILITY_ENABLED_KEY];
213
+ if (typeof raw === "boolean") {
214
+ resolve(raw);
215
+ return;
216
+ }
217
+ try {
218
+ chrome.storage.local.set({
219
+ [DEBUGGER_CAPABILITY_ENABLED_KEY]: DEFAULT_DEBUGGER_CAPABILITY_ENABLED
220
+ });
221
+ } catch {
222
+ }
223
+ resolve(DEFAULT_DEBUGGER_CAPABILITY_ENABLED);
224
+ }
225
+ );
226
+ });
227
+ };
202
228
  var isSiteAllowed = async (siteKey) => {
203
229
  const key = normalizeSiteKey(siteKey);
204
230
  const allowlist = await readAllowlistRaw();
@@ -410,6 +436,54 @@ var PermissionPromptController = class {
410
436
  }
411
437
  };
412
438
 
439
+ // packages/extension/src/drive-reliability.ts
440
+ var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
441
+ "receiving end does not exist",
442
+ "message channel closed before a response was received",
443
+ "the message port closed before a response was received",
444
+ "extension port is moved into back/forward cache"
445
+ ];
446
+ var TAB_CHANNEL_RETRY_DELAYS_MS = [120, 200, 320, 500, 750, 1e3, 1200];
447
+ var normalizePathname = (pathname) => {
448
+ if (pathname.length === 0) {
449
+ return "/";
450
+ }
451
+ if (pathname.length > 1 && pathname.endsWith("/")) {
452
+ return pathname.slice(0, -1);
453
+ }
454
+ return pathname;
455
+ };
456
+ var isTransientTabChannelError = (message) => {
457
+ if (typeof message !== "string") {
458
+ return false;
459
+ }
460
+ const normalized = message.toLowerCase();
461
+ return TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS.some(
462
+ (pattern) => normalized.includes(pattern)
463
+ );
464
+ };
465
+ var getTabChannelRetryDelayMs = (attempt) => {
466
+ if (!Number.isInteger(attempt) || attempt < 1) {
467
+ return void 0;
468
+ }
469
+ return TAB_CHANNEL_RETRY_DELAYS_MS[attempt - 1];
470
+ };
471
+ var isLikelyNavigationCommitted = (requestedUrl, tabUrl) => {
472
+ if (typeof requestedUrl !== "string" || typeof tabUrl !== "string") {
473
+ return false;
474
+ }
475
+ if (requestedUrl === tabUrl) {
476
+ return true;
477
+ }
478
+ try {
479
+ const requested = new URL(requestedUrl);
480
+ const actual = new URL(tabUrl);
481
+ return requested.origin === actual.origin && normalizePathname(requested.pathname) === normalizePathname(actual.pathname) && requested.search === actual.search;
482
+ } catch {
483
+ return requestedUrl === tabUrl;
484
+ }
485
+ };
486
+
413
487
  // packages/extension/src/connection-state.ts
414
488
  var toIso = (ms) => new Date(ms).toISOString();
415
489
  var ConnectionStateTracker = class {
@@ -501,9 +575,62 @@ var HISTORY_POST_NAV_DOM_GRACE_TIMEOUT_MS = 2e3;
501
575
  var AGENT_TAB_ID_KEY = "agentTabId";
502
576
  var AGENT_TAB_GROUP_TITLE = "Browser Bridge";
503
577
  var AGENT_TAB_BOOTSTRAP_PATH = "agent-tab.html";
578
+ var AGENT_TAB_FAVICON_ASSET_PATH = "assets/icons/icon-32.png";
579
+ var AGENT_TAB_BRANDING_ACTION = "drive.agent_tab_branding";
580
+ var AGENT_TAB_GROUP_RETRY_DELAYS_MS = [0, 120, 300];
581
+ var AGENT_TAB_BRANDING_TIMEOUT_MS = 1500;
582
+ var BASE_NEGOTIATED_CAPABILITIES = Object.freeze({
583
+ "drive.navigate": true,
584
+ "drive.go_back": true,
585
+ "drive.go_forward": true,
586
+ "drive.click": true,
587
+ "drive.hover": true,
588
+ "drive.select": true,
589
+ "drive.type": true,
590
+ "drive.fill_form": true,
591
+ "drive.drag": true,
592
+ "drive.handle_dialog": true,
593
+ "drive.key": true,
594
+ "drive.key_press": true,
595
+ "drive.scroll": true,
596
+ "drive.screenshot": true,
597
+ "drive.wait_for": true,
598
+ "drive.tab_list": true,
599
+ "drive.tab_activate": true,
600
+ "drive.tab_close": true,
601
+ "drive.ping": true
602
+ });
603
+ var DEBUGGER_CAPABILITY_ACTIONS = [
604
+ "debugger.attach",
605
+ "debugger.detach",
606
+ "debugger.command"
607
+ ];
608
+ var buildNegotiatedCapabilities = (debuggerCapabilityEnabled) => {
609
+ const capabilities = {
610
+ ...BASE_NEGOTIATED_CAPABILITIES
611
+ };
612
+ for (const action of DEBUGGER_CAPABILITY_ACTIONS) {
613
+ capabilities[action] = debuggerCapabilityEnabled;
614
+ }
615
+ return capabilities;
616
+ };
617
+ var debuggerCapabilityDisabledError = () => {
618
+ return {
619
+ code: "ATTACH_DENIED",
620
+ message: "Debugger capability is disabled. Enable debugger-based inspect in extension options and retry.",
621
+ retryable: false,
622
+ details: {
623
+ reason: "debugger_capability_disabled",
624
+ next_step: "Open Browser Bridge extension options, enable debugger-based inspect, then retry."
625
+ }
626
+ };
627
+ };
504
628
  var getAgentTabBootstrapUrl = () => {
505
629
  return typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_BOOTSTRAP_PATH) : AGENT_TAB_BOOTSTRAP_PATH;
506
630
  };
631
+ var getAgentTabFaviconUrl = () => {
632
+ return typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_FAVICON_ASSET_PATH) : AGENT_TAB_FAVICON_ASSET_PATH;
633
+ };
507
634
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
508
635
  var makeEventId = /* @__PURE__ */ (() => {
509
636
  let counter = 0;
@@ -872,23 +999,38 @@ var ensureAgentTabGroup = async (tabId, windowId) => {
872
999
  if (!chrome.tabGroups || typeof chrome.tabGroups.update !== "function") {
873
1000
  return;
874
1001
  }
875
- try {
876
- const groupId = await wrapChromeCallback(
877
- (callback) => chrome.tabs.group(
878
- { tabIds: tabId, createProperties: { windowId } },
879
- callback
880
- )
881
- );
882
- await wrapChromeVoid(
883
- (callback) => chrome.tabGroups.update(
884
- groupId,
885
- { title: AGENT_TAB_GROUP_TITLE },
886
- () => callback()
887
- )
888
- );
889
- } catch (error) {
890
- console.debug("Failed to create/update agent tab group.", error);
1002
+ let lastError;
1003
+ for (const retryDelayMs of AGENT_TAB_GROUP_RETRY_DELAYS_MS) {
1004
+ if (retryDelayMs > 0) {
1005
+ await delayMs(retryDelayMs);
1006
+ }
1007
+ try {
1008
+ const groupId = await wrapChromeCallback(
1009
+ (callback) => chrome.tabs.group(
1010
+ { tabIds: tabId, createProperties: { windowId } },
1011
+ callback
1012
+ )
1013
+ );
1014
+ await wrapChromeVoid(
1015
+ (callback) => chrome.tabGroups.update(
1016
+ groupId,
1017
+ { title: AGENT_TAB_GROUP_TITLE },
1018
+ () => callback()
1019
+ )
1020
+ );
1021
+ return;
1022
+ } catch (error) {
1023
+ lastError = error;
1024
+ }
1025
+ }
1026
+ console.debug("Failed to create/update agent tab group.", lastError);
1027
+ };
1028
+ var ensureAgentTabGroupForTab = async (tabId, tab) => {
1029
+ const windowId = tab.windowId;
1030
+ if (typeof windowId !== "number") {
1031
+ return;
891
1032
  }
1033
+ await ensureAgentTabGroup(tabId, windowId);
892
1034
  };
893
1035
  var createAgentWindow = async () => {
894
1036
  const created = await wrapChromeCallback(
@@ -943,6 +1085,8 @@ var getOrCreateAgentTabId = async () => {
943
1085
  if (typeof url === "string" && isRestrictedUrl(url)) {
944
1086
  throw new Error(`Agent tab points at restricted URL: ${url}`);
945
1087
  }
1088
+ await ensureAgentTabGroupForTab(agentTabId, tab);
1089
+ void refreshAgentTabBranding(agentTabId);
946
1090
  return agentTabId;
947
1091
  } catch {
948
1092
  clearAgentTarget();
@@ -959,6 +1103,8 @@ var getOrCreateAgentTabId = async () => {
959
1103
  agentTabId = stored;
960
1104
  ensureLastActiveAt(stored);
961
1105
  markTabActive(stored);
1106
+ await ensureAgentTabGroupForTab(stored, tab);
1107
+ void refreshAgentTabBranding(stored);
962
1108
  return stored;
963
1109
  } catch {
964
1110
  await writeAgentTabId(null);
@@ -1017,12 +1163,18 @@ var sendToTab = async (tabId, action, params, options) => {
1017
1163
  chrome.tabs.sendMessage(tabId, message, (response) => {
1018
1164
  const error = chrome.runtime.lastError;
1019
1165
  if (error) {
1166
+ const retryable = isTransientTabChannelError(error.message);
1020
1167
  finish({
1021
1168
  ok: false,
1022
1169
  error: {
1023
1170
  code: "EVALUATION_FAILED",
1024
1171
  message: error.message,
1025
- retryable: false
1172
+ retryable,
1173
+ ...retryable ? {
1174
+ details: {
1175
+ reason: "transient_tab_channel_error"
1176
+ }
1177
+ } : {}
1026
1178
  }
1027
1179
  });
1028
1180
  return;
@@ -1042,27 +1194,31 @@ var sendToTab = async (tabId, action, params, options) => {
1042
1194
  });
1043
1195
  });
1044
1196
  };
1045
- const MAX_ATTEMPTS = 5;
1046
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
1197
+ for (let attempt = 1; ; attempt += 1) {
1047
1198
  const result = await attemptSend();
1048
1199
  if (result.ok) {
1049
1200
  return result;
1050
1201
  }
1051
- const message = result.error?.message;
1052
- const isNoReceiver = typeof message === "string" && message.toLowerCase().includes("receiving end does not exist");
1053
- if (!isNoReceiver || attempt === MAX_ATTEMPTS) {
1202
+ if (!isTransientTabChannelError(result.error?.message)) {
1054
1203
  return result;
1055
1204
  }
1056
- await delayMs(200);
1057
- }
1058
- return {
1059
- ok: false,
1060
- error: {
1061
- code: "INTERNAL",
1062
- message: "Failed to send message to content script.",
1063
- retryable: false
1205
+ const retryDelayMs = getTabChannelRetryDelayMs(attempt);
1206
+ if (retryDelayMs === void 0) {
1207
+ return result;
1064
1208
  }
1065
- };
1209
+ await delayMs(retryDelayMs);
1210
+ }
1211
+ };
1212
+ var refreshAgentTabBranding = async (tabId) => {
1213
+ const result = await sendToTab(
1214
+ tabId,
1215
+ AGENT_TAB_BRANDING_ACTION,
1216
+ { favicon_url: getAgentTabFaviconUrl() },
1217
+ { timeoutMs: AGENT_TAB_BRANDING_TIMEOUT_MS }
1218
+ );
1219
+ if (!result.ok) {
1220
+ return;
1221
+ }
1066
1222
  };
1067
1223
  var waitForHistoryNavigationSignal = async (tabId, timeoutMs) => {
1068
1224
  return await new Promise((resolve, reject) => {
@@ -1290,6 +1446,11 @@ var DriveSocket = class {
1290
1446
  getConnectionStatus() {
1291
1447
  return this.connection.getStatus();
1292
1448
  }
1449
+ refreshCapabilities() {
1450
+ void this.sendHello().catch((error) => {
1451
+ console.error("DriveSocket refreshCapabilities failed:", error);
1452
+ });
1453
+ }
1293
1454
  handleSocketUnavailable(socket2, reason) {
1294
1455
  if (this.socket !== socket2) {
1295
1456
  return;
@@ -1319,6 +1480,7 @@ var DriveSocket = class {
1319
1480
  async sendHello() {
1320
1481
  const manifest = chrome.runtime.getManifest();
1321
1482
  const endpoint = await readCoreEndpointConfig();
1483
+ const debuggerCapabilityEnabled = await readDebuggerCapabilityEnabled();
1322
1484
  let tabs = [];
1323
1485
  try {
1324
1486
  tabs = await queryTabs();
@@ -1328,6 +1490,8 @@ var DriveSocket = class {
1328
1490
  }
1329
1491
  const params = {
1330
1492
  version: manifest.version,
1493
+ protocol_version: DRIVE_WS_PROTOCOL_VERSION,
1494
+ capabilities: buildNegotiatedCapabilities(debuggerCapabilityEnabled),
1331
1495
  core_host: endpoint.host,
1332
1496
  core_port: endpoint.port,
1333
1497
  core_port_source: endpoint.portSource,
@@ -1402,6 +1566,19 @@ var DriveSocket = class {
1402
1566
  });
1403
1567
  }
1404
1568
  }
1569
+ async refreshDebuggerCapabilityState() {
1570
+ const enabled = await readDebuggerCapabilityEnabled();
1571
+ if (!enabled) {
1572
+ await this.detachAllDebuggerSessions();
1573
+ }
1574
+ this.refreshCapabilities();
1575
+ }
1576
+ async handleDebuggerCapabilityChange(enabled) {
1577
+ if (!enabled) {
1578
+ await this.detachAllDebuggerSessions();
1579
+ }
1580
+ this.refreshCapabilities();
1581
+ }
1405
1582
  async handleRequest(message) {
1406
1583
  let driveMessage = null;
1407
1584
  let gatedSiteKey = null;
@@ -1440,6 +1617,15 @@ var DriveSocket = class {
1440
1617
  return;
1441
1618
  }
1442
1619
  if (message.action.startsWith("debugger.")) {
1620
+ if (!await readDebuggerCapabilityEnabled()) {
1621
+ this.sendMessage({
1622
+ id: message.id,
1623
+ action: message.action,
1624
+ status: "error",
1625
+ error: sanitizeDriveErrorInfo(debuggerCapabilityDisabledError())
1626
+ });
1627
+ return;
1628
+ }
1443
1629
  await this.handleDebuggerRequest(message);
1444
1630
  return;
1445
1631
  }
@@ -1451,8 +1637,6 @@ var DriveSocket = class {
1451
1637
  "drive.navigate",
1452
1638
  "drive.go_back",
1453
1639
  "drive.go_forward",
1454
- "drive.back",
1455
- "drive.forward",
1456
1640
  "drive.click",
1457
1641
  "drive.hover",
1458
1642
  "drive.select",
@@ -1629,29 +1813,42 @@ var DriveSocket = class {
1629
1813
  tabId = await getDefaultTabId();
1630
1814
  }
1631
1815
  const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
1816
+ const domContentLoadedSignal = waitMode === "domcontentloaded" ? waitForDomContentLoaded(tabId, 3e4) : null;
1817
+ const warnings = [];
1632
1818
  await wrapChromeVoid(
1633
1819
  (callback) => chrome.tabs.update(tabId, { url }, () => callback())
1634
1820
  );
1635
1821
  markTabActive(tabId);
1636
- if (waitMode === "domcontentloaded") {
1822
+ if (domContentLoadedSignal) {
1637
1823
  try {
1638
- await waitForDomContentLoaded(tabId, 3e4);
1824
+ await domContentLoadedSignal;
1639
1825
  } catch (error) {
1640
- respondError({
1641
- code: "TIMEOUT",
1642
- message: error instanceof Error ? error.message : "Timed out waiting.",
1643
- retryable: true
1644
- });
1645
- return;
1826
+ const tab = await getTab(tabId).catch(() => void 0);
1827
+ if (tab && isLikelyNavigationCommitted(url, tab.url ?? void 0)) {
1828
+ warnings.push(
1829
+ "Timed out waiting for DOMContentLoaded, but the tab URL already updated to the requested target."
1830
+ );
1831
+ } else {
1832
+ respondError({
1833
+ code: "TIMEOUT",
1834
+ message: error instanceof Error ? error.message : "Timed out waiting.",
1835
+ retryable: true
1836
+ });
1837
+ return;
1838
+ }
1646
1839
  }
1647
1840
  }
1648
- respondOk({ ok: true });
1841
+ if (tabId === agentTabId) {
1842
+ void refreshAgentTabBranding(tabId);
1843
+ }
1844
+ respondOk({
1845
+ ok: true,
1846
+ ...warnings.length > 0 ? { warnings } : {}
1847
+ });
1649
1848
  return;
1650
1849
  }
1651
1850
  case "drive.go_back":
1652
- case "drive.back":
1653
- case "drive.go_forward":
1654
- case "drive.forward": {
1851
+ case "drive.go_forward": {
1655
1852
  const params = message.params ?? {};
1656
1853
  let tabId = params.tab_id;
1657
1854
  if (tabId !== void 0 && typeof tabId !== "number") {
@@ -1693,11 +1890,18 @@ var DriveSocket = class {
1693
1890
  }
1694
1891
  } catch {
1695
1892
  if (!result.ok) {
1696
- respondError(result.error);
1893
+ respondError({
1894
+ ...result.error
1895
+ });
1697
1896
  return;
1698
1897
  }
1699
1898
  }
1700
- respondOk({ ok: true });
1899
+ if (tabId === agentTabId) {
1900
+ void refreshAgentTabBranding(tabId);
1901
+ }
1902
+ respondOk({
1903
+ ok: true
1904
+ });
1701
1905
  return;
1702
1906
  }
1703
1907
  case "drive.tab_list": {
@@ -3082,6 +3286,9 @@ var DriveSocket = class {
3082
3286
  }
3083
3287
  }
3084
3288
  async handleDebuggerEvent(source, method, params) {
3289
+ if (!await readDebuggerCapabilityEnabled()) {
3290
+ return;
3291
+ }
3085
3292
  const tabId = source.tabId;
3086
3293
  if (typeof tabId !== "number") {
3087
3294
  return;
@@ -3100,6 +3307,9 @@ var DriveSocket = class {
3100
3307
  return;
3101
3308
  }
3102
3309
  this.clearDebuggerSession(tabId);
3310
+ if (!await readDebuggerCapabilityEnabled()) {
3311
+ return;
3312
+ }
3103
3313
  this.sendDebuggerEvent({
3104
3314
  tab_id: tabId,
3105
3315
  method: "Debugger.detached",
@@ -3238,6 +3448,15 @@ var DriveSocket = class {
3238
3448
  }
3239
3449
  return null;
3240
3450
  }
3451
+ async detachAllDebuggerSessions() {
3452
+ const tabIds = Array.from(this.debuggerSessions.keys());
3453
+ for (const tabId of tabIds) {
3454
+ const error = await this.detachDebugger(tabId);
3455
+ if (error) {
3456
+ console.warn("DriveSocket detachDebugger failed:", tabId, error);
3457
+ }
3458
+ }
3459
+ }
3241
3460
  async sendDebuggerCommand(tabId, method, params, timeoutMs) {
3242
3461
  return await new Promise((resolve, reject) => {
3243
3462
  let finished = false;
@@ -3368,14 +3587,53 @@ chrome.debugger.onDetach.addListener(
3368
3587
  );
3369
3588
  chrome.runtime.onMessage.addListener(
3370
3589
  (message, _sender, sendResponse) => {
3371
- if (!message || typeof message !== "object" || message.action !== "drive.connection_status") {
3590
+ if (!message || typeof message !== "object") {
3372
3591
  return void 0;
3373
3592
  }
3374
- sendResponse({
3375
- ok: true,
3376
- result: socket.getConnectionStatus()
3593
+ const action = message.action;
3594
+ if (action === "drive.connection_status") {
3595
+ sendResponse({
3596
+ ok: true,
3597
+ result: socket.getConnectionStatus()
3598
+ });
3599
+ return true;
3600
+ }
3601
+ if (action === "drive.refresh_capabilities") {
3602
+ void socket.refreshDebuggerCapabilityState().then(() => {
3603
+ sendResponse({ ok: true, result: { refreshed: true } });
3604
+ }).catch((error) => {
3605
+ const message2 = error instanceof Error ? error.message : "Failed to refresh capabilities.";
3606
+ sendResponse({ ok: false, error: { message: message2 } });
3607
+ });
3608
+ return true;
3609
+ }
3610
+ return void 0;
3611
+ }
3612
+ );
3613
+ chrome.storage.onChanged.addListener(
3614
+ (changes, areaName) => {
3615
+ if (areaName !== "local") {
3616
+ return;
3617
+ }
3618
+ const debuggerChange = changes[DEBUGGER_CAPABILITY_ENABLED_KEY];
3619
+ if (!debuggerChange) {
3620
+ return;
3621
+ }
3622
+ if (typeof debuggerChange.newValue === "boolean") {
3623
+ void socket.handleDebuggerCapabilityChange(debuggerChange.newValue).catch((error) => {
3624
+ console.error(
3625
+ "DriveSocket handleDebuggerCapabilityChange failed:",
3626
+ error
3627
+ );
3628
+ });
3629
+ return;
3630
+ }
3631
+ void socket.refreshDebuggerCapabilityState().catch((error) => {
3632
+ console.error(
3633
+ "DriveSocket refreshDebuggerCapabilityState failed:",
3634
+ error
3635
+ );
3377
3636
  });
3378
- return true;
3379
3637
  }
3380
3638
  );
3381
3639
  socket.start();