@btraut/browser-bridge 0.4.3 → 0.5.0

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.
@@ -92,6 +92,301 @@ var sanitizeDriveErrorInfo = (error) => {
92
92
  };
93
93
  };
94
94
 
95
+ // packages/extension/src/site-permissions.ts
96
+ var SITE_ALLOWLIST_KEY = "siteAllowlist";
97
+ var PERMISSION_PROMPT_WAIT_MS_KEY = "permissionPromptWaitMs";
98
+ var DEFAULT_PERMISSION_PROMPT_WAIT_MS = 3e4;
99
+ var siteKeyFromUrl = (rawUrl) => {
100
+ if (!rawUrl || typeof rawUrl !== "string") {
101
+ return null;
102
+ }
103
+ try {
104
+ const parsed = new URL(rawUrl);
105
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
106
+ return null;
107
+ }
108
+ if (!parsed.hostname) {
109
+ return null;
110
+ }
111
+ return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
112
+ } catch {
113
+ return null;
114
+ }
115
+ };
116
+ var isAllowlistEntry = (value) => {
117
+ if (!value || typeof value !== "object") {
118
+ return false;
119
+ }
120
+ const v = value;
121
+ return typeof v.createdAt === "string" && typeof v.lastUsedAt === "string";
122
+ };
123
+ var normalizeSiteKey = (siteKey) => siteKey.toLowerCase();
124
+ var readAllowlistRaw = async () => {
125
+ return await new Promise((resolve) => {
126
+ chrome.storage.local.get(
127
+ [SITE_ALLOWLIST_KEY],
128
+ (result) => {
129
+ const raw = result?.[SITE_ALLOWLIST_KEY];
130
+ if (!raw || typeof raw !== "object") {
131
+ resolve({});
132
+ return;
133
+ }
134
+ const out = {};
135
+ for (const [k, v] of Object.entries(raw)) {
136
+ if (typeof k !== "string") {
137
+ continue;
138
+ }
139
+ if (!isAllowlistEntry(v)) {
140
+ continue;
141
+ }
142
+ out[normalizeSiteKey(k)] = v;
143
+ }
144
+ resolve(out);
145
+ }
146
+ );
147
+ });
148
+ };
149
+ var writeAllowlistRaw = async (allowlist) => {
150
+ return await new Promise((resolve) => {
151
+ chrome.storage.local.set(
152
+ { [SITE_ALLOWLIST_KEY]: allowlist },
153
+ () => resolve()
154
+ );
155
+ });
156
+ };
157
+ var readPermissionPromptWaitMs = async () => {
158
+ return await new Promise((resolve) => {
159
+ chrome.storage.local.get(
160
+ [PERMISSION_PROMPT_WAIT_MS_KEY],
161
+ (result) => {
162
+ const raw = result?.[PERMISSION_PROMPT_WAIT_MS_KEY];
163
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
164
+ resolve(raw);
165
+ return;
166
+ }
167
+ if (typeof raw === "string") {
168
+ const parsed = Number(raw);
169
+ if (Number.isFinite(parsed) && parsed > 0) {
170
+ resolve(parsed);
171
+ return;
172
+ }
173
+ }
174
+ resolve(DEFAULT_PERMISSION_PROMPT_WAIT_MS);
175
+ }
176
+ );
177
+ });
178
+ };
179
+ var isSiteAllowed = async (siteKey) => {
180
+ const key = normalizeSiteKey(siteKey);
181
+ const allowlist = await readAllowlistRaw();
182
+ return Boolean(allowlist[key]);
183
+ };
184
+ var allowSiteAlways = async (siteKey, now = /* @__PURE__ */ new Date()) => {
185
+ const key = normalizeSiteKey(siteKey);
186
+ const allowlist = await readAllowlistRaw();
187
+ const nowIso2 = now.toISOString();
188
+ const existing = allowlist[key];
189
+ allowlist[key] = {
190
+ createdAt: existing?.createdAt ?? nowIso2,
191
+ lastUsedAt: nowIso2
192
+ };
193
+ await writeAllowlistRaw(allowlist);
194
+ };
195
+ var touchSiteLastUsed = async (siteKey, now = /* @__PURE__ */ new Date()) => {
196
+ const key = normalizeSiteKey(siteKey);
197
+ const allowlist = await readAllowlistRaw();
198
+ const existing = allowlist[key];
199
+ if (!existing) {
200
+ return;
201
+ }
202
+ allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };
203
+ await writeAllowlistRaw(allowlist);
204
+ };
205
+
206
+ // packages/extension/src/permission-prompt.ts
207
+ var PERMISSION_PROMPT_PORT_NAME = "permission_prompt";
208
+ var defaultMakeRequestId = () => {
209
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
210
+ return crypto.randomUUID();
211
+ }
212
+ return `perm-${Date.now()}-${Math.random().toString(16).slice(2)}`;
213
+ };
214
+ var defaultOpenWindow = async (url) => {
215
+ return await new Promise((resolve, reject) => {
216
+ chrome.windows.create(
217
+ {
218
+ type: "popup",
219
+ url,
220
+ focused: true,
221
+ width: 460,
222
+ height: 420
223
+ },
224
+ (win) => {
225
+ const err = chrome.runtime.lastError;
226
+ if (err) {
227
+ reject(new Error(err.message));
228
+ return;
229
+ }
230
+ const windowId = win?.id;
231
+ if (typeof windowId !== "number") {
232
+ reject(new Error("Prompt window id missing."));
233
+ return;
234
+ }
235
+ resolve(windowId);
236
+ }
237
+ );
238
+ });
239
+ };
240
+ var defaultCloseWindow = async (windowId) => {
241
+ return await new Promise((resolve) => {
242
+ chrome.windows.remove(windowId, () => resolve());
243
+ });
244
+ };
245
+ var delay = async (ms) => {
246
+ return await new Promise((resolve) => {
247
+ setTimeout(resolve, ms);
248
+ });
249
+ };
250
+ var PermissionPromptController = class {
251
+ constructor(deps) {
252
+ this.stateBySite = /* @__PURE__ */ new Map();
253
+ this.stateByRequestId = /* @__PURE__ */ new Map();
254
+ this.stateByWindowId = /* @__PURE__ */ new Map();
255
+ this.deps = {
256
+ openWindow: deps?.openWindow ?? defaultOpenWindow,
257
+ closeWindow: deps?.closeWindow ?? defaultCloseWindow,
258
+ getWaitMs: deps?.getWaitMs ?? readPermissionPromptWaitMs,
259
+ persistAlwaysAllow: deps?.persistAlwaysAllow ?? allowSiteAlways,
260
+ makeRequestId: deps?.makeRequestId ?? defaultMakeRequestId
261
+ };
262
+ }
263
+ async requestPermission(request) {
264
+ const siteKey = request.siteKey.toLowerCase();
265
+ let state = this.stateBySite.get(siteKey);
266
+ if (!state) {
267
+ const requestId = this.deps.makeRequestId();
268
+ state = {
269
+ siteKey,
270
+ action: request.action,
271
+ requestId,
272
+ windowId: null,
273
+ decided: null,
274
+ waiters: /* @__PURE__ */ new Set()
275
+ };
276
+ this.stateBySite.set(siteKey, state);
277
+ this.stateByRequestId.set(requestId, state);
278
+ const url = this.buildPromptUrl(state);
279
+ const windowId = await this.deps.openWindow(url);
280
+ state.windowId = windowId;
281
+ this.stateByWindowId.set(windowId, state);
282
+ if (state.decided) {
283
+ await this.deps.closeWindow(windowId);
284
+ this.cleanupState(state);
285
+ }
286
+ }
287
+ const waitMs = await this.deps.getWaitMs();
288
+ const decision = await this.waitForDecisionOrTimeout(state, waitMs);
289
+ if (!decision) {
290
+ return { kind: "timed_out", waitMs };
291
+ }
292
+ return { kind: decision };
293
+ }
294
+ handleConnect(port) {
295
+ if (!port || typeof port !== "object") {
296
+ return;
297
+ }
298
+ const p = port;
299
+ if (p.name !== PERMISSION_PROMPT_PORT_NAME) {
300
+ return;
301
+ }
302
+ const onMessage = p.onMessage;
303
+ if (!onMessage || typeof onMessage.addListener !== "function") {
304
+ return;
305
+ }
306
+ onMessage.addListener((message) => {
307
+ void this.handlePortMessage(message).catch((error) => {
308
+ console.error(
309
+ "PermissionPromptController handlePortMessage failed:",
310
+ error
311
+ );
312
+ });
313
+ });
314
+ }
315
+ handleWindowRemoved(windowId) {
316
+ const state = this.stateByWindowId.get(windowId);
317
+ if (!state) {
318
+ return;
319
+ }
320
+ this.cleanupState(state);
321
+ }
322
+ buildPromptUrl(state) {
323
+ const base = chrome.runtime.getURL("permission.html");
324
+ const u = new URL(base);
325
+ u.searchParams.set("requestId", state.requestId);
326
+ u.searchParams.set("site", state.siteKey);
327
+ u.searchParams.set("action", state.action);
328
+ return u.toString();
329
+ }
330
+ async waitForDecisionOrTimeout(state, waitMs) {
331
+ if (state.decided) {
332
+ return state.decided;
333
+ }
334
+ let waiter = null;
335
+ const decisionPromise = new Promise((resolve) => {
336
+ waiter = resolve;
337
+ state.waiters.add(resolve);
338
+ });
339
+ const winner = await Promise.race([
340
+ decisionPromise,
341
+ delay(waitMs).then(() => null)
342
+ ]);
343
+ if (winner === null && waiter) {
344
+ state.waiters.delete(waiter);
345
+ }
346
+ return winner;
347
+ }
348
+ async handlePortMessage(message) {
349
+ if (!message || typeof message !== "object") {
350
+ return;
351
+ }
352
+ const m = message;
353
+ if (m.type !== "decision") {
354
+ return;
355
+ }
356
+ const requestId = m.requestId;
357
+ const decision = m.decision;
358
+ if (typeof requestId !== "string" || requestId.length === 0) {
359
+ return;
360
+ }
361
+ if (decision !== "allow_once" && decision !== "allow_always" && decision !== "deny") {
362
+ return;
363
+ }
364
+ const state = this.stateByRequestId.get(requestId);
365
+ if (!state) {
366
+ return;
367
+ }
368
+ state.decided = decision;
369
+ if (decision === "allow_always") {
370
+ await this.deps.persistAlwaysAllow(state.siteKey);
371
+ }
372
+ for (const waiter of state.waiters) {
373
+ waiter(decision);
374
+ }
375
+ state.waiters.clear();
376
+ if (typeof state.windowId === "number") {
377
+ await this.deps.closeWindow(state.windowId);
378
+ this.cleanupState(state);
379
+ }
380
+ }
381
+ cleanupState(state) {
382
+ this.stateBySite.delete(state.siteKey);
383
+ this.stateByRequestId.delete(state.requestId);
384
+ if (typeof state.windowId === "number") {
385
+ this.stateByWindowId.delete(state.windowId);
386
+ }
387
+ }
388
+ };
389
+
95
390
  // packages/extension/src/background.ts
