@opentabs-dev/browser-extension 0.0.34 → 0.0.35

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 (81) hide show
  1. package/dist/background-message-handlers.d.ts.map +1 -1
  2. package/dist/background-message-handlers.js +13 -4
  3. package/dist/background-message-handlers.js.map +1 -1
  4. package/dist/background.js +231 -114
  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.map +1 -1
  9. package/dist/browser-commands/extension-commands.js +37 -19
  10. package/dist/browser-commands/extension-commands.js.map +1 -1
  11. package/dist/browser-commands/index.d.ts +1 -1
  12. package/dist/browser-commands/index.d.ts.map +1 -1
  13. package/dist/browser-commands/index.js +1 -1
  14. package/dist/browser-commands/index.js.map +1 -1
  15. package/dist/browser-commands/interaction-commands.d.ts.map +1 -1
  16. package/dist/browser-commands/interaction-commands.js +5 -1
  17. package/dist/browser-commands/interaction-commands.js.map +1 -1
  18. package/dist/browser-commands/network-commands.d.ts +1 -0
  19. package/dist/browser-commands/network-commands.d.ts.map +1 -1
  20. package/dist/browser-commands/network-commands.js +16 -2
  21. package/dist/browser-commands/network-commands.js.map +1 -1
  22. package/dist/browser-commands/tab-commands.js +1 -1
  23. package/dist/browser-commands/tab-commands.js.map +1 -1
  24. package/dist/confirmation-badge.d.ts +16 -4
  25. package/dist/confirmation-badge.d.ts.map +1 -1
  26. package/dist/confirmation-badge.js +72 -7
  27. package/dist/confirmation-badge.js.map +1 -1
  28. package/dist/iife-injection.d.ts +1 -6
  29. package/dist/iife-injection.d.ts.map +1 -1
  30. package/dist/iife-injection.js +9 -34
  31. package/dist/iife-injection.js.map +1 -1
  32. package/dist/known-methods.d.ts +2 -2
  33. package/dist/known-methods.d.ts.map +1 -1
  34. package/dist/known-methods.js +1 -0
  35. package/dist/known-methods.js.map +1 -1
  36. package/dist/message-router.d.ts.map +1 -1
  37. package/dist/message-router.js +2 -1
  38. package/dist/message-router.js.map +1 -1
  39. package/dist/network-capture.d.ts +14 -1
  40. package/dist/network-capture.d.ts.map +1 -1
  41. package/dist/network-capture.js +103 -4
  42. package/dist/network-capture.js.map +1 -1
  43. package/dist/offscreen/index.d.ts +1 -1
  44. package/dist/offscreen/index.js +26 -30
  45. package/dist/offscreen/index.js.map +1 -1
  46. package/dist/offscreen/ws-utils.d.ts +8 -0
  47. package/dist/offscreen/ws-utils.d.ts.map +1 -0
  48. package/dist/offscreen/ws-utils.js +30 -0
  49. package/dist/offscreen/ws-utils.js.map +1 -0
  50. package/dist/rate-limiter.d.ts +2 -0
  51. package/dist/rate-limiter.d.ts.map +1 -1
  52. package/dist/rate-limiter.js +6 -0
  53. package/dist/rate-limiter.js.map +1 -1
  54. package/dist/sanitize-svg.d.ts +1 -1
  55. package/dist/sanitize-svg.js +1 -1
  56. package/dist/side-panel/App.d.ts.map +1 -1
  57. package/dist/side-panel/App.js +16 -2
  58. package/dist/side-panel/App.js.map +1 -1
  59. package/dist/side-panel/components/ConfirmationDialog.d.ts +8 -1
  60. package/dist/side-panel/components/ConfirmationDialog.d.ts.map +1 -1
  61. package/dist/side-panel/components/ConfirmationDialog.js +16 -8
  62. package/dist/side-panel/components/ConfirmationDialog.js.map +1 -1
  63. package/dist/side-panel/components/PluginCard.d.ts.map +1 -1
  64. package/dist/side-panel/components/PluginCard.js +2 -1
  65. package/dist/side-panel/components/PluginCard.js.map +1 -1
  66. package/dist/side-panel/constants.d.ts +2 -0
  67. package/dist/side-panel/constants.d.ts.map +1 -1
  68. package/dist/side-panel/constants.js +2 -0
  69. package/dist/side-panel/constants.js.map +1 -1
  70. package/dist/side-panel/hooks/useServerNotifications.d.ts.map +1 -1
  71. package/dist/side-panel/hooks/useServerNotifications.js +27 -3
  72. package/dist/side-panel/hooks/useServerNotifications.js.map +1 -1
  73. package/dist/side-panel/side-panel.js +4328 -4111
  74. package/dist/side-panel/styles.css +1 -1
  75. package/dist/side-panel-toggle.d.ts.map +1 -1
  76. package/dist/side-panel-toggle.js +21 -6
  77. package/dist/side-panel-toggle.js.map +1 -1
  78. package/dist/tab-state.d.ts.map +1 -1
  79. package/dist/tab-state.js +14 -4
  80. package/dist/tab-state.js.map +1 -1
  81. package/package.json +17 -17
@@ -1,5 +1,8 @@
1
- // dist/confirmation-badge.js
1
+ // dist/background.js
2
2
  var pendingConfirmationCount = 0;
