@opentabs-dev/browser-extension 0.0.34 → 0.0.36

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.
Files changed (82) hide show
  1. package/dist/background-message-handlers.d.ts.map +1 -1
  2. package/dist/background-message-handlers.js +17 -6
  3. package/dist/background-message-handlers.js.map +1 -1
  4. package/dist/background.js +339 -103
  5. package/dist/background.js.map +1 -1
  6. package/dist/browser-commands/cookie-commands.js +2 -2
  7. package/dist/browser-commands/cookie-commands.js.map +1 -1
  8. package/dist/browser-commands/extension-commands.d.ts +2 -0
  9. package/dist/browser-commands/extension-commands.d.ts.map +1 -1
  10. package/dist/browser-commands/extension-commands.js +75 -43
  11. package/dist/browser-commands/extension-commands.js.map +1 -1
  12. package/dist/browser-commands/index.d.ts +1 -1
  13. package/dist/browser-commands/index.d.ts.map +1 -1
  14. package/dist/browser-commands/index.js +1 -1
  15. package/dist/browser-commands/index.js.map +1 -1
  16. package/dist/browser-commands/interaction-commands.d.ts.map +1 -1
  17. package/dist/browser-commands/interaction-commands.js +24 -14
  18. package/dist/browser-commands/interaction-commands.js.map +1 -1
  19. package/dist/browser-commands/network-commands.d.ts +1 -0
  20. package/dist/browser-commands/network-commands.d.ts.map +1 -1
  21. package/dist/browser-commands/network-commands.js +16 -2
  22. package/dist/browser-commands/network-commands.js.map +1 -1
  23. package/dist/browser-commands/tab-commands.js +1 -1
  24. package/dist/browser-commands/tab-commands.js.map +1 -1
  25. package/dist/confirmation-badge.d.ts +26 -4
  26. package/dist/confirmation-badge.d.ts.map +1 -1
  27. package/dist/confirmation-badge.js +108 -7
  28. package/dist/confirmation-badge.js.map +1 -1
  29. package/dist/iife-injection.d.ts +1 -6
  30. package/dist/iife-injection.d.ts.map +1 -1
  31. package/dist/iife-injection.js +9 -34
  32. package/dist/iife-injection.js.map +1 -1
  33. package/dist/known-methods.d.ts +2 -2
  34. package/dist/known-methods.d.ts.map +1 -1
  35. package/dist/known-methods.js +1 -0
  36. package/dist/known-methods.js.map +1 -1
  37. package/dist/message-router.d.ts.map +1 -1
  38. package/dist/message-router.js +2 -1
  39. package/dist/message-router.js.map +1 -1
  40. package/dist/network-capture.d.ts +14 -1
  41. package/dist/network-capture.d.ts.map +1 -1
  42. package/dist/network-capture.js +167 -23
  43. package/dist/network-capture.js.map +1 -1
  44. package/dist/offscreen/index.d.ts +1 -1
  45. package/dist/offscreen/index.js +41 -24
  46. package/dist/offscreen/index.js.map +1 -1
  47. package/dist/offscreen/ws-utils.d.ts +8 -0
  48. package/dist/offscreen/ws-utils.d.ts.map +1 -0
  49. package/dist/offscreen/ws-utils.js +30 -0
  50. package/dist/offscreen/ws-utils.js.map +1 -0
  51. package/dist/rate-limiter.d.ts +2 -0
  52. package/dist/rate-limiter.d.ts.map +1 -1
  53. package/dist/rate-limiter.js +6 -0
  54. package/dist/rate-limiter.js.map +1 -1
  55. package/dist/sanitize-svg.d.ts +1 -1
  56. package/dist/sanitize-svg.js +1 -1
  57. package/dist/side-panel/App.d.ts.map +1 -1
  58. package/dist/side-panel/App.js +16 -2
  59. package/dist/side-panel/App.js.map +1 -1
  60. package/dist/side-panel/components/ConfirmationDialog.d.ts +8 -1
  61. package/dist/side-panel/components/ConfirmationDialog.d.ts.map +1 -1
  62. package/dist/side-panel/components/ConfirmationDialog.js +16 -8
  63. package/dist/side-panel/components/ConfirmationDialog.js.map +1 -1
  64. package/dist/side-panel/components/PluginCard.d.ts.map +1 -1
  65. package/dist/side-panel/components/PluginCard.js +10 -2
  66. package/dist/side-panel/components/PluginCard.js.map +1 -1
  67. package/dist/side-panel/constants.d.ts +2 -0
  68. package/dist/side-panel/constants.d.ts.map +1 -1
  69. package/dist/side-panel/constants.js +2 -0
  70. package/dist/side-panel/constants.js.map +1 -1
  71. package/dist/side-panel/hooks/useServerNotifications.d.ts.map +1 -1
  72. package/dist/side-panel/hooks/useServerNotifications.js +27 -3
  73. package/dist/side-panel/hooks/useServerNotifications.js.map +1 -1
  74. package/dist/side-panel/side-panel.js +4336 -4112
  75. package/dist/side-panel/styles.css +1 -1
  76. package/dist/side-panel-toggle.d.ts.map +1 -1
  77. package/dist/side-panel-toggle.js +21 -6
  78. package/dist/side-panel-toggle.js.map +1 -1
  79. package/dist/tab-state.d.ts.map +1 -1
  80. package/dist/tab-state.js +14 -4
  81. package/dist/tab-state.js.map +1 -1
  82. package/package.json +17 -17