96
391
  var DEFAULT_CORE_PORT = 3210;
97
392
  var CORE_PORT_KEY = "corePort";
@@ -100,12 +395,15 @@ var DEBUGGER_PROTOCOL_VERSION = "1.3";
100
395
  var DEBUGGER_IDLE_TIMEOUT_KEY = "debuggerIdleTimeoutMs";
101
396
  var DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS = 15e3;
102
397
  var DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS = 1e4;
398
+ var AGENT_TAB_ID_KEY = "agentTabId";
399
+ var AGENT_TAB_GROUP_TITLE = "\u{1F309} Browser Bridge";
103
400
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
104
401
  var makeEventId = /* @__PURE__ */ (() => {
105
402
  let counter = 0;
106
403
  return () => `evt-${Date.now()}-${counter += 1}`;
107
404
  })();
108
405
  var lastActiveAtByTab = /* @__PURE__ */ new Map();
406
+ var agentTabId = null;
109
407
  var ensureLastActiveAt = (tabId) => {
110
408
  const existing = lastActiveAtByTab.get(tabId);
111
409
  if (existing) {
@@ -353,36 +651,194 @@ var getActiveTabId = async () => {
353
651
  }
354
652
  throw new Error("No active tab found.");
355
653
  };
356
- var sendToTab = async (tabId, action, params) => {
654
+ var clearAgentTarget = () => {
655
+ agentTabId = null;
656
+ void writeAgentTabId(null);
657
+ };
658
+ var queryActiveTabIdInWindow = async (windowId) => {
659
+ const tabs = await wrapChromeCallback(
660
+ (callback) => chrome.tabs.query({ active: true, windowId }, callback)
661
+ );
662
+ const first = tabs[0];
663
+ if (first && typeof first.id === "number") {
664
+ return first.id;
665
+ }
666
+ const anyTabs = await wrapChromeCallback(
667
+ (callback) => chrome.tabs.query({ windowId }, callback)
668
+ );
669
+ const fallback = anyTabs[0];
670
+ if (fallback && typeof fallback.id === "number") {
671
+ return fallback.id;
672
+ }
673
+ throw new Error("No tab found for window.");
674
+ };
675
+ var ensureAgentTabGroup = async (tabId, windowId) => {
676
+ if (typeof chrome.tabs?.group !== "function") {
677
+ return;
678
+ }
679
+ if (!chrome.tabGroups || typeof chrome.tabGroups.update !== "function") {
680
+ return;
681
+ }
682
+ try {
683
+ const groupId = await wrapChromeCallback(
684
+ (callback) => chrome.tabs.group(
685
+ { tabIds: tabId, createProperties: { windowId } },
686
+ callback
687
+ )
688
+ );
689
+ await wrapChromeVoid(
690
+ (callback) => chrome.tabGroups.update(
691
+ groupId,
692
+ { title: AGENT_TAB_GROUP_TITLE },
693
+ () => callback()
694
+ )
695
+ );
696
+ } catch (error) {
697
+ console.debug("Failed to create/update agent tab group.", error);
698
+ }
699
+ };
700
+ var createAgentWindow = async () => {
701
+ const created = await wrapChromeCallback(
702
+ (callback) => chrome.windows.create({ url: "about:blank", focused: true }, callback)
703
+ );
704
+ const windowId = created.id;
705
+ if (typeof windowId !== "number") {
706
+ throw new Error("Failed to create agent window.");
707
+ }
708
+ const tabId = await queryActiveTabIdInWindow(windowId);
709
+ await ensureAgentTabGroup(tabId, windowId);
710
+ return tabId;
711
+ };
712
+ var readAgentTabId = async () => {
357
713
  return await new Promise((resolve) => {
358
- const message = { action, params };
359
- chrome.tabs.sendMessage(tabId, message, (response) => {
714
+ chrome.storage.local.get(
715
+ [AGENT_TAB_ID_KEY],
716
+ (result) => {
717
+ const raw = result?.[AGENT_TAB_ID_KEY];
718
+ resolve(typeof raw === "number" && Number.isFinite(raw) ? raw : null);
719
+ }
720
+ );
721
+ });
722
+ };
723
+ var writeAgentTabId = async (tabId) => {
724
+ await new Promise((resolve, reject) => {
725
+ const done = () => {
360
726
  const error = chrome.runtime.lastError;
361
727
  if (error) {
362
- resolve({
363
- ok: false,
364
- error: {
365
- code: "EVALUATION_FAILED",
366
- message: error.message,
367
- retryable: false
368
- }
369
- });
728
+ reject(new Error(error.message));
370
729
  return;
371
730
  }
372
- if (!response || typeof response !== "object") {
373
- resolve({
374
- ok: false,
375
- error: {
376
- code: "EVALUATION_FAILED",
377
- message: "Empty response from content script.",
378
- retryable: false
379
- }
380
- });
381
- return;
731
+ resolve();
732
+ };
733
+ if (tabId === null) {
734
+ chrome.storage.local.remove([AGENT_TAB_ID_KEY], done);
735
+ return;
736
+ }
737
+ chrome.storage.local.set({ [AGENT_TAB_ID_KEY]: tabId }, done);
738
+ }).catch((error) => {
739
+ console.debug("Failed to persist agentTabId.", error);
740
+ });
741
+ };
742
+ var getOrCreateAgentTabId = async () => {
743
+ if (agentTabId !== null) {
744
+ try {
745
+ const tab = await getTab(agentTabId);
746
+ const url = tab.url;
747
+ if (typeof url === "string" && isRestrictedUrl(url)) {
748
+ throw new Error(`Agent tab points at restricted URL: ${url}`);
382
749
  }
383
- resolve(response);
750
+ return agentTabId;
751
+ } catch {
752
+ clearAgentTarget();
753
+ }
754
+ }
755
+ const stored = await readAgentTabId();
756
+ if (stored !== null) {
757
+ try {
758
+ const tab = await getTab(stored);
759
+ const url = tab.url;
760
+ if (typeof url === "string" && isRestrictedUrl(url)) {
761
+ throw new Error(`Stored agent tab points at restricted URL: ${url}`);
762
+ }
763
+ agentTabId = stored;
764
+ ensureLastActiveAt(stored);
765
+ markTabActive(stored);
766
+ return stored;
767
+ } catch {
768
+ await writeAgentTabId(null);
769
+ }
770
+ }
771
+ const tabId = await createAgentWindow();
772
+ agentTabId = tabId;
773
+ ensureLastActiveAt(tabId);
774
+ markTabActive(tabId);
775
+ await writeAgentTabId(tabId);
776
+ return tabId;
777
+ };
778
+ var getDefaultTabId = async () => {
779
+ try {
780
+ return await getOrCreateAgentTabId();
781
+ } catch (error) {
782
+ console.warn(
783
+ "Failed to create agent window/tab; falling back to active tab.",
784
+ error
785
+ );
786
+ return await getActiveTabId();
787
+ }
788
+ };
789
+ var sendToTab = async (tabId, action, params) => {
790
+ const attemptSend = async () => {
791
+ return await new Promise((resolve) => {
792
+ const message = { action, params };
793
+ chrome.tabs.sendMessage(tabId, message, (response) => {
794
+ const error = chrome.runtime.lastError;
795
+ if (error) {
796
+ resolve({
797
+ ok: false,
798
+ error: {
799
+ code: "EVALUATION_FAILED",
800
+ message: error.message,
801
+ retryable: false
802
+ }
803
+ });
804
+ return;
805
+ }
806
+ if (!response || typeof response !== "object") {
807
+ resolve({
808
+ ok: false,
809
+ error: {
810
+ code: "EVALUATION_FAILED",
811
+ message: "Empty response from content script.",
812
+ retryable: false
813
+ }
814
+ });
815
+ return;
816
+ }
817
+ resolve(response);
818
+ });
384
819
  });
385
- });
820
+ };
821
+ const MAX_ATTEMPTS = 5;
822
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
823
+ const result = await attemptSend();
824
+ if (result.ok) {
825
+ return result;
826
+ }
827
+ const message = result.error?.message;
828
+ const isNoReceiver = typeof message === "string" && message.toLowerCase().includes("receiving end does not exist");
829
+ if (!isNoReceiver || attempt === MAX_ATTEMPTS) {
830
+ return result;
831
+ }
832
+ await delayMs(200);
833
+ }
834
+ return {
835
+ ok: false,
836
+ error: {
837
+ code: "INTERNAL",
838
+ message: "Failed to send message to content script.",
839
+ retryable: false
840
+ }
841
+ };
386
842
  };
387
843
  var waitForDomContentLoaded = async (tabId, timeoutMs) => {
388
844
  return await new Promise((resolve, reject) => {
@@ -447,13 +903,13 @@ var DriveSocket = class {
447
903
  if (this.reconnectTimer !== null) {
448
904
  return;
449
905
  }
450
- const delay = this.reconnectDelayMs;
906
+ const delay2 = this.reconnectDelayMs;
451
907
  this.reconnectTimer = self.setTimeout(() => {
452
908
  this.reconnectTimer = null;
453
909
  void this.connect().catch((error) => {
454
910
  console.error("DriveSocket reconnect failed:", error);
455
911
  });
456
- }, delay);
912
+ }, delay2);
457
913
  this.reconnectDelayMs = Math.min(
458
914
  this.maxReconnectDelayMs,
459
915
  this.reconnectDelayMs * 2
@@ -573,10 +1029,17 @@ var DriveSocket = class {
573
1029
  }
574
1030
  async handleRequest(message) {
575
1031
  let driveMessage = null;
1032
+ let gatedSiteKey = null;
1033
+ let touchGatedSiteOnSuccess = false;
576
1034
  const respondOk = (result) => {
577
1035
  if (!driveMessage) {
578
1036
  return;
579
1037
  }
1038
+ if (touchGatedSiteOnSuccess && gatedSiteKey) {
1039
+ void touchSiteLastUsed(gatedSiteKey).catch((error) => {
1040
+ console.error("Failed to touch site allowlist entry:", error);
1041
+ });
1042
+ }
580
1043
  const response = {
581
1044
  id: driveMessage.id,
582
1045
  action: driveMessage.action,
@@ -609,6 +1072,156 @@ var DriveSocket = class {
609
1072
  return;
610
1073
  }
611
1074
  driveMessage = message;
1075
+ const gatedActions = /* @__PURE__ */ new Set([
1076
+ "drive.navigate",
1077
+ "drive.go_back",
1078
+ "drive.go_forward",
1079
+ "drive.back",
1080
+ "drive.forward",
1081
+ "drive.click",
1082
+ "drive.hover",
1083
+ "drive.select",
1084
+ "drive.type",
1085
+ "drive.fill_form",
1086
+ "drive.drag",
1087
+ "drive.handle_dialog",
1088
+ "drive.key",
1089
+ "drive.key_press",
1090
+ "drive.scroll",
1091
+ "drive.screenshot",
1092
+ "drive.wait_for"
1093
+ ]);
1094
+ const gateDriveAction = async () => {
1095
+ const action = message.action;
1096
+ if (!gatedActions.has(action)) {
1097
+ return { ok: true, siteKey: null, touchOnSuccess: false };
1098
+ }
1099
+ const params = message.params ?? {};
1100
+ let siteKey = null;
1101
+ if (action === "drive.navigate") {
1102
+ const url = params.url;
1103
+ if (typeof url !== "string" || url.length === 0) {
1104
+ return { ok: true, siteKey: null, touchOnSuccess: false };
1105
+ }
1106
+ if (isRestrictedUrl(url)) {
1107
+ return {
1108
+ ok: false,
1109
+ error: {
1110
+ code: "NOT_SUPPORTED",
1111
+ message: "Navigation is not supported for this URL.",
1112
+ retryable: false,
1113
+ details: { url }
1114
+ }
1115
+ };
1116
+ }
1117
+ siteKey = siteKeyFromUrl(url);
1118
+ if (!siteKey) {
1119
+ return {
1120
+ ok: false,
1121
+ error: {
1122
+ code: "INVALID_ARGUMENT",
1123
+ message: "Unable to resolve site permission key for url.",
1124
+ retryable: false,
1125
+ details: { url }
1126
+ }
1127
+ };
1128
+ }
1129
+ } else {
1130
+ const tabId = params.tab_id;
1131
+ if (tabId !== void 0 && typeof tabId !== "number") {
1132
+ return { ok: true, siteKey: null, touchOnSuccess: false };
1133
+ }
1134
+ const resolvedTabId = typeof tabId === "number" ? tabId : await getDefaultTabId();
1135
+ const tab = await getTab(resolvedTabId);
1136
+ const url = tab.url;
1137
+ if (typeof url !== "string" || url.length === 0) {
1138
+ return {
1139
+ ok: false,
1140
+ error: {
1141
+ code: "FAILED_PRECONDITION",
1142
+ message: "Active tab URL is unavailable for permission gating.",
1143
+ retryable: false,
1144
+ details: { tab_id: resolvedTabId }
1145
+ }
1146
+ };
1147
+ }
1148
+ if (isRestrictedUrl(url)) {
1149
+ const message2 = action === "drive.screenshot" ? "Screenshots are not supported for this URL." : "This action is not supported for this URL.";
1150
+ return {
1151
+ ok: false,
1152
+ error: {
1153
+ code: "NOT_SUPPORTED",
1154
+ message: message2,
1155
+ retryable: false,
1156
+ details: { url }
1157
+ }
1158
+ };
1159
+ }
1160
+ siteKey = siteKeyFromUrl(url);
1161
+ if (!siteKey) {
1162
+ return {
1163
+ ok: false,
1164
+ error: {
1165
+ code: "FAILED_PRECONDITION",
1166
+ message: "Unable to resolve site permission key for active tab.",
1167
+ retryable: false,
1168
+ details: { url, tab_id: resolvedTabId }
1169
+ }
1170
+ };
1171
+ }
1172
+ }
1173
+ if (await isSiteAllowed(siteKey)) {
1174
+ return { ok: true, siteKey, touchOnSuccess: true };
1175
+ }
1176
+ const decision = await permissionPrompts.requestPermission({
1177
+ siteKey,
1178
+ action
1179
+ });
1180
+ if (decision.kind === "timed_out") {
1181
+ return {
1182
+ ok: false,
1183
+ error: {
1184
+ code: "PERMISSION_PROMPT_TIMEOUT",
1185
+ message: `Permission prompt timed out for ${siteKey}.`,
1186
+ retryable: true,
1187
+ details: {
1188
+ reason: "prompt_timed_out",
1189
+ site: siteKey,
1190
+ action,
1191
+ wait_ms: decision.waitMs
1192
+ }
1193
+ }
1194
+ };
1195
+ }
1196
+ if (decision.kind === "deny") {
1197
+ return {
1198
+ ok: false,
1199
+ error: {
1200
+ code: "PERMISSION_DENIED",
1201
+ message: `User denied Browser Bridge permission for ${siteKey}.`,
1202
+ retryable: false,
1203
+ details: {
1204
+ reason: "user_denied",
1205
+ site: siteKey,
1206
+ action,
1207
+ next_step: "Ask the user to approve the permission prompt (Allow/Always allow) or allow the site in the extension options page, then retry the command."
1208
+ }
1209
+ }
1210
+ };
1211
+ }
1212
+ if (decision.kind === "allow_always") {
1213
+ await allowSiteAlways(siteKey);
1214
+ return { ok: true, siteKey, touchOnSuccess: true };
1215
+ }
1216
+ return { ok: true, siteKey, touchOnSuccess: false };
1217
+ };
1218
+ const gated = await gateDriveAction();
1219
+ if (!gated.ok) {
1220
+ respondError(gated.error);
1221
+ return;
1222
+ }
1223
+ gatedSiteKey = gated.siteKey;
1224
+ touchGatedSiteOnSuccess = gated.touchOnSuccess;
612
1225
  switch (message.action) {
613
1226
  case "drive.ping": {
614
1227
  respondOk({ ok: true });
@@ -635,7 +1248,7 @@ var DriveSocket = class {
635
1248
  return;
636
1249
  }
637
1250
  if (tabId === void 0) {
638
- tabId = await getActiveTabId();
1251
+ tabId = await getDefaultTabId();
639
1252
  }
640
1253
  const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
641
1254
  await wrapChromeVoid(
@@ -672,7 +1285,7 @@ var DriveSocket = class {
672
1285
  return;
673
1286
  }
674
1287
  if (tabId === void 0) {
675
- tabId = await getActiveTabId();
1288
+ tabId = await getDefaultTabId();
676
1289
  }
677
1290
  try {
678
1291
  const isBack = message.action === "drive.go_back" || message.action === "drive.back";
@@ -753,6 +1366,9 @@ var DriveSocket = class {
753
1366
  await wrapChromeVoid(
754
1367
  (callback) => chrome.tabs.remove(tabId, () => callback())
755
1368
  );
1369
+ if (agentTabId === tabId) {
1370
+ clearAgentTarget();
1371
+ }
756
1372
  lastActiveAtByTab.delete(tabId);
757
1373
  respondOk({ ok: true });
758
1374
  this.sendTabReport();
@@ -788,7 +1404,7 @@ var DriveSocket = class {
788
1404
  return;
789
1405
  }
790
1406
  if (tabId === void 0) {
791
- tabId = await getActiveTabId();
1407
+ tabId = await getDefaultTabId();
792
1408
  }
793
1409
  const error = await this.ensureDebuggerAttached(tabId);
794
1410
  if (error) {
@@ -836,7 +1452,7 @@ var DriveSocket = class {
836
1452
  return;
837
1453
  }
838
1454
  if (tabId === void 0) {
839
- tabId = await getActiveTabId();
1455
+ tabId = await getDefaultTabId();
840
1456
  }
841
1457
  const result = await sendToTab(
842
1458
  tabId,
@@ -862,7 +1478,7 @@ var DriveSocket = class {
862
1478
  return;
863
1479
  }
864
1480
  if (tabId === void 0) {
865
- tabId = await getActiveTabId();
1481
+ tabId = await getDefaultTabId();
866
1482
  }
867
1483
  const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
868
1484
  const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
@@ -1600,6 +2216,13 @@ var DebuggerTimeoutError = class extends Error {
1600
2216
  }
1601
2217
  };
1602
2218
  var socket = new DriveSocket();
2219
+ var permissionPrompts = new PermissionPromptController();
2220
+ chrome.runtime.onConnect.addListener((port) => {
2221
+ permissionPrompts.handleConnect(port);
2222
+ });
2223
+ chrome.windows.onRemoved.addListener((windowId) => {
2224
+ permissionPrompts.handleWindowRemoved(windowId);
2225
+ });
1603
2226
  chrome.tabs.onActivated.addListener((activeInfo) => {
1604
2227
  markTabActive(activeInfo.tabId);
1605
2228
  socket.sendTabReport();
@@ -1623,6 +2246,9 @@ chrome.tabs.onUpdated.addListener(
1623
2246
  }
1624
2247
  );
1625
2248
  chrome.tabs.onRemoved.addListener((tabId) => {
2249
+ if (agentTabId === tabId) {
2250
+ clearAgentTarget();
2251
+ }
1626
2252
  lastActiveAtByTab.delete(tabId);
1627
2253
  socket.sendTabReport();
1628
2254
  });