3
+ var confirmationTimeouts = /* @__PURE__ */ new Map();
4
+ var CONFIRMATION_BACKGROUND_TIMEOUT_BUFFER_MS = 2e3;
5
+ var CONFIRMATION_FALLBACK_TIMEOUT_MS = 3e4;
3
6
  var updateConfirmationBadge = () => {
4
7
  if (pendingConfirmationCount > 0) {
5
8
  chrome.action.setBadgeText({ text: String(pendingConfirmationCount) }).catch(() => {
@@ -12,11 +15,24 @@ var updateConfirmationBadge = () => {
12
15
  }
13
16
  };
14
17
  var notifyConfirmationRequest = (params) => {
15
- pendingConfirmationCount++;
16
- updateConfirmationBadge();
17
18
  const tool = typeof params.tool === "string" ? params.tool : "unknown tool";
18
19
  const domain = typeof params.domain === "string" ? params.domain : "unknown domain";
19
- chrome.notifications.create(`opentabs-confirm-${typeof params.id === "string" ? params.id : Date.now()}`, {
20
+ const id = typeof params.id === "string" ? params.id : String(Date.now());
21
+ const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 0;
22
+ const existingTimeoutId = confirmationTimeouts.get(id);
23
+ if (existingTimeoutId !== void 0) {
24
+ clearTimeout(existingTimeoutId);
25
+ } else {
26
+ pendingConfirmationCount++;
27
+ updateConfirmationBadge();
28
+ }
29
+ const effectiveTimeoutMs = timeoutMs > 0 ? timeoutMs : CONFIRMATION_FALLBACK_TIMEOUT_MS;
30
+ const bgTimeoutId = setTimeout(() => {
31
+ confirmationTimeouts.delete(id);
32
+ clearConfirmationBadge(id);
33
+ }, effectiveTimeoutMs + CONFIRMATION_BACKGROUND_TIMEOUT_BUFFER_MS);
34
+ confirmationTimeouts.set(id, bgTimeoutId);
35
+ chrome.notifications.create(`opentabs-confirm-${id}`, {
20
36
  type: "basic",
21
37
  iconUrl: chrome.runtime.getURL("icons/icon-128.png"),
22
38
  title: "OpenTabs: Approval Required",
@@ -26,11 +42,28 @@ var notifyConfirmationRequest = (params) => {
26
42
  }).catch(() => {
27
43
  });
28
44
  };
29
- var clearConfirmationBadge = () => {
45
+ var clearConfirmationBadge = (id) => {
46
+ if (id !== void 0) {
47
+ chrome.notifications.clear(`opentabs-confirm-${id}`).catch(() => {
48
+ });
49
+ }
30
50
  pendingConfirmationCount = Math.max(0, pendingConfirmationCount - 1);
31
51
  updateConfirmationBadge();
32
52
  };
53
+ var clearConfirmationBackgroundTimeout = (id) => {
54
+ const timeoutId = confirmationTimeouts.get(id);
55
+ if (timeoutId !== void 0) {
56
+ clearTimeout(timeoutId);
57
+ confirmationTimeouts.delete(id);
58
+ }
59
+ };
33
60
  var clearAllConfirmationBadges = () => {
61
+ for (const [id, timeoutId] of confirmationTimeouts.entries()) {
62
+ clearTimeout(timeoutId);
63
+ chrome.notifications.clear(`opentabs-confirm-${id}`).catch(() => {
64
+ });
65
+ }
66
+ confirmationTimeouts.clear();
34
67
  pendingConfirmationCount = 0;
35
68
  updateConfirmationBadge();
36
69
  };
@@ -49,8 +82,6 @@ var initConfirmationBadge = () => {
49
82
  }
50
83
  });
51
84
  };
52
-
53
- // dist/constants.js
54
85
  var KEEPALIVE_ALARM = "opentabs-keepalive";
55
86
  var KEEPALIVE_INTERVAL_MINUTES = 0.5;
56
87
  var PLUGINS_META_KEY = "plugins_meta";
@@ -78,15 +109,11 @@ var DEFAULT_LOG_LIMIT = 100;
78
109
  var VALID_PLUGIN_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
79
110
  var isValidPluginName = (name) => VALID_PLUGIN_NAME.test(name);
80
111
  var buildWsUrl = (port) => `ws://localhost:${port}/ws`;
81
-
82
- // dist/json-rpc-errors.js
83
112
  var JSONRPC_METHOD_NOT_FOUND = -32601;
84
113
  var JSONRPC_INVALID_PARAMS = -32602;
85
114
  var JSONRPC_INTERNAL_ERROR = -32603;
86
115
  var JSONRPC_NO_USABLE_TAB = -32001;
87
116
  var JSONRPC_ADAPTER_NOT_READY = -32002;
88
-
89
- // dist/messaging.js
90
117
  var sendToServer = (data) => {
91
118
  const method = data.method ?? "unknown";
92
119
  chrome.runtime.sendMessage({ type: "ws:send", data }).catch((err2) => {
@@ -120,8 +147,6 @@ var sendTabStateNotification = (pluginName, stateInfo) => {
120
147
  }
121
148
  });
122
149
  };
123
-
124
- // dist/sanitize-error.js
125
150
  var MAX_LENGTH = 500;
126
151
  var sanitizeErrorMessage = (message) => {
127
152
  let sanitized = message.replace(/[a-z]:[/\\][^\s,;)}\]]+/gi, "[PATH]").replace(/\/[a-z0-9._-]+(?:\/[a-z0-9._-]+)+/gi, "[PATH]").replace(/https?:\/\/[^\s,;)}\]]+/gi, "[URL]").replace(/localhost:\d+/gi, "[LOCALHOST]").replace(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[IP]");
@@ -130,11 +155,7 @@ var sanitizeErrorMessage = (message) => {
130
155
  }
131
156
  return sanitized;
132
157
  };