@@ -1,5 +1,9 @@
1
1
  // dist/confirmation-badge.js
2
2
  var pendingConfirmationCount = 0;
3
+ var confirmationTimeouts = /* @__PURE__ */ new Map();
4
+ var clearedConfirmationIds = /* @__PURE__ */ new Set();
5
+ var CONFIRMATION_BACKGROUND_TIMEOUT_BUFFER_MS = 2e3;
6
+ var CONFIRMATION_FALLBACK_TIMEOUT_MS = 3e4;
3
7
  var updateConfirmationBadge = () => {
4
8
  if (pendingConfirmationCount > 0) {
5
9
  chrome.action.setBadgeText({ text: String(pendingConfirmationCount) }).catch(() => {
@@ -12,11 +16,26 @@ var updateConfirmationBadge = () => {
12
16
  }
13
17
  };
14
18
  var notifyConfirmationRequest = (params) => {
15
- pendingConfirmationCount++;
16
- updateConfirmationBadge();
17
19
  const tool = typeof params.tool === "string" ? params.tool : "unknown tool";
18
20
  const domain = typeof params.domain === "string" ? params.domain : "unknown domain";
19
- chrome.notifications.create(`opentabs-confirm-${typeof params.id === "string" ? params.id : Date.now()}`, {
21
+ const id = typeof params.id === "string" ? params.id : String(Date.now());
22
+ const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 0;
23
+ const existingTimeoutId = confirmationTimeouts.get(id);
24
+ if (existingTimeoutId !== void 0) {
25
+ clearTimeout(existingTimeoutId);
26
+ } else {
27
+ clearedConfirmationIds.delete(id);
28
+ pendingConfirmationCount++;
29
+ updateConfirmationBadge();
30
+ }
31
+ const effectiveTimeoutMs = timeoutMs > 0 ? timeoutMs : CONFIRMATION_FALLBACK_TIMEOUT_MS;
32
+ const bgTimeoutId = setTimeout(() => {
33
+ confirmationTimeouts.delete(id);
34
+ clearConfirmationBadge(id);
35
+ clearedConfirmationIds.delete(id);
36
+ }, effectiveTimeoutMs + CONFIRMATION_BACKGROUND_TIMEOUT_BUFFER_MS);
37
+ confirmationTimeouts.set(id, bgTimeoutId);
38
+ chrome.notifications.create(`opentabs-confirm-${id}`, {
20
39
  type: "basic",
21
40
  iconUrl: chrome.runtime.getURL("icons/icon-128.png"),
22
41
  title: "OpenTabs: Approval Required",
@@ -26,11 +45,37 @@ var notifyConfirmationRequest = (params) => {
26
45
  }).catch(() => {
27
46
  });
28
47
  };
29
- var clearConfirmationBadge = () => {
48
+ var clearConfirmationBadge = (id) => {
49
+ if (id !== void 0) {
50
+ if (clearedConfirmationIds.has(id)) {
51
+ return;
52
+ }
53
+ clearedConfirmationIds.add(id);
54
+ chrome.notifications.clear(`opentabs-confirm-${id}`).catch(() => {
55
+ });
56
+ }
30
57
  pendingConfirmationCount = Math.max(0, pendingConfirmationCount - 1);
31
58
  updateConfirmationBadge();
59
+ if (id !== void 0 && !confirmationTimeouts.has(id)) {
60
+ clearedConfirmationIds.delete(id);
61
+ }
62
+ };
63
+ var clearConfirmationBackgroundTimeout = (id) => {
64
+ const timeoutId = confirmationTimeouts.get(id);
65
+ if (timeoutId !== void 0) {
66
+ clearTimeout(timeoutId);
67
+ confirmationTimeouts.delete(id);
68
+ clearedConfirmationIds.delete(id);
69
+ }
32
70
  };
33
71
  var clearAllConfirmationBadges = () => {
72
+ for (const [id, timeoutId] of confirmationTimeouts.entries()) {
73
+ clearTimeout(timeoutId);
74
+ chrome.notifications.clear(`opentabs-confirm-${id}`).catch(() => {
75
+ });
76
+ }
77
+ confirmationTimeouts.clear();
78
+ clearedConfirmationIds.clear();
34
79
  pendingConfirmationCount = 0;
35
80
  updateConfirmationBadge();
36
81
  };
@@ -465,8 +510,8 @@ var handleBrowserDeleteCookies = async (params, id) => {
465
510
  const name = requireStringParam(params, "name", id);
466
511
  if (name === null)
467
512
  return;
468
- await chrome.cookies.remove({ url, name });
469
- sendSuccessResult(id, { deleted: true, name, url });
513
+ const result = await chrome.cookies.remove({ url, name });
514
+ sendSuccessResult(id, { deleted: result !== null, name, url });
470
515
  } catch (err2) {
471
516
  sendErrorResult(id, err2);
472
517
  }
@@ -565,6 +610,9 @@ var bgLogCollector = installLogCollector("background");
565
610
 
566
611
  // dist/network-capture.js
567
612
  var MAX_BODY_LENGTH = 102400;
613
+ var PENDING_REQUEST_TTL_MS = 6e4;
614
+ var PRUNE_INTERVAL_MS = 3e4;
615
+ var WS_TTL_MS = 5 * 6e4;
568
616
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
569
617
  "authorization",
570
618
  "cookie",
@@ -596,6 +644,7 @@ var scrubHeaders = (headers) => {
596
644
  return scrubbed;
597
645
  };
598
646
  var captures = /* @__PURE__ */ new Map();
647
+ var pendingCaptures = /* @__PURE__ */ new Map();
599
648
  var headersToRecord = (raw) => {
600
649
  if (!raw)
601
650
  return void 0;
@@ -652,6 +701,23 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
652
701
  const request = paramsRecord?.request;
653
702
  if (!requestId || !request)
654
703
  return;
704
+ const now = Date.now();
705
+ for (const [id, pending] of state.pendingRequests) {
706
+ if (pending.timestamp !== void 0 && now - pending.timestamp > PENDING_REQUEST_TTL_MS) {
707
+ state.pendingRequests.delete(id);
708
+ }
709
+ }
710
+ for (const [id, req] of state.requestIdToRequest) {
711
+ if (now - req.timestamp > PENDING_REQUEST_TTL_MS) {
712
+ state.requestIdToRequest.delete(id);
713
+ }
714
+ }
715
+ for (const [id, createdAt] of state.wsCreatedAt) {
716
+ if (now - createdAt > WS_TTL_MS) {
717
+ state.wsFramesByRequestId.delete(id);
718
+ state.wsCreatedAt.delete(id);
719
+ }
720
+ }
655
721
  const url = stringProp(request, "url", "");
656
722
  if (state.urlFilter && !url.includes(state.urlFilter))
657
723
  return;
@@ -727,15 +793,22 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
727
793
  const responseData = result;
728
794
  if (typeof responseData.body !== "string")
729
795
  return;
796
+ if (!state.requests.includes(request))
797
+ return;
730
798
  const body = responseData.base64Encoded ? new TextDecoder().decode(Uint8Array.from(atob(responseData.body), (c) => c.charCodeAt(0))) : responseData.body;
731
799
  request.responseBody = truncateBody(body);
732
800
  });
733
801
  } else if (method === "Network.webSocketCreated") {
734
802
  const url = paramsRecord?.url;
803
+ const requestId = paramsRecord?.requestId;
735
804
  if (!url)
736
805
  return;
737
806
  if (state.urlFilter && !url.includes(state.urlFilter))
738
807
  return;
808
+ if (requestId) {
809
+ state.wsFramesByRequestId.set(requestId, url);
810
+ state.wsCreatedAt.set(requestId, Date.now());
811
+ }
739
812
  const completed = {
740
813
  url,
741
814
  method: "GET",
@@ -748,6 +821,28 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
748
821
  evictOldestRequest(state);
749
822
  }
750
823
  state.requests.push(completed);
824
+ } else if (method === "Network.webSocketFrameSent" || method === "Network.webSocketFrameReceived") {
825
+ const requestId = paramsRecord?.requestId;
826
+ const response = paramsRecord?.response;
827
+ if (!requestId || !response)
828
+ return;
829
+ const url = state.wsFramesByRequestId.get(requestId);
830
+ if (!url)
831
+ return;
832
+ const opcode = typeof response.opcode === "number" ? response.opcode : 1;
833
+ const payloadData = typeof response.payloadData === "string" ? response.payloadData : "";
834
+ const direction = method === "Network.webSocketFrameSent" ? "sent" : "received";
835
+ const data = truncateBody(payloadData);
836
+ if (state.wsFrames.length >= state.maxWsFrames) {
837
+ state.wsFrames.shift();
838
+ }
839
+ state.wsFrames.push({ url, direction, data, opcode, timestamp: Date.now() });
840
+ } else if (method === "Network.webSocketClosed") {
841
+ const requestId = paramsRecord?.requestId;
842
+ if (requestId) {
843
+ state.wsFramesByRequestId.delete(requestId);
844
+ state.wsCreatedAt.delete(requestId);
845
+ }
751
846
  } else if (method === "Runtime.consoleAPICalled") {
752
847
  const type = paramsRecord?.type;
753
848
  const args = paramsRecord?.args;
@@ -774,43 +869,93 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
774
869
  }
775
870
  });
776
871
  chrome.tabs.onRemoved.addListener((tabId) => {
777
- if (captures.has(tabId)) {
872
+ const state = captures.get(tabId);
873
+ if (state) {
874
+ clearInterval(state.pruneIntervalId);
778
875
  void chrome.debugger.detach({ tabId }).catch(() => {
779
876
  });
780
877
  captures.delete(tabId);
781
878
  }
782
879
  });
783
- var startCapture = async (tabId, maxRequests = 100, urlFilter, maxConsoleLogs = 500) => {
880
+ chrome.debugger.onDetach.addListener((source, _reason) => {
881
+ const tabId = source.tabId;
882
+ if (tabId !== void 0) {
883
+ const state = captures.get(tabId);
884
+ if (state)
885
+ clearInterval(state.pruneIntervalId);
886
+ captures.delete(tabId);
887
+ }
888
+ });
889
+ var startCapture = async (tabId, maxRequests = 100, urlFilter, maxConsoleLogs = 500, maxWsFrames = 200) => {
890
+ const inFlightCapture = pendingCaptures.get(tabId);
891
+ if (inFlightCapture) {
892
+ return inFlightCapture;
893
+ }
784
894
  if (captures.has(tabId)) {
785
895
  throw new Error(`Network capture already active for tab ${tabId}. Call stopCapture first.`);
786
896
  }
897
+ const capturePromise = (async () => {
898
+ try {
899
+ await chrome.debugger.attach({ tabId }, CDP_VERSION);
900
+ } catch (err2) {
901
+ throw new Error(`Failed to attach debugger to tab ${tabId}: ${toErrorMessage(err2)}. Another debugger (e.g., DevTools) may already be attached.`);
902
+ }
903
+ try {
904
+ await chrome.debugger.sendCommand({ tabId }, "Network.enable");
905
+ await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
906
+ } catch (err2) {
907
+ await chrome.debugger.detach({ tabId }).catch(() => {
908
+ });
909
+ throw err2;
910
+ }
911
+ const captureState = {
912
+ requests: [],
913
+ consoleLogs: [],
914
+ wsFrames: [],
915
+ maxRequests,
916
+ maxConsoleLogs,
917
+ maxWsFrames,
918
+ urlFilter,
919
+ pendingRequests: /* @__PURE__ */ new Map(),
920
+ requestIdToRequest: /* @__PURE__ */ new Map(),
921
+ wsFramesByRequestId: /* @__PURE__ */ new Map(),
922
+ wsCreatedAt: /* @__PURE__ */ new Map()
923
+ };
924
+ captureState.pruneIntervalId = setInterval(() => {
925
+ const now = Date.now();
926
+ for (const [id, pendingReq] of captureState.pendingRequests) {
927
+ if (pendingReq.timestamp !== void 0 && now - pendingReq.timestamp > PENDING_REQUEST_TTL_MS) {
928
+ captureState.pendingRequests.delete(id);
929
+ }
930
+ }
931
+ for (const [id, req] of captureState.requestIdToRequest) {
932
+ if (now - req.timestamp > PENDING_REQUEST_TTL_MS) {
933
+ captureState.requestIdToRequest.delete(id);
934
+ }
935
+ }
936
+ for (const [id, createdAt] of captureState.wsCreatedAt) {
937
+ if (now - createdAt > WS_TTL_MS) {
938
+ captureState.wsFramesByRequestId.delete(id);
939
+ captureState.wsCreatedAt.delete(id);
940
+ }
941
+ }
942
+ }, PRUNE_INTERVAL_MS);
943
+ captures.set(tabId, captureState);
944
+ })();
945
+ pendingCaptures.set(tabId, capturePromise);
787
946
  try {
788
- await chrome.debugger.attach({ tabId }, CDP_VERSION);
789
- } catch (err2) {
790
- throw new Error(`Failed to attach debugger to tab ${tabId}: ${toErrorMessage(err2)}. Another debugger (e.g., DevTools) may already be attached.`);
947
+ await capturePromise;
948
+ } finally {
949
+ pendingCaptures.delete(tabId);
791
950
  }
792
- try {
793
- await chrome.debugger.sendCommand({ tabId }, "Network.enable");
794
- await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
795
- } catch (err2) {
796
- await chrome.debugger.detach({ tabId }).catch(() => {
797
- });
798
- throw err2;
799
- }
800
- captures.set(tabId, {
801
- requests: [],
802
- consoleLogs: [],
803
- maxRequests,
804
- maxConsoleLogs,
805
- urlFilter,
806
- pendingRequests: /* @__PURE__ */ new Map(),
807
- requestIdToRequest: /* @__PURE__ */ new Map()
808
- });
809
951
  };
810
952
  var stopCapture = (tabId) => {
811
953
  const state = captures.get(tabId);
812
954
  if (!state)
813
955
  return;
956
+ clearInterval(state.pruneIntervalId);
957
+ state.wsFramesByRequestId.clear();
958
+ state.wsCreatedAt.clear();
814
959
  void chrome.debugger.detach({ tabId }).catch(() => {
815
960
  });
816
961
  captures.delete(tabId);
@@ -850,6 +995,16 @@ var clearConsoleLogs = (tabId) => {
850
995
  state.consoleLogs = [];
851
996
  }
852
997
  };
998
+ var getWsFrames = (tabId, clear = false) => {
999
+ const state = captures.get(tabId);
1000
+ if (!state)
1001
+ return [];
1002
+ const frames = [...state.wsFrames];
1003
+ if (clear) {
1004
+ state.wsFrames = [];
1005
+ }
1006
+ return frames;
1007
+ };
853
1008
  var getActiveCapturesSummary = () => Array.from(captures.entries()).map(([tabId, state]) => ({
854
1009
  tabId,
855
1010
  requestCount: state.requests.length,
@@ -1031,11 +1186,17 @@ var lastKnownState = /* @__PURE__ */ new Map();
1031
1186
  var pluginLocks = /* @__PURE__ */ new Map();
1032
1187
  var withPluginLock = (pluginName, fn) => {
1033
1188
  const prev = pluginLocks.get(pluginName) ?? Promise.resolve();
1034
- const next = prev.then(fn);
1035
- pluginLocks.set(pluginName, next.catch((err2) => {
1189
+ const operation = prev.then(fn);
1190
+ const lock = operation.catch((err2) => {
1036
1191
  console.warn("[opentabs] tab state operation failed for plugin", pluginName, ":", err2);
1037
- }));
1038
- return next;
1192
+ });
1193
+ pluginLocks.set(pluginName, lock);
1194
+ void lock.then(() => {
1195
+ if (pluginLocks.get(pluginName) === lock) {
1196
+ pluginLocks.set(pluginName, Promise.resolve());
1197
+ }
1198
+ });
1199
+ return operation;
1039
1200
  };
1040
1201
  var probeTabReadiness = async (tabId, pluginName) => {
1041
1202
  let timerId;
@@ -1294,10 +1455,18 @@ var handleExtensionGetLogs = async (params, id) => {
1294
1455
  };
1295
1456
  var handleExtensionGetSidePanel = async (id) => {
1296
1457
  try {
1297
- const sidePanelResult = await Promise.race([
1298
- chrome.runtime.sendMessage({ type: "sp:getState" }).then((raw) => raw),
1299
- new Promise((resolve) => setTimeout(() => resolve(null), SIDE_PANEL_TIMEOUT_MS))
1300
- ]);
1458
+ let timeoutId;
1459
+ let sidePanelResult;
1460
+ try {
1461
+ sidePanelResult = await Promise.race([
1462
+ chrome.runtime.sendMessage({ type: "sp:getState" }).then((raw) => raw),
1463
+ new Promise((resolve) => {
1464
+ timeoutId = setTimeout(() => resolve(null), SIDE_PANEL_TIMEOUT_MS);
1465
+ })
1466
+ ]);
1467
+ } finally {
1468
+ clearTimeout(timeoutId);
1469
+ }
1301
1470
  if (!sidePanelResult || typeof sidePanelResult !== "object") {
1302
1471
  sendSuccessResult(id, { open: false });
1303
1472
  return;
@@ -1376,21 +1545,29 @@ var handleExtensionCheckAdapter = async (params, id) => {
1376
1545
  }
1377
1546
  let isReady = false;
1378
1547
  try {
1379
- const readyResults = await Promise.race([
1380
- chrome.scripting.executeScript({
1381
- target: { tabId },
1382
- world: "MAIN",
1383
- func: async (pName) => {
1384
- const ot = globalThis.__openTabs;
1385
- const adapter = ot?.adapters?.[pName];
1386
- if (!adapter || typeof adapter.isReady !== "function")
1387
- return false;
1388
- return await adapter.isReady();
1389
- },
1390
- args: [pluginName]
1391
- }),
1392
- new Promise((resolve) => setTimeout(() => resolve(null), IS_READY_TIMEOUT_MS))
1393
- ]);
1548
+ let isReadyTimeoutId;
1549
+ let readyResults;
1550
+ try {
1551
+ readyResults = await Promise.race([
1552
+ chrome.scripting.executeScript({
1553
+ target: { tabId },
1554
+ world: "MAIN",
1555
+ func: async (pName) => {
1556
+ const ot = globalThis.__openTabs;
1557
+ const adapter = ot?.adapters?.[pName];
1558
+ if (!adapter || typeof adapter.isReady !== "function")
1559
+ return false;
1560
+ return await adapter.isReady();
1561
+ },
1562
+ args: [pluginName]
1563
+ }),
1564
+ new Promise((resolve) => {
1565
+ isReadyTimeoutId = setTimeout(() => resolve(null), IS_READY_TIMEOUT_MS);
1566
+ })
1567
+ ]);
1568
+ } finally {
1569
+ clearTimeout(isReadyTimeoutId);
1570
+ }
1394
1571
  if (readyResults !== null) {
1395
1572
  const readyResult = readyResults[0];
1396
1573
  isReady = readyResult?.result === true;
@@ -1446,7 +1623,11 @@ var handleBrowserExecuteScript = async (params, id) => {
1446
1623
  sendValidationError(id, "Invalid execFile format");
1447
1624
  return;
1448
1625
  }
1626
+ const execUuid = execFile.replace(/^__exec-/, "").replace(/\.js$/, "");
1627
+ const resultKey = `__execResult_${execUuid}`;
1628
+ const asyncKey = `__execAsync_${execUuid}`;
1449
1629
  let timeoutId;
1630
+ const cancelled = { value: false };
1450
1631
  const injectPromise = (async () => {
1451
1632
  await chrome.scripting.executeScript({
1452
1633
  target: { tabId },
@@ -1455,15 +1636,17 @@ var handleBrowserExecuteScript = async (params, id) => {
1455
1636
  });
1456
1637
  let elapsed = 0;
1457
1638
  while (elapsed <= EXEC_MAX_ASYNC_WAIT_MS) {
1639
+ if (cancelled.value)
1640
+ return;
1458
1641
  const results = await chrome.scripting.executeScript({
1459
1642
  target: { tabId },
1460
1643
  world: "MAIN",
1461
- func: (truncLimit) => {
1644
+ func: (truncLimit, rKey, aKey) => {
1462
1645
  const ot = globalThis.__openTabs;
1463
1646
  if (!ot)
1464
1647
  return { pending: false, result: { error: "__openTabs not found" } };
1465
- const result2 = ot.__lastExecResult;
1466
- const isAsync = ot.__lastExecAsync === true;
1648
+ const result2 = ot[rKey];
1649
+ const isAsync = ot[aKey] === true;
1467
1650
  if (result2 && ("value" in result2 || "error" in result2)) {
1468
1651
  const captured = { ...result2 };
1469
1652
  if (captured.value === void 0)
@@ -1476,15 +1659,15 @@ var handleBrowserExecuteScript = async (params, id) => {
1476
1659
  captured.value = String(captured.value);
1477
1660
  }
1478
1661
  }
1479
- delete ot.__lastExecResult;
1480
- delete ot.__lastExecAsync;
1662
+ Reflect.deleteProperty(ot, rKey);
1663
+ Reflect.deleteProperty(ot, aKey);
1481
1664
  return { pending: false, result: captured };
1482
1665
  }
1483
1666
  if (isAsync)
1484
1667
  return { pending: true };
1485
1668
  return { pending: false, result: { error: "No result captured" } };
1486
1669
  },
1487
- args: [EXEC_RESULT_TRUNCATION_LIMIT]
1670
+ args: [EXEC_RESULT_TRUNCATION_LIMIT, resultKey, asyncKey]
1488
1671
  });
1489
1672
  const first = results[0];
1490
1673
  const data = first?.result;
@@ -1494,18 +1677,21 @@ var handleBrowserExecuteScript = async (params, id) => {
1494
1677
  await new Promise((resolve) => setTimeout(resolve, EXEC_POLL_INTERVAL_MS));
1495
1678
  elapsed += EXEC_POLL_INTERVAL_MS;
1496
1679
  }
1497
- await chrome.scripting.executeScript({
1498
- target: { tabId },
1499
- world: "MAIN",
1500
- func: () => {
1501
- const ot = globalThis.__openTabs;
1502
- if (ot) {
1503
- delete ot.__lastExecResult;
1504
- delete ot.__lastExecAsync;
1505
- }
1506
- }
1507
- }).catch(() => {
1508
- });
1680
+ if (!cancelled.value) {
1681
+ await chrome.scripting.executeScript({
1682
+ target: { tabId },
1683
+ world: "MAIN",
1684
+ func: (rKey, aKey) => {
1685
+ const ot = globalThis.__openTabs;
1686
+ if (ot) {
1687
+ Reflect.deleteProperty(ot, rKey);
1688
+ Reflect.deleteProperty(ot, aKey);
1689
+ }
1690
+ },
1691
+ args: [resultKey, asyncKey]
1692
+ }).catch(() => {
1693
+ });
1694
+ }
1509
1695
  return { value: { error: `Async code did not resolve within ${EXEC_MAX_ASYNC_WAIT_MS}ms` } };
1510
1696
  })();
1511
1697
  const timeoutPromise = new Promise((_resolve, reject) => {
@@ -1518,6 +1704,7 @@ var handleBrowserExecuteScript = async (params, id) => {
1518
1704
  result = await Promise.race([injectPromise, timeoutPromise]);
1519
1705
  } finally {
1520
1706
  clearTimeout(timeoutId);
1707
+ cancelled.value = true;
1521
1708
  }
1522
1709
  sendSuccessResult(id, result);
1523
1710
  } catch (err2) {
@@ -1824,24 +2011,30 @@ var handleBrowserWaitForElement = async (params, id) => {
1824
2011
  func: (sel, tmo, vis, maxPreview, pollMs) => new Promise((resolve) => {
1825
2012
  let elapsed = 0;
1826
2013
  const poll = setInterval(() => {
1827
- const el = document.querySelector(sel);
1828
- if (el) {
1829
- const htmlEl = el;
1830
- const isVisible = !vis || htmlEl.offsetParent !== null && getComputedStyle(htmlEl).display !== "none";
1831
- if (isVisible) {
2014
+ try {
2015
+ const el = document.querySelector(sel);
2016
+ if (el) {
2017
+ const htmlEl = el;
2018
+ const style = getComputedStyle(htmlEl);
2019
+ const isVisible = !vis || style.display !== "none" && style.visibility !== "hidden" && (htmlEl.offsetParent !== null || style.position === "fixed" || style.position === "sticky");
2020
+ if (isVisible) {
2021
+ clearInterval(poll);
2022
+ resolve({
2023
+ found: true,
2024
+ tagName: el.tagName.toLowerCase(),
2025
+ text: (el.textContent || "").trim().slice(0, maxPreview)
2026
+ });
2027
+ return;
2028
+ }
2029
+ }
2030
+ elapsed += pollMs;
2031
+ if (elapsed >= tmo) {
1832
2032
  clearInterval(poll);
1833
- resolve({
1834
- found: true,
1835
- tagName: el.tagName.toLowerCase(),
1836
- text: (el.textContent || "").trim().slice(0, maxPreview)
1837
- });
1838
- return;
2033
+ resolve({ error: `Timeout waiting for element: ${sel} (${tmo}ms)` });
1839
2034
  }
1840
- }
1841
- elapsed += pollMs;
1842
- if (elapsed >= tmo) {
2035
+ } catch (err2) {
1843
2036
  clearInterval(poll);
1844
- resolve({ error: `Timeout waiting for element: ${sel} (${tmo}ms)` });
2037
+ resolve({ error: `Error checking element: ${err2 instanceof Error ? err2.message : String(err2)}` });
1845
2038
  }
1846
2039
  }, pollMs);
1847
2040
  }),
@@ -2230,7 +2423,8 @@ var handleBrowserEnableNetworkCapture = async (params, id) => {
2230
2423
  const maxRequests = typeof params.maxRequests === "number" ? params.maxRequests : 100;
2231
2424
  const urlFilter = typeof params.urlFilter === "string" ? params.urlFilter : void 0;
2232
2425
  const maxConsoleLogs = typeof params.maxConsoleLogs === "number" ? params.maxConsoleLogs : 500;
2233
- await startCapture(tabId, maxRequests, urlFilter, maxConsoleLogs);
2426
+ const maxWsFrames = typeof params.maxWsFrames === "number" ? params.maxWsFrames : 200;
2427
+ await startCapture(tabId, maxRequests, urlFilter, maxConsoleLogs, maxWsFrames);
2234
2428
  sendSuccessResult(id, { enabled: true, tabId });
2235
2429
  } catch (err2) {
2236
2430
  sendErrorResult(id, err2);
@@ -2283,6 +2477,18 @@ var handleBrowserClearConsoleLogs = (params, id) => {
2283
2477
  sendErrorResult(id, err2);
2284
2478
  }
2285
2479
  };
2480
+ var handleBrowserGetWebSocketFrames = (params, id) => {
2481
+ try {
2482
+ const tabId = requireTabId(params, id);
2483
+ if (tabId === null)
2484
+ return;
2485
+ const clear = typeof params.clear === "boolean" ? params.clear : false;
2486
+ const frames = getWsFrames(tabId, clear);
2487
+ sendSuccessResult(id, { frames });
2488
+ } catch (err2) {
2489
+ sendErrorResult(id, err2);
2490
+ }
2491
+ };
2286
2492
 
2287
2493
  // dist/browser-commands/tab-commands.js
2288
2494
  var handleBrowserListTabs = async (id) => {
@@ -2331,7 +2537,7 @@ var handleBrowserNavigateTab = async (params, id) => {
2331
2537
  if (url === null)
2332
2538
  return;
2333
2539
  const tab = await chrome.tabs.update(tabId, { url });
2334
- sendSuccessResult(id, { id: tab?.id ?? tabId, title: tab?.title ?? "", url: tab?.url ?? url });
2540
+ sendSuccessResult(id, { id: tab?.id ?? tabId, title: tab?.title ?? "", url });
2335
2541
  } catch (err2) {
2336
2542
  sendErrorResult(id, err2);
2337
2543
  }
@@ -2449,8 +2655,10 @@ var injectLogRelay = async (tabId) => {
2449
2655
  const win = window;
2450
2656
  if (win[guard]) {
2451
2657
  const nonceSet = win.__opentabs_log_nonces;
2452
- if (nonceSet)
2658
+ if (nonceSet) {
2659
+ nonceSet.clear();
2453
2660
  nonceSet.add(n);
2661
+ }
2454
2662
  return;
2455
2663
  }
2456
2664
  win[guard] = true;
@@ -2702,6 +2910,9 @@ var checkRateLimit = (method, now = Date.now()) => {
2702
2910
  const config = METHOD_LIMITS.get(method) ?? DEFAULT_LIMIT;
2703
2911
  const cutoff = now - config.windowMs;
2704
2912
  const timestamps = (methodTimestamps.get(method) ?? []).filter((t) => t > cutoff);
2913
+ if (timestamps.length === 0) {
2914
+ methodTimestamps.delete(method);
2915
+ }
2705
2916
  if (timestamps.length >= config.maxRequests) {
2706
2917
  methodTimestamps.set(method, timestamps);
2707
2918
  return false;
@@ -3455,6 +3666,7 @@ var methodHandlers = /* @__PURE__ */ new Map([
3455
3666
  ["browser.deleteCookies", wrapAsync("browser.deleteCookies", handleBrowserDeleteCookies)],
3456
3667
  ["browser.enableNetworkCapture", wrapAsync("browser.enableNetworkCapture", handleBrowserEnableNetworkCapture)],
3457
3668
  ["browser.getNetworkRequests", wrapSync("browser.getNetworkRequests", handleBrowserGetNetworkRequests)],
3669
+ ["browser.getWebSocketFrames", wrapSync("browser.getWebSocketFrames", handleBrowserGetWebSocketFrames)],
3458
3670
  ["browser.disableNetworkCapture", wrapSync("browser.disableNetworkCapture", handleBrowserDisableNetworkCapture)],
3459
3671
  ["browser.getConsoleLogs", wrapSync("browser.getConsoleLogs", handleBrowserGetConsoleLogs)],
3460
3672
  ["browser.clearConsoleLogs", wrapSync("browser.clearConsoleLogs", handleBrowserClearConsoleLogs)],
@@ -3540,7 +3752,6 @@ var handleOffscreenGetUrl = (_message, sendResponse) => {
3540
3752
  });
3541
3753
  };
3542
3754
  var handleWsState = (message, sendResponse) => {
3543
- const wasConnected = wsConnected;
3544
3755
  const nowConnected = message.connected;
3545
3756
  persistWsConnected(nowConnected);
3546
3757
  lastDisconnectReason = nowConnected ? void 0 : message.disconnectReason;
@@ -3551,7 +3762,7 @@ var handleWsState = (message, sendResponse) => {
3551
3762
  disconnectReason: lastDisconnectReason
3552
3763
  }
3553
3764
  });
3554
- if (!nowConnected && wasConnected) {
3765
+ if (!nowConnected) {
3555
3766
  clearTabStateCache();
3556
3767
  clearAllConfirmationBadges();
3557
3768
  }
@@ -3623,11 +3834,20 @@ var handleSpConfirmationResponse = (message, sendResponse) => {
3623
3834
  params: message.data
3624
3835
  });
3625
3836
  }
3626
- clearConfirmationBadge();
3837
+ const data = message.data;
3838
+ const id = typeof data?.id === "string" ? data.id : void 0;
3839
+ if (id !== void 0) {
3840
+ clearConfirmationBackgroundTimeout(id);
3841
+ }
3842
+ clearConfirmationBadge(id);
3627
3843
  sendResponse({ ok: true });
3628
3844
  };
3629
- var handleSpConfirmationTimeout = (_message, sendResponse) => {
3630
- clearConfirmationBadge();
3845
+ var handleSpConfirmationTimeout = (message, sendResponse) => {
3846
+ const id = typeof message.id === "string" ? message.id : void 0;
3847
+ if (id !== void 0) {
3848
+ clearConfirmationBackgroundTimeout(id);
3849
+ }
3850
+ clearConfirmationBadge(id);
3631
3851
  sendResponse({ ok: true });
3632
3852
  };
3633
3853
  var handlePortChanged = (message, sendResponse) => {
@@ -3653,7 +3873,10 @@ var EXTENSION_ONLY_TYPES = /* @__PURE__ */ new Set([
3653
3873
  "ws:message",
3654
3874
  "bg:send",
3655
3875
  "bg:getConnectionState",
3656
- "offscreen:getLogs"
3876
+ "offscreen:getLogs",
3877
+ "sp:confirmationResponse",
3878
+ "sp:confirmationTimeout",
3879
+ "port-changed"
3657
3880
  ]);
3658
3881
  var initBackgroundMessageHandlers = () => {
3659
3882
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
@@ -3684,15 +3907,28 @@ var initSidePanelToggle = () => {
3684
3907
  chrome.sidePanel.onClosed.addListener(({ windowId }) => {
3685
3908
  openWindows.delete(windowId);
3686
3909
  });
3910
+ chrome.windows.onRemoved.addListener((windowId) => {
3911
+ openWindows.delete(windowId);
3912
+ });
3687
3913
  }
3688
3914
  chrome.action.onClicked.addListener(({ windowId }) => {
3689
- if (canToggle && openWindows.has(windowId)) {
3690
- chrome.sidePanel.close({ windowId }).catch(() => {
3691
- });
3692
- } else {
3693
- chrome.sidePanel.open({ windowId }).catch(() => {
3694
- });
3695
- }
3915
+ void (async () => {
3916
+ if (canToggle && openWindows.has(windowId)) {
3917
+ try {
3918
+ await chrome.windows.get(windowId);
3919
+ } catch {
3920
+ openWindows.delete(windowId);
3921
+ await chrome.sidePanel.open({ windowId }).catch(() => {
3922
+ });
3923
+ return;
3924
+ }
3925
+ chrome.sidePanel.close({ windowId }).catch(() => {
3926
+ });
3927
+ } else {
3928
+ chrome.sidePanel.open({ windowId }).catch(() => {
3929
+ });
3930
+ }
3931
+ })();
3696
3932
  });
3697
3933
  };
3698
3934
 
@@ -3752,14 +3988,14 @@ chrome.runtime.onInstalled.addListener(() => {
3752
3988
  await ensureOffscreenDocument();
3753
3989
  await setupKeepaliveAlarm();
3754
3990
  await reinjectStoredPlugins();
3755
- })();
3991
+ })().catch((err2) => console.warn("[opentabs] onInstalled failed:", err2));
3756
3992
  });
3757
3993
  chrome.runtime.onStartup.addListener(() => {
3758
3994
  void (async () => {
3759
3995
  await ensureOffscreenDocument();
3760
3996
  await setupKeepaliveAlarm();
3761
3997
  await reinjectStoredPlugins();
3762
- })();
3998
+ })().catch((err2) => console.warn("[opentabs] onStartup failed:", err2));
3763
3999
  });
3764
4000
  ensureOffscreenDocument().catch((err2) => console.warn("[opentabs] offscreen creation failed:", err2));
3765
4001
  setupKeepaliveAlarm().catch((err2) => console.warn("[opentabs] keepalive alarm failed:", err2));
@@ -3769,7 +4005,7 @@ chrome.storage.onChanged.addListener((changes, area) => {
3769
4005
  if (area !== "local")
3770
4006
  return;
3771
4007
  const portChange = changes[SERVER_PORT_KEY];
3772
- if (typeof portChange?.newValue === "number" && portChange.newValue > 0) {
4008
+ if (typeof portChange?.newValue === "number" && Number.isInteger(portChange.newValue) && portChange.newValue >= 1 && portChange.newValue <= 65535) {
3773
4009
  const newUrl = buildWsUrl(portChange.newValue);
3774
4010
  chrome.runtime.sendMessage({ type: "ws:setUrl", url: newUrl }).catch(() => {
3775
4011
  });