133
-
134
- // ../shared/dist/error.js
135
158
  var toErrorMessage = (err2) => err2 instanceof Error ? err2.message : String(err2);
136
-
137
- // ../shared/dist/index.js
138
159
  var BLOCKED_URL_SCHEMES = [
139
160
  "javascript:",
140
161
  "data:",
@@ -151,8 +172,6 @@ var isBlockedUrlScheme = (url) => {
151
172
  return true;
152
173
  }
153
174
  };
154
-
155
- // dist/browser-commands/helpers.js
156
175
  var requireTabId = (params, id) => {
157
176
  const tabId = params.tabId;
158
177
  if (typeof tabId !== "number") {
@@ -245,8 +264,6 @@ var sendValidationError = (id, message) => {
245
264
  var sendSuccessResult = (id, result) => {
246
265
  sendToServer({ jsonrpc: "2.0", result, id });
247
266
  };
248
-
249
- // dist/browser-commands/content-commands.js
250
267
  var handleBrowserGetTabContent = async (params, id) => {
251
268
  try {
252
269
  const tabId = requireTabId(params, id);
@@ -385,8 +402,6 @@ var handleBrowserScreenshotTab = async (params, id) => {
385
402
  sendErrorResult(id, err2);
386
403
  }
387
404
  };
388
-
389
- // dist/browser-commands/cookie-commands.js
390
405
  var handleBrowserGetCookies = async (params, id) => {
391
406
  try {
392
407
  const url = requireUrl(params, id);
@@ -465,14 +480,12 @@ var handleBrowserDeleteCookies = async (params, id) => {
465
480
  const name = requireStringParam(params, "name", id);
466
481
  if (name === null)
467
482
  return;
468
- await chrome.cookies.remove({ url, name });
469
- sendSuccessResult(id, { deleted: true, name, url });
483
+ const result = await chrome.cookies.remove({ url, name });
484
+ sendSuccessResult(id, { deleted: result !== null, name, url });
470
485
  } catch (err2) {
471
486
  sendErrorResult(id, err2);
472
487
  }
473
488
  };
474
-
475
- // dist/log-collector.js
476
489
  var MAX_MESSAGE_LENGTH = 2e3;
477
490
  var DEFAULT_MAX_ENTRIES = 500;
478
491
  var formatArg = (arg) => {
@@ -559,12 +572,10 @@ var installLogCollector = (source, maxEntries) => {
559
572
  }
560
573
  return collector;
561
574
  };
562
-
563
- // dist/background-log-state.js
564
575
  var bgLogCollector = installLogCollector("background");
565
-
566
- // dist/network-capture.js
567
576
  var MAX_BODY_LENGTH = 102400;
577
+ var PENDING_REQUEST_TTL_MS = 6e4;
578
+ var PRUNE_INTERVAL_MS = 3e4;
568
579
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
569
580
  "authorization",
570
581
  "cookie",
@@ -652,6 +663,17 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
652
663
  const request = paramsRecord?.request;
653
664
  if (!requestId || !request)
654
665
  return;
666
+ const now = Date.now();
667
+ for (const [id, pending] of state.pendingRequests) {
668
+ if (pending.timestamp !== void 0 && now - pending.timestamp > PENDING_REQUEST_TTL_MS) {
669
+ state.pendingRequests.delete(id);
670
+ }
671
+ }
672
+ for (const [id, req] of state.requestIdToRequest) {
673
+ if (now - req.timestamp > PENDING_REQUEST_TTL_MS) {
674
+ state.requestIdToRequest.delete(id);
675
+ }
676
+ }
655
677
  const url = stringProp(request, "url", "");
656
678
  if (state.urlFilter && !url.includes(state.urlFilter))
657
679
  return;
@@ -727,15 +749,21 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
727
749
  const responseData = result;
728
750
  if (typeof responseData.body !== "string")
729
751
  return;
752
+ if (!state.requests.includes(request))
753
+ return;
730
754
  const body = responseData.base64Encoded ? new TextDecoder().decode(Uint8Array.from(atob(responseData.body), (c) => c.charCodeAt(0))) : responseData.body;
731
755
  request.responseBody = truncateBody(body);
732
756
  });
733
757
  } else if (method === "Network.webSocketCreated") {
734
758
  const url = paramsRecord?.url;
759
+ const requestId = paramsRecord?.requestId;
735
760
  if (!url)
736
761
  return;
737
762
  if (state.urlFilter && !url.includes(state.urlFilter))
738
763
  return;
764
+ if (requestId) {
765
+ state.wsFramesByRequestId.set(requestId, url);
766
+ }
739
767
  const completed = {
740
768
  url,
741
769
  method: "GET",
@@ -748,6 +776,27 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
748
776
  evictOldestRequest(state);
749
777
  }
750
778
  state.requests.push(completed);
779
+ } else if (method === "Network.webSocketFrameSent" || method === "Network.webSocketFrameReceived") {
780
+ const requestId = paramsRecord?.requestId;
781
+ const response = paramsRecord?.response;
782
+ if (!requestId || !response)
783
+ return;
784
+ const url = state.wsFramesByRequestId.get(requestId);
785
+ if (!url)
786
+ return;
787
+ const opcode = typeof response.opcode === "number" ? response.opcode : 1;
788
+ const payloadData = typeof response.payloadData === "string" ? response.payloadData : "";
789
+ const direction = method === "Network.webSocketFrameSent" ? "sent" : "received";
790
+ const data = truncateBody(payloadData);
791
+ if (state.wsFrames.length >= state.maxWsFrames) {
792
+ state.wsFrames.shift();
793
+ }
794
+ state.wsFrames.push({ url, direction, data, opcode, timestamp: Date.now() });
795
+ } else if (method === "Network.webSocketClosed") {
796
+ const requestId = paramsRecord?.requestId;
797
+ if (requestId) {
798
+ state.wsFramesByRequestId.delete(requestId);
799
+ }
751
800
  } else if (method === "Runtime.consoleAPICalled") {
752
801
  const type = paramsRecord?.type;
753
802
  const args = paramsRecord?.args;
@@ -774,13 +823,24 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
774
823
  }
775
824
  });
776
825
  chrome.tabs.onRemoved.addListener((tabId) => {
777
- if (captures.has(tabId)) {
826
+ const state = captures.get(tabId);
827
+ if (state) {
828
+ clearInterval(state.pruneIntervalId);
778
829
  void chrome.debugger.detach({ tabId }).catch(() => {
779
830
  });
780
831
  captures.delete(tabId);
781
832
  }
782
833
  });
783
- var startCapture = async (tabId, maxRequests = 100, urlFilter, maxConsoleLogs = 500) => {
834
+ chrome.debugger.onDetach.addListener((source, _reason) => {
835
+ const tabId = source.tabId;
836
+ if (tabId !== void 0) {
837
+ const state = captures.get(tabId);
838
+ if (state)
839
+ clearInterval(state.pruneIntervalId);
840
+ captures.delete(tabId);
841
+ }
842
+ });
843
+ var startCapture = async (tabId, maxRequests = 100, urlFilter, maxConsoleLogs = 500, maxWsFrames = 200) => {
784
844
  if (captures.has(tabId)) {
785
845
  throw new Error(`Network capture already active for tab ${tabId}. Call stopCapture first.`);
786
846
  }
@@ -797,20 +857,39 @@ var startCapture = async (tabId, maxRequests = 100, urlFilter, maxConsoleLogs =
797
857
  });
798
858
  throw err2;
799
859
  }
800
- captures.set(tabId, {
860
+ const captureState = {
801
861
  requests: [],
802
862
  consoleLogs: [],
863
+ wsFrames: [],
803
864
  maxRequests,
804
865
  maxConsoleLogs,
866
+ maxWsFrames,
805
867
  urlFilter,
806
868
  pendingRequests: /* @__PURE__ */ new Map(),
807
- requestIdToRequest: /* @__PURE__ */ new Map()
808
- });
869
+ requestIdToRequest: /* @__PURE__ */ new Map(),
870
+ wsFramesByRequestId: /* @__PURE__ */ new Map()
871
+ };
872
+ captureState.pruneIntervalId = setInterval(() => {
873
+ const now = Date.now();
874
+ for (const [id, pending] of captureState.pendingRequests) {
875
+ if (pending.timestamp !== void 0 && now - pending.timestamp > PENDING_REQUEST_TTL_MS) {
876
+ captureState.pendingRequests.delete(id);
877
+ }
878
+ }
879
+ for (const [id, req] of captureState.requestIdToRequest) {
880
+ if (now - req.timestamp > PENDING_REQUEST_TTL_MS) {
881
+ captureState.requestIdToRequest.delete(id);
882
+ }
883
+ }
884
+ }, PRUNE_INTERVAL_MS);
885
+ captures.set(tabId, captureState);
809
886
  };
810
887
  var stopCapture = (tabId) => {
811
888
  const state = captures.get(tabId);
812
889
  if (!state)
813
890
  return;
891
+ clearInterval(state.pruneIntervalId);
892
+ state.wsFramesByRequestId.clear();
814
893
  void chrome.debugger.detach({ tabId }).catch(() => {
815
894
  });
816
895
  captures.delete(tabId);
@@ -827,6 +906,7 @@ var getRequests = (tabId, clear = false) => {
827
906
  if (clear) {
828
907
  state.requests = [];
829
908
  state.requestIdToRequest.clear();
909
+ state.pendingRequests.clear();
830
910
  }
831
911
  return requests;
832
912
  };
@@ -850,13 +930,22 @@ var clearConsoleLogs = (tabId) => {
850
930
  state.consoleLogs = [];
851
931
  }
852
932
  };
933
+ var getWsFrames = (tabId, clear = false) => {
934
+ const state = captures.get(tabId);
935
+ if (!state)
936
+ return [];
937
+ const frames = [...state.wsFrames];
938
+ if (clear) {
939
+ state.wsFrames = [];
940
+ state.wsFramesByRequestId.clear();
941
+ }
942
+ return frames;
943
+ };
853
944
  var getActiveCapturesSummary = () => Array.from(captures.entries()).map(([tabId, state]) => ({
854
945
  tabId,
855
946
  requestCount: state.requests.length,
856
947
  isCapturing: true
857
948
  }));
858
-
859
- // dist/plugin-storage.js
860
949
  var metaCache = null;
861
950
  var writeMutex = Promise.resolve();
862
951
  var serialize = (fn) => {
@@ -933,8 +1022,6 @@ var getPluginMeta = async (pluginName) => {
933
1022
  var invalidatePluginCache = () => {
934
1023
  metaCache = null;
935
1024
  };
936
-
937
- // dist/tab-matching.js
938
1025
  var urlMatchesPatterns = (url, patterns) => {
939
1026
  for (const pattern of patterns) {
940
1027
  if (matchPattern(url, pattern))
@@ -1025,17 +1112,21 @@ var findAllMatchingTabs = async (plugin) => {
1025
1112
  };
1026
1113
  return allMatches.slice().sort((a, b) => rank(b) - rank(a));
1027
1114
  };
1028
-
1029
- // dist/tab-state.js
1030
1115
  var lastKnownState = /* @__PURE__ */ new Map();
1031
1116
  var pluginLocks = /* @__PURE__ */ new Map();
1032
1117
  var withPluginLock = (pluginName, fn) => {
1033
1118
  const prev = pluginLocks.get(pluginName) ?? Promise.resolve();
1034
- const next = prev.then(fn);
1035
- pluginLocks.set(pluginName, next.catch((err2) => {
1119
+ const operation = prev.then(fn);
1120
+ const lock = operation.catch((err2) => {
1036
1121
  console.warn("[opentabs] tab state operation failed for plugin", pluginName, ":", err2);
1037
- }));
1038
- return next;
1122
+ });
1123
+ pluginLocks.set(pluginName, lock);
1124
+ void lock.then(() => {
1125
+ if (pluginLocks.get(pluginName) === lock) {
1126
+ pluginLocks.set(pluginName, Promise.resolve());
1127
+ }
1128
+ });
1129
+ return operation;
1039
1130
  };
1040
1131
  var probeTabReadiness = async (tabId, pluginName) => {
1041
1132
  let timerId;
@@ -1201,8 +1292,6 @@ var checkTabChanged = async (changedTabId, changeInfo) => {
1201
1292
  return;
1202
1293
  await notifyAffectedPlugins(affectedPlugins);
1203
1294
  };
1204
-
1205
- // dist/browser-commands/extension-commands.js
1206
1295
  var handleExtensionGetState = async (id) => {
1207
1296
  try {
1208
1297
  const sessionData = await chrome.storage.session.get(WS_CONNECTED_KEY).catch(() => ({}));
@@ -1294,10 +1383,18 @@ var handleExtensionGetLogs = async (params, id) => {
1294
1383
  };
1295
1384
  var handleExtensionGetSidePanel = async (id) => {
1296
1385
  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
- ]);
1386
+ let timeoutId;
1387
+ let sidePanelResult;
1388
+ try {
1389
+ sidePanelResult = await Promise.race([
1390
+ chrome.runtime.sendMessage({ type: "sp:getState" }).then((raw) => raw),
1391
+ new Promise((resolve) => {
1392
+ timeoutId = setTimeout(() => resolve(null), SIDE_PANEL_TIMEOUT_MS);
1393
+ })
1394
+ ]);
1395
+ } finally {
1396
+ clearTimeout(timeoutId);
1397
+ }
1301
1398
  if (!sidePanelResult || typeof sidePanelResult !== "object") {
1302
1399
  sendSuccessResult(id, { open: false });
1303
1400
  return;
@@ -1376,21 +1473,29 @@ var handleExtensionCheckAdapter = async (params, id) => {
1376
1473
  }
1377
1474
  let isReady = false;
1378
1475
  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
- ]);
1476
+ let isReadyTimeoutId;
1477
+ let readyResults;
1478
+ try {
1479
+ readyResults = await Promise.race([
1480
+ chrome.scripting.executeScript({
1481
+ target: { tabId },
1482
+ world: "MAIN",
1483
+ func: async (pName) => {
1484
+ const ot = globalThis.__openTabs;
1485
+ const adapter = ot?.adapters?.[pName];
1486
+ if (!adapter || typeof adapter.isReady !== "function")
1487
+ return false;
1488
+ return await adapter.isReady();
1489
+ },
1490
+ args: [pluginName]
1491
+ }),
1492
+ new Promise((resolve) => {
1493
+ isReadyTimeoutId = setTimeout(() => resolve(null), IS_READY_TIMEOUT_MS);
1494
+ })
1495
+ ]);
1496
+ } finally {
1497
+ clearTimeout(isReadyTimeoutId);
1498
+ }
1394
1499
  if (readyResults !== null) {
1395
1500
  const readyResult = readyResults[0];
1396
1501
  isReady = readyResult?.result === true;
@@ -1524,8 +1629,6 @@ var handleBrowserExecuteScript = async (params, id) => {
1524
1629
  sendErrorResult(id, err2);
1525
1630
  }
1526
1631
  };
1527
-
1528
- // dist/browser-commands/resource-commands.js
1529
1632
  var TEXT_MIME_PREFIXES = ["text/"];
1530
1633
  var TEXT_MIME_EXACT = /* @__PURE__ */ new Set([
1531
1634
  "application/javascript",
@@ -1651,8 +1754,6 @@ var handleBrowserGetResourceContent = async (params, id) => {
1651
1754
  sendErrorResult(id, err2);
1652
1755
  }
1653
1756
  };
1654
-
1655
- // dist/browser-commands/interaction-commands.js
1656
1757
  var handleBrowserClickElement = async (params, id) => {
1657
1758
  try {
1658
1759
  const tabId = requireTabId(params, id);
@@ -1827,7 +1928,8 @@ var handleBrowserWaitForElement = async (params, id) => {
1827
1928
  const el = document.querySelector(sel);
1828
1929
  if (el) {
1829
1930
  const htmlEl = el;
1830
- const isVisible = !vis || htmlEl.offsetParent !== null && getComputedStyle(htmlEl).display !== "none";
1931
+ const style = getComputedStyle(htmlEl);
1932
+ const isVisible = !vis || style.display !== "none" && style.visibility !== "hidden" && (htmlEl.offsetParent !== null || style.position === "fixed" || style.position === "sticky");
1831
1933
  if (isVisible) {
1832
1934
  clearInterval(poll);
1833
1935
  resolve({
@@ -1972,8 +2074,6 @@ var handleBrowserHandleDialog = async (params, id) => {
1972
2074
  sendErrorResult(id, err2);
1973
2075
  }
1974
2076
  };
1975
-
1976
- // dist/browser-commands/key-press-command.js
1977
2077
  var handleBrowserPressKey = async (params, id) => {
1978
2078
  try {
1979
2079
  const tabId = requireTabId(params, id);
@@ -2105,8 +2205,6 @@ var handleBrowserPressKey = async (params, id) => {
2105
2205
  sendErrorResult(id, err2);
2106
2206
  }
2107
2207
  };
2108
-
2109
- // dist/browser-commands/scroll-command.js
2110
2208
  var handleBrowserScroll = async (params, id) => {
2111
2209
  try {
2112
2210
  const tabId = requireTabId(params, id);
@@ -2220,8 +2318,6 @@ var handleBrowserScroll = async (params, id) => {
2220
2318
  sendErrorResult(id, err2);
2221
2319
  }
2222
2320
  };
2223
-
2224
- // dist/browser-commands/network-commands.js
2225
2321
  var handleBrowserEnableNetworkCapture = async (params, id) => {
2226
2322
  try {
2227
2323
  const tabId = requireTabId(params, id);
@@ -2230,7 +2326,8 @@ var handleBrowserEnableNetworkCapture = async (params, id) => {
2230
2326
  const maxRequests = typeof params.maxRequests === "number" ? params.maxRequests : 100;
2231
2327
  const urlFilter = typeof params.urlFilter === "string" ? params.urlFilter : void 0;
2232
2328
  const maxConsoleLogs = typeof params.maxConsoleLogs === "number" ? params.maxConsoleLogs : 500;
2233
- await startCapture(tabId, maxRequests, urlFilter, maxConsoleLogs);
2329
+ const maxWsFrames = typeof params.maxWsFrames === "number" ? params.maxWsFrames : 200;
2330
+ await startCapture(tabId, maxRequests, urlFilter, maxConsoleLogs, maxWsFrames);
2234
2331
  sendSuccessResult(id, { enabled: true, tabId });
2235
2332
  } catch (err2) {
2236
2333
  sendErrorResult(id, err2);
@@ -2283,8 +2380,18 @@ var handleBrowserClearConsoleLogs = (params, id) => {
2283
2380
  sendErrorResult(id, err2);
2284
2381
  }
2285
2382
  };
2286
-
2287
- // dist/browser-commands/tab-commands.js
2383
+ var handleBrowserGetWebSocketFrames = (params, id) => {
2384
+ try {
2385
+ const tabId = requireTabId(params, id);
2386
+ if (tabId === null)
2387
+ return;
2388
+ const clear = typeof params.clear === "boolean" ? params.clear : false;
2389
+ const frames = getWsFrames(tabId, clear);
2390
+ sendSuccessResult(id, { frames });
2391
+ } catch (err2) {
2392
+ sendErrorResult(id, err2);
2393
+ }
2394
+ };
2288
2395
  var handleBrowserListTabs = async (id) => {
2289
2396
  try {
2290
2397
  const tabs = await chrome.tabs.query({});
@@ -2331,7 +2438,7 @@ var handleBrowserNavigateTab = async (params, id) => {
2331
2438
  if (url === null)
2332
2439
  return;
2333
2440
  const tab = await chrome.tabs.update(tabId, { url });
2334
- sendSuccessResult(id, { id: tab?.id ?? tabId, title: tab?.title ?? "", url: tab?.url ?? url });
2441
+ sendSuccessResult(id, { id: tab?.id ?? tabId, title: tab?.title ?? "", url });
2335
2442
  } catch (err2) {
2336
2443
  sendErrorResult(id, err2);
2337
2444
  }
@@ -2372,8 +2479,6 @@ var handleBrowserGetTabInfo = async (params, id) => {
2372
2479
  sendErrorResult(id, err2);
2373
2480
  }
2374
2481
  };
2375
-
2376
- // dist/iife-injection.js
2377
2482
  var RESERVED_NAMES = /* @__PURE__ */ new Set(["system", "browser", "opentabs", "extension", "config", "plugin", "tool", "mcp"]);
2378
2483
  var isSafePluginName = (name) => isValidPluginName(name) && !RESERVED_NAMES.has(name);
2379
2484
  var isAdapterPresent = async (tabId, pluginName) => {
@@ -2449,8 +2554,10 @@ var injectLogRelay = async (tabId) => {
2449
2554
  const win = window;
2450
2555
  if (win[guard]) {
2451
2556
  const nonceSet = win.__opentabs_log_nonces;
2452
- if (nonceSet)
2557
+ if (nonceSet) {
2558
+ nonceSet.clear();
2453
2559
  nonceSet.add(n);
2560
+ }
2454
2561
  return;
2455
2562
  }
2456
2563
  win[guard] = true;
@@ -2683,8 +2790,6 @@ var reinjectStoredPlugins = async () => {
2683
2790
  }
2684
2791
  }
2685
2792
  };
2686
-
2687
- // dist/rate-limiter.js
2688
2793
  var METHOD_LIMITS = /* @__PURE__ */ new Map([
2689
2794
  // Expensive operations — tight limits
2690
2795
  ["browser.screenshotTab", { maxRequests: 2, windowMs: 1e3 }],
@@ -2702,6 +2807,9 @@ var checkRateLimit = (method, now = Date.now()) => {
2702
2807
  const config = METHOD_LIMITS.get(method) ?? DEFAULT_LIMIT;
2703
2808
  const cutoff = now - config.windowMs;
2704
2809
  const timestamps = (methodTimestamps.get(method) ?? []).filter((t) => t > cutoff);
2810
+ if (timestamps.length === 0) {
2811
+ methodTimestamps.delete(method);
2812
+ }
2705
2813
  if (timestamps.length >= config.maxRequests) {
2706
2814
  methodTimestamps.set(method, timestamps);
2707
2815
  return false;
@@ -2710,8 +2818,6 @@ var checkRateLimit = (method, now = Date.now()) => {
2710
2818
  methodTimestamps.set(method, timestamps);
2711
2819
  return true;
2712
2820
  };
2713
-
2714
- // dist/dispatch-helpers.js
2715
2821
  var isAdapterNotReady = (result) => result.type === "error" && result.code === JSONRPC_ADAPTER_NOT_READY;
2716
2822
  var resolvePlugin = async (pluginName, id) => {
2717
2823
  const plugin = await getPluginMeta(pluginName);
@@ -2818,8 +2924,6 @@ var dispatchWithTabFallback = async (config) => {
2818
2924
  });
2819
2925
  }
2820
2926
  };
2821
-
2822
- // dist/resource-prompt-dispatch.js
2823
2927
  var executeResourceReadOnTab = async (tabId, pluginName, resourceUri) => {
2824
2928
  const scriptPromise = chrome.scripting.executeScript({
2825
2929
  target: { tabId },
@@ -2986,8 +3090,6 @@ var handlePromptGet = async (params, id) => {
2986
3090
  executeOnTab: (tabId) => executePromptGetOnTab(tabId, pluginName, promptName, promptArgs)
2987
3091
  });
2988
3092
  };
2989
-
2990
- // dist/tool-dispatch.js
2991
3093
  var progressCallbacks = /* @__PURE__ */ new Map();
2992
3094
  var notifyDispatchProgress = (dispatchId) => {
2993
3095
  const cb = progressCallbacks.get(dispatchId);
@@ -3245,8 +3347,6 @@ var handleToolDispatch = async (params, id) => {
3245
3347
  }
3246
3348
  });
3247
3349
  };
3248
-
3249
- // dist/message-router.js
3250
3350
  var wrapAsync = (method, fn) => (params, id) => {
3251
3351
  if (id !== void 0) {
3252
3352
  fn(params, id).catch((err2) => console.warn(`[opentabs] ${method} handler failed:`, err2));
@@ -3455,6 +3555,7 @@ var methodHandlers = /* @__PURE__ */ new Map([
3455
3555
  ["browser.deleteCookies", wrapAsync("browser.deleteCookies", handleBrowserDeleteCookies)],
3456
3556
  ["browser.enableNetworkCapture", wrapAsync("browser.enableNetworkCapture", handleBrowserEnableNetworkCapture)],
3457
3557
  ["browser.getNetworkRequests", wrapSync("browser.getNetworkRequests", handleBrowserGetNetworkRequests)],
3558
+ ["browser.getWebSocketFrames", wrapSync("browser.getWebSocketFrames", handleBrowserGetWebSocketFrames)],
3458
3559
  ["browser.disableNetworkCapture", wrapSync("browser.disableNetworkCapture", handleBrowserDisableNetworkCapture)],
3459
3560
  ["browser.getConsoleLogs", wrapSync("browser.getConsoleLogs", handleBrowserGetConsoleLogs)],
3460
3561
  ["browser.clearConsoleLogs", wrapSync("browser.clearConsoleLogs", handleBrowserClearConsoleLogs)],
@@ -3512,8 +3613,6 @@ var handleServerMessage = (message) => {
3512
3613
  }
3513
3614
  };
3514
3615
  var methodHandlerNames = Array.from(methodHandlers.keys());
3515
-
3516
- // dist/background-message-handlers.js
3517
3616
  var wsConnected = false;
3518
3617
  var lastDisconnectReason;
3519
3618
  var restoreWsConnectedState = () => {
@@ -3623,11 +3722,20 @@ var handleSpConfirmationResponse = (message, sendResponse) => {
3623
3722
  params: message.data
3624
3723
  });
3625
3724
  }
3626
- clearConfirmationBadge();
3725
+ const data = message.data;
3726
+ const id = typeof data?.id === "string" ? data.id : void 0;
3727
+ if (id !== void 0) {
3728
+ clearConfirmationBackgroundTimeout(id);
3729
+ }
3730
+ clearConfirmationBadge(id);
3627
3731
  sendResponse({ ok: true });
3628
3732
  };
3629
- var handleSpConfirmationTimeout = (_message, sendResponse) => {
3630
- clearConfirmationBadge();
3733
+ var handleSpConfirmationTimeout = (message, sendResponse) => {
3734
+ const id = typeof message.id === "string" ? message.id : void 0;
3735
+ if (id !== void 0) {
3736
+ clearConfirmationBackgroundTimeout(id);
3737
+ }
3738
+ clearConfirmationBadge(id);
3631
3739
  sendResponse({ ok: true });
3632
3740
  };
3633
3741
  var handlePortChanged = (message, sendResponse) => {
@@ -3670,8 +3778,6 @@ var initBackgroundMessageHandlers = () => {
3670
3778
  });
3671
3779
  };
3672
3780
  var backgroundHandlerNames = [...backgroundHandlers.keys()];
3673
-
3674
- // dist/side-panel-toggle.js
3675
3781
  var openWindows = /* @__PURE__ */ new Set();
3676
3782
  var initSidePanelToggle = () => {
3677
3783
  chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => {
@@ -3684,19 +3790,30 @@ var initSidePanelToggle = () => {
3684
3790
  chrome.sidePanel.onClosed.addListener(({ windowId }) => {
3685
3791
  openWindows.delete(windowId);
3686
3792
  });
3793
+ chrome.windows.onRemoved.addListener((windowId) => {
3794
+ openWindows.delete(windowId);
3795
+ });
3687
3796
  }
3688
3797
  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
- }
3798
+ void (async () => {
3799
+ if (canToggle && openWindows.has(windowId)) {
3800
+ try {
3801
+ await chrome.windows.get(windowId);
3802
+ } catch {
3803
+ openWindows.delete(windowId);
3804
+ await chrome.sidePanel.open({ windowId }).catch(() => {
3805
+ });
3806
+ return;
3807
+ }
3808
+ chrome.sidePanel.close({ windowId }).catch(() => {
3809
+ });
3810
+ } else {
3811
+ chrome.sidePanel.open({ windowId }).catch(() => {
3812
+ });
3813
+ }
3814
+ })();
3696
3815
  });
3697
3816
  };
3698
-
3699
- // dist/background.js
3700
3817
  initSidePanelToggle();
3701
3818
  restoreWsConnectedState();
3702
3819
  var creatingOffscreen = null;
@@ -3752,14 +3869,14 @@ chrome.runtime.onInstalled.addListener(() => {
3752
3869
  await ensureOffscreenDocument();
3753
3870
  await setupKeepaliveAlarm();
3754
3871
  await reinjectStoredPlugins();
3755
- })();
3872
+ })().catch((err2) => console.warn("[opentabs] onInstalled failed:", err2));
3756
3873
  });
3757
3874
  chrome.runtime.onStartup.addListener(() => {
3758
3875
  void (async () => {
3759
3876
  await ensureOffscreenDocument();
3760
3877
  await setupKeepaliveAlarm();
3761
3878
  await reinjectStoredPlugins();
3762
- })();
3879
+ })().catch((err2) => console.warn("[opentabs] onStartup failed:", err2));
3763
3880
  });
3764
3881
  ensureOffscreenDocument().catch((err2) => console.warn("[opentabs] offscreen creation failed:", err2));
3765
3882
  setupKeepaliveAlarm().catch((err2) => console.warn("[opentabs] keepalive alarm failed:", err2));
@@ -3769,7 +3886,7 @@ chrome.storage.onChanged.addListener((changes, area) => {
3769
3886
  if (area !== "local")
3770
3887
  return;
3771
3888
  const portChange = changes[SERVER_PORT_KEY];
3772
- if (typeof portChange?.newValue === "number" && portChange.newValue > 0) {
3889
+ if (typeof portChange?.newValue === "number" && Number.isInteger(portChange.newValue) && portChange.newValue >= 1 && portChange.newValue <= 65535) {
3773
3890
  const newUrl = buildWsUrl(portChange.newValue);
3774
3891
  chrome.runtime.sendMessage({ type: "ws:setUrl", url: newUrl }).catch(() => {
3775
3892
  });