@btraut/browser-bridge 0.4.3 → 0.6.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,318 @@ 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 SITE_PERMISSIONS_MODE_KEY = "sitePermissionsMode";
100
+ var DEFAULT_SITE_PERMISSIONS_MODE = "granular";
101
+ var siteKeyFromUrl = (rawUrl) => {
102
+ if (!rawUrl || typeof rawUrl !== "string") {
103
+ return null;
104
+ }
105
+ try {
106
+ const parsed = new URL(rawUrl);
107
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
108
+ return null;
109
+ }
110
+ if (!parsed.hostname) {
111
+ return null;
112
+ }
113
+ return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
114
+ } catch {
115
+ return null;
116
+ }
117
+ };
118
+ var isAllowlistEntry = (value) => {
119
+ if (!value || typeof value !== "object") {
120
+ return false;
121
+ }
122
+ const v = value;
123
+ return typeof v.createdAt === "string" && typeof v.lastUsedAt === "string";
124
+ };
125
+ var normalizeSiteKey = (siteKey) => siteKey.toLowerCase();
126
+ var readAllowlistRaw = async () => {
127
+ return await new Promise((resolve) => {
128
+ chrome.storage.local.get(
129
+ [SITE_ALLOWLIST_KEY],
130
+ (result) => {
131
+ const raw = result?.[SITE_ALLOWLIST_KEY];
132
+ if (!raw || typeof raw !== "object") {
133
+ resolve({});
134
+ return;
135
+ }
136
+ const out = {};
137
+ for (const [k, v] of Object.entries(raw)) {
138
+ if (typeof k !== "string") {
139
+ continue;
140
+ }
141
+ if (!isAllowlistEntry(v)) {
142
+ continue;
143
+ }
144
+ out[normalizeSiteKey(k)] = v;
145
+ }
146
+ resolve(out);
147
+ }
148
+ );
149
+ });
150
+ };
151
+ var writeAllowlistRaw = async (allowlist) => {
152
+ return await new Promise((resolve) => {
153
+ chrome.storage.local.set(
154
+ { [SITE_ALLOWLIST_KEY]: allowlist },
155
+ () => resolve()
156
+ );
157
+ });
158
+ };
159
+ var readSitePermissionsMode = async () => {
160
+ return await new Promise((resolve) => {
161
+ chrome.storage.local.get(
162
+ [SITE_PERMISSIONS_MODE_KEY],
163
+ (result) => {
164
+ const raw = result?.[SITE_PERMISSIONS_MODE_KEY];
165
+ if (raw === "granular" || raw === "bypass") {
166
+ resolve(raw);
167
+ return;
168
+ }
169
+ resolve(DEFAULT_SITE_PERMISSIONS_MODE);
170
+ }
171
+ );
172
+ });
173
+ };
174
+ var readPermissionPromptWaitMs = async () => {
175
+ return await new Promise((resolve) => {
176
+ chrome.storage.local.get(
177
+ [PERMISSION_PROMPT_WAIT_MS_KEY],
178
+ (result) => {
179
+ const raw = result?.[PERMISSION_PROMPT_WAIT_MS_KEY];
180
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
181
+ resolve(raw);
182
+ return;
183
+ }
184
+ if (typeof raw === "string") {
185
+ const parsed = Number(raw);
186
+ if (Number.isFinite(parsed) && parsed > 0) {
187
+ resolve(parsed);
188
+ return;
189
+ }
190
+ }
191
+ resolve(DEFAULT_PERMISSION_PROMPT_WAIT_MS);
192
+ }
193
+ );
194
+ });
195
+ };
196
+ var isSiteAllowed = async (siteKey) => {
197
+ const key = normalizeSiteKey(siteKey);
198
+ const allowlist = await readAllowlistRaw();
199
+ return Boolean(allowlist[key]);
200
+ };
201
+ var allowSiteAlways = async (siteKey, now = /* @__PURE__ */ new Date()) => {
202
+ const key = normalizeSiteKey(siteKey);
203
+ const allowlist = await readAllowlistRaw();
204
+ const nowIso2 = now.toISOString();
205
+ const existing = allowlist[key];
206
+ allowlist[key] = {
207
+ createdAt: existing?.createdAt ?? nowIso2,
208
+ lastUsedAt: nowIso2
209
+ };
210
+ await writeAllowlistRaw(allowlist);
211
+ };
212
+ var touchSiteLastUsed = async (siteKey, now = /* @__PURE__ */ new Date()) => {
213
+ const key = normalizeSiteKey(siteKey);
214
+ const allowlist = await readAllowlistRaw();
215
+ const existing = allowlist[key];
216
+ if (!existing) {
217
+ return;
218
+ }
219
+ allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };
220
+ await writeAllowlistRaw(allowlist);
221
+ };
222
+
223
+ // packages/extension/src/permission-prompt.ts
224
+ var PERMISSION_PROMPT_PORT_NAME = "permission_prompt";
225
+ var defaultMakeRequestId = () => {
226
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
227
+ return crypto.randomUUID();
228
+ }
229
+ return `perm-${Date.now()}-${Math.random().toString(16).slice(2)}`;
230
+ };
231
+ var defaultOpenWindow = async (url) => {
232
+ return await new Promise((resolve, reject) => {
233
+ chrome.windows.create(
234
+ {
235
+ type: "popup",
236
+ url,
237
+ focused: true,
238
+ width: 460,
239
+ height: 420
240
+ },
241
+ (win) => {
242
+ const err = chrome.runtime.lastError;
243
+ if (err) {
244
+ reject(new Error(err.message));
245
+ return;
246
+ }
247
+ const windowId = win?.id;
248
+ if (typeof windowId !== "number") {
249
+ reject(new Error("Prompt window id missing."));
250
+ return;
251
+ }
252
+ resolve(windowId);
253
+ }
254
+ );
255
+ });
256
+ };
257
+ var defaultCloseWindow = async (windowId) => {
258
+ return await new Promise((resolve) => {
259
+ chrome.windows.remove(windowId, () => resolve());
260
+ });
261
+ };
262
+ var delay = async (ms) => {
263
+ return await new Promise((resolve) => {
264
+ setTimeout(resolve, ms);
265
+ });
266
+ };
267
+ var PermissionPromptController = class {
268
+ constructor(deps) {
269
+ this.stateBySite = /* @__PURE__ */ new Map();
270
+ this.stateByRequestId = /* @__PURE__ */ new Map();
271
+ this.stateByWindowId = /* @__PURE__ */ new Map();
272
+ this.deps = {
273
+ openWindow: deps?.openWindow ?? defaultOpenWindow,
274
+ closeWindow: deps?.closeWindow ?? defaultCloseWindow,
275
+ getWaitMs: deps?.getWaitMs ?? readPermissionPromptWaitMs,
276
+ persistAlwaysAllow: deps?.persistAlwaysAllow ?? allowSiteAlways,
277
+ makeRequestId: deps?.makeRequestId ?? defaultMakeRequestId
278
+ };
279
+ }
280
+ async requestPermission(request) {
281
+ const siteKey = request.siteKey.toLowerCase();
282
+ let state = this.stateBySite.get(siteKey);
283
+ if (!state) {
284
+ const requestId = this.deps.makeRequestId();
285
+ state = {
286
+ siteKey,
287
+ action: request.action,
288
+ requestId,
289
+ windowId: null,
290
+ decided: null,
291
+ waiters: /* @__PURE__ */ new Set()
292
+ };
293
+ this.stateBySite.set(siteKey, state);
294
+ this.stateByRequestId.set(requestId, state);
295
+ const url = this.buildPromptUrl(state);
296
+ const windowId = await this.deps.openWindow(url);
297
+ state.windowId = windowId;
298
+ this.stateByWindowId.set(windowId, state);
299
+ if (state.decided) {
300
+ await this.deps.closeWindow(windowId);
301
+ this.cleanupState(state);
302
+ }
303
+ }
304
+ const waitMs = await this.deps.getWaitMs();
305
+ const decision = await this.waitForDecisionOrTimeout(state, waitMs);
306
+ if (!decision) {
307
+ return { kind: "timed_out", waitMs };
308
+ }
309
+ return { kind: decision };
310
+ }
311
+ handleConnect(port) {
312
+ if (!port || typeof port !== "object") {
313
+ return;
314
+ }
315
+ const p = port;
316
+ if (p.name !== PERMISSION_PROMPT_PORT_NAME) {
317
+ return;
318
+ }
319
+ const onMessage = p.onMessage;
320
+ if (!onMessage || typeof onMessage.addListener !== "function") {
321
+ return;
322
+ }
323
+ onMessage.addListener((message) => {
324
+ void this.handlePortMessage(message).catch((error) => {
325
+ console.error(
326
+ "PermissionPromptController handlePortMessage failed:",
327
+ error
328
+ );
329
+ });
330
+ });
331
+ }
332
+ handleWindowRemoved(windowId) {
333
+ const state = this.stateByWindowId.get(windowId);
334
+ if (!state) {
335
+ return;
336
+ }
337
+ this.cleanupState(state);
338
+ }
339
+ buildPromptUrl(state) {
340
+ const base = chrome.runtime.getURL("permission.html");
341
+ const u = new URL(base);
342
+ u.searchParams.set("requestId", state.requestId);
343
+ u.searchParams.set("site", state.siteKey);
344
+ u.searchParams.set("action", state.action);
345
+ return u.toString();
346
+ }
347
+ async waitForDecisionOrTimeout(state, waitMs) {
348
+ if (state.decided) {
349
+ return state.decided;
350
+ }
351
+ let waiter = null;
352
+ const decisionPromise = new Promise((resolve) => {
353
+ waiter = resolve;
354
+ state.waiters.add(resolve);
355
+ });
356
+ const winner = await Promise.race([
357
+ decisionPromise,
358
+ delay(waitMs).then(() => null)
359
+ ]);
360
+ if (winner === null && waiter) {
361
+ state.waiters.delete(waiter);
362
+ }
363
+ return winner;
364
+ }
365
+ async handlePortMessage(message) {
366
+ if (!message || typeof message !== "object") {
367
+ return;
368
+ }
369
+ const m = message;
370
+ if (m.type !== "decision") {
371
+ return;
372
+ }
373
+ const requestId = m.requestId;
374
+ const decision = m.decision;
375
+ if (typeof requestId !== "string" || requestId.length === 0) {
376
+ return;
377
+ }
378
+ if (decision !== "allow_once" && decision !== "allow_always" && decision !== "deny") {
379
+ return;
380
+ }
381
+ const state = this.stateByRequestId.get(requestId);
382
+ if (!state) {
383
+ return;
384
+ }
385
+ state.decided = decision;
386
+ if (decision === "allow_always") {
387
+ await this.deps.persistAlwaysAllow(state.siteKey);
388
+ }
389
+ for (const waiter of state.waiters) {
390
+ waiter(decision);
391
+ }
392
+ state.waiters.clear();
393
+ if (typeof state.windowId === "number") {
394
+ await this.deps.closeWindow(state.windowId);
395
+ this.cleanupState(state);
396
+ }
397
+ }
398
+ cleanupState(state) {
399
+ this.stateBySite.delete(state.siteKey);
400
+ this.stateByRequestId.delete(state.requestId);
401
+ if (typeof state.windowId === "number") {
402
+ this.stateByWindowId.delete(state.windowId);
403
+ }
404
+ }
405
+ };
406
+
95
407
  // packages/extension/src/background.ts
96
408
  var DEFAULT_CORE_PORT = 3210;
97
409
  var CORE_PORT_KEY = "corePort";
@@ -100,12 +412,15 @@ var DEBUGGER_PROTOCOL_VERSION = "1.3";
100
412
  var DEBUGGER_IDLE_TIMEOUT_KEY = "debuggerIdleTimeoutMs";
101
413
  var DEFAULT_DEBUGGER_IDLE_TIMEOUT_MS = 15e3;
102
414
  var DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS = 1e4;
415
+ var AGENT_TAB_ID_KEY = "agentTabId";
416
+ var AGENT_TAB_GROUP_TITLE = "\u{1F309} Browser Bridge";
103
417
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
104
418
  var makeEventId = /* @__PURE__ */ (() => {
105
419
  let counter = 0;
106
420
  return () => `evt-${Date.now()}-${counter += 1}`;
107
421
  })();
108
422
  var lastActiveAtByTab = /* @__PURE__ */ new Map();
423
+ var agentTabId = null;
109
424
  var ensureLastActiveAt = (tabId) => {
110
425
  const existing = lastActiveAtByTab.get(tabId);
111
426
  if (existing) {
@@ -353,36 +668,194 @@ var getActiveTabId = async () => {
353
668
  }
354
669
  throw new Error("No active tab found.");
355
670
  };
356
- var sendToTab = async (tabId, action, params) => {
671
+ var clearAgentTarget = () => {
672
+ agentTabId = null;
673
+ void writeAgentTabId(null);
674
+ };
675
+ var queryActiveTabIdInWindow = async (windowId) => {
676
+ const tabs = await wrapChromeCallback(
677
+ (callback) => chrome.tabs.query({ active: true, windowId }, callback)
678
+ );
679
+ const first = tabs[0];
680
+ if (first && typeof first.id === "number") {
681
+ return first.id;
682
+ }
683
+ const anyTabs = await wrapChromeCallback(
684
+ (callback) => chrome.tabs.query({ windowId }, callback)
685
+ );
686
+ const fallback = anyTabs[0];
687
+ if (fallback && typeof fallback.id === "number") {
688
+ return fallback.id;
689
+ }
690
+ throw new Error("No tab found for window.");
691
+ };
692
+ var ensureAgentTabGroup = async (tabId, windowId) => {
693
+ if (typeof chrome.tabs?.group !== "function") {
694
+ return;
695
+ }
696
+ if (!chrome.tabGroups || typeof chrome.tabGroups.update !== "function") {
697
+ return;
698
+ }
699
+ try {
700
+ const groupId = await wrapChromeCallback(
701
+ (callback) => chrome.tabs.group(
702
+ { tabIds: tabId, createProperties: { windowId } },
703
+ callback
704
+ )
705
+ );
706
+ await wrapChromeVoid(
707
+ (callback) => chrome.tabGroups.update(
708
+ groupId,
709
+ { title: AGENT_TAB_GROUP_TITLE },
710
+ () => callback()
711
+ )
712
+ );
713
+ } catch (error) {
714
+ console.debug("Failed to create/update agent tab group.", error);
715
+ }
716
+ };
717
+ var createAgentWindow = async () => {
718
+ const created = await wrapChromeCallback(
719
+ (callback) => chrome.windows.create({ url: "about:blank", focused: true }, callback)
720
+ );
721
+ const windowId = created.id;
722
+ if (typeof windowId !== "number") {
723
+ throw new Error("Failed to create agent window.");
724
+ }
725
+ const tabId = await queryActiveTabIdInWindow(windowId);
726
+ await ensureAgentTabGroup(tabId, windowId);
727
+ return tabId;
728
+ };
729
+ var readAgentTabId = async () => {
357
730
  return await new Promise((resolve) => {
358
- const message = { action, params };
359
- chrome.tabs.sendMessage(tabId, message, (response) => {
731
+ chrome.storage.local.get(
732
+ [AGENT_TAB_ID_KEY],
733
+ (result) => {
734
+ const raw = result?.[AGENT_TAB_ID_KEY];
735
+ resolve(typeof raw === "number" && Number.isFinite(raw) ? raw : null);
736
+ }
737
+ );
738
+ });
739
+ };
740
+ var writeAgentTabId = async (tabId) => {
741
+ await new Promise((resolve, reject) => {
742
+ const done = () => {
360
743
  const error = chrome.runtime.lastError;
361
744
  if (error) {
362
- resolve({
363
- ok: false,
364
- error: {
365
- code: "EVALUATION_FAILED",
366
- message: error.message,
367
- retryable: false
368
- }
369
- });
745
+ reject(new Error(error.message));
370
746
  return;
371
747
  }
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;
748
+ resolve();
749
+ };
750
+ if (tabId === null) {
751
+ chrome.storage.local.remove([AGENT_TAB_ID_KEY], done);
752
+ return;
753
+ }
754
+ chrome.storage.local.set({ [AGENT_TAB_ID_KEY]: tabId }, done);
755
+ }).catch((error) => {
756
+ console.debug("Failed to persist agentTabId.", error);
757
+ });
758
+ };
759
+ var getOrCreateAgentTabId = async () => {
760
+ if (agentTabId !== null) {
761
+ try {
762
+ const tab = await getTab(agentTabId);
763
+ const url = tab.url;
764
+ if (typeof url === "string" && isRestrictedUrl(url)) {
765
+ throw new Error(`Agent tab points at restricted URL: ${url}`);
766
+ }
767
+ return agentTabId;
768
+ } catch {
769
+ clearAgentTarget();
770
+ }
771
+ }
772
+ const stored = await readAgentTabId();
773
+ if (stored !== null) {
774
+ try {
775
+ const tab = await getTab(stored);
776
+ const url = tab.url;
777
+ if (typeof url === "string" && isRestrictedUrl(url)) {
778
+ throw new Error(`Stored agent tab points at restricted URL: ${url}`);
382
779
  }
383
- resolve(response);
780
+ agentTabId = stored;
781
+ ensureLastActiveAt(stored);
782
+ markTabActive(stored);
783
+ return stored;
784
+ } catch {
785
+ await writeAgentTabId(null);
786
+ }
787
+ }
788
+ const tabId = await createAgentWindow();
789
+ agentTabId = tabId;
790
+ ensureLastActiveAt(tabId);
791
+ markTabActive(tabId);
792
+ await writeAgentTabId(tabId);
793
+ return tabId;
794
+ };
795
+ var getDefaultTabId = async () => {
796
+ try {
797
+ return await getOrCreateAgentTabId();
798
+ } catch (error) {
799
+ console.warn(
800
+ "Failed to create agent window/tab; falling back to active tab.",
801
+ error
802
+ );
803
+ return await getActiveTabId();
804
+ }
805
+ };
806
+ var sendToTab = async (tabId, action, params) => {
807
+ const attemptSend = async () => {
808
+ return await new Promise((resolve) => {
809
+ const message = { action, params };
810
+ chrome.tabs.sendMessage(tabId, message, (response) => {
811
+ const error = chrome.runtime.lastError;
812
+ if (error) {
813
+ resolve({
814
+ ok: false,
815
+ error: {
816
+ code: "EVALUATION_FAILED",
817
+ message: error.message,
818
+ retryable: false
819
+ }
820
+ });
821
+ return;
822
+ }
823
+ if (!response || typeof response !== "object") {
824
+ resolve({
825
+ ok: false,
826
+ error: {
827
+ code: "EVALUATION_FAILED",
828
+ message: "Empty response from content script.",
829
+ retryable: false
830
+ }
831
+ });
832
+ return;
833
+ }
834
+ resolve(response);
835
+ });
384
836
  });
385
- });
837
+ };
838
+ const MAX_ATTEMPTS = 5;
839
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
840
+ const result = await attemptSend();
841
+ if (result.ok) {
842
+ return result;
843
+ }
844
+ const message = result.error?.message;
845
+ const isNoReceiver = typeof message === "string" && message.toLowerCase().includes("receiving end does not exist");
846
+ if (!isNoReceiver || attempt === MAX_ATTEMPTS) {
847
+ return result;
848
+ }
849
+ await delayMs(200);
850
+ }
851
+ return {
852
+ ok: false,
853
+ error: {
854
+ code: "INTERNAL",
855
+ message: "Failed to send message to content script.",
856
+ retryable: false
857
+ }
858
+ };
386
859
  };
387
860
  var waitForDomContentLoaded = async (tabId, timeoutMs) => {
388
861
  return await new Promise((resolve, reject) => {
@@ -447,13 +920,13 @@ var DriveSocket = class {
447
920
  if (this.reconnectTimer !== null) {
448
921
  return;
449
922
  }
450
- const delay = this.reconnectDelayMs;
923
+ const delay2 = this.reconnectDelayMs;
451
924
  this.reconnectTimer = self.setTimeout(() => {
452
925
  this.reconnectTimer = null;
453
926
  void this.connect().catch((error) => {
454
927
  console.error("DriveSocket reconnect failed:", error);
455
928
  });
456
- }, delay);
929
+ }, delay2);
457
930
  this.reconnectDelayMs = Math.min(
458
931
  this.maxReconnectDelayMs,
459
932
  this.reconnectDelayMs * 2
@@ -573,10 +1046,17 @@ var DriveSocket = class {
573
1046
  }
574
1047
  async handleRequest(message) {
575
1048
  let driveMessage = null;
1049
+ let gatedSiteKey = null;
1050
+ let touchGatedSiteOnSuccess = false;
576
1051
  const respondOk = (result) => {
577
1052
  if (!driveMessage) {
578
1053
  return;
579
1054
  }
1055
+ if (touchGatedSiteOnSuccess && gatedSiteKey) {
1056
+ void touchSiteLastUsed(gatedSiteKey).catch((error) => {
1057
+ console.error("Failed to touch site allowlist entry:", error);
1058
+ });
1059
+ }
580
1060
  const response = {
581
1061
  id: driveMessage.id,
582
1062
  action: driveMessage.action,
@@ -609,6 +1089,159 @@ var DriveSocket = class {
609
1089
  return;
610
1090
  }
611
1091
  driveMessage = message;
1092
+ const gatedActions = /* @__PURE__ */ new Set([
1093
+ "drive.navigate",
1094
+ "drive.go_back",
1095
+ "drive.go_forward",
1096
+ "drive.back",
1097
+ "drive.forward",
1098
+ "drive.click",
1099
+ "drive.hover",
1100
+ "drive.select",
1101
+ "drive.type",
1102
+ "drive.fill_form",
1103
+ "drive.drag",
1104
+ "drive.handle_dialog",
1105
+ "drive.key",
1106
+ "drive.key_press",
1107
+ "drive.scroll",
1108
+ "drive.screenshot",
1109
+ "drive.wait_for"
1110
+ ]);
1111
+ const gateDriveAction = async () => {
1112
+ const action = message.action;
1113
+ if (!gatedActions.has(action)) {
1114
+ return { ok: true, siteKey: null, touchOnSuccess: false };
1115
+ }
1116
+ const params = message.params ?? {};
1117
+ let siteKey = null;
1118
+ if (action === "drive.navigate") {
1119
+ const url = params.url;
1120
+ if (typeof url !== "string" || url.length === 0) {
1121
+ return { ok: true, siteKey: null, touchOnSuccess: false };
1122
+ }
1123
+ if (isRestrictedUrl(url)) {
1124
+ return {
1125
+ ok: false,
1126
+ error: {
1127
+ code: "NOT_SUPPORTED",
1128
+ message: "Navigation is not supported for this URL.",
1129
+ retryable: false,
1130
+ details: { url }
1131
+ }
1132
+ };
1133
+ }
1134
+ siteKey = siteKeyFromUrl(url);
1135
+ if (!siteKey) {
1136
+ return {
1137
+ ok: false,
1138
+ error: {
1139
+ code: "INVALID_ARGUMENT",
1140
+ message: "Unable to resolve site permission key for url.",
1141
+ retryable: false,
1142
+ details: { url }
1143
+ }
1144
+ };
1145
+ }
1146
+ } else {
1147
+ const tabId = params.tab_id;
1148
+ if (tabId !== void 0 && typeof tabId !== "number") {
1149
+ return { ok: true, siteKey: null, touchOnSuccess: false };
1150
+ }
1151
+ const resolvedTabId = typeof tabId === "number" ? tabId : await getDefaultTabId();
1152
+ const tab = await getTab(resolvedTabId);
1153
+ const url = tab.url;
1154
+ if (typeof url !== "string" || url.length === 0) {
1155
+ return {
1156
+ ok: false,
1157
+ error: {
1158
+ code: "FAILED_PRECONDITION",
1159
+ message: "Active tab URL is unavailable for permission gating.",
1160
+ retryable: false,
1161
+ details: { tab_id: resolvedTabId }
1162
+ }
1163
+ };
1164
+ }
1165
+ if (isRestrictedUrl(url)) {
1166
+ const message2 = action === "drive.screenshot" ? "Screenshots are not supported for this URL." : "This action is not supported for this URL.";
1167
+ return {
1168
+ ok: false,
1169
+ error: {
1170
+ code: "NOT_SUPPORTED",
1171
+ message: message2,
1172
+ retryable: false,
1173
+ details: { url }
1174
+ }
1175
+ };
1176
+ }
1177
+ siteKey = siteKeyFromUrl(url);
1178
+ if (!siteKey) {
1179
+ return {
1180
+ ok: false,
1181
+ error: {
1182
+ code: "FAILED_PRECONDITION",
1183
+ message: "Unable to resolve site permission key for active tab.",
1184
+ retryable: false,
1185
+ details: { url, tab_id: resolvedTabId }
1186
+ }
1187
+ };
1188
+ }
1189
+ }
1190
+ if (await readSitePermissionsMode() === "bypass") {
1191
+ return { ok: true, siteKey, touchOnSuccess: false };
1192
+ }
1193
+ if (await isSiteAllowed(siteKey)) {
1194
+ return { ok: true, siteKey, touchOnSuccess: true };
1195
+ }
1196
+ const decision = await permissionPrompts.requestPermission({
1197
+ siteKey,
1198
+ action
1199
+ });
1200
+ if (decision.kind === "timed_out") {
1201
+ return {
1202
+ ok: false,
1203
+ error: {
1204
+ code: "PERMISSION_PROMPT_TIMEOUT",
1205
+ message: `Permission prompt timed out for ${siteKey}.`,
1206
+ retryable: true,
1207
+ details: {
1208
+ reason: "prompt_timed_out",
1209
+ site: siteKey,
1210
+ action,
1211
+ wait_ms: decision.waitMs
1212
+ }
1213
+ }
1214
+ };
1215
+ }
1216
+ if (decision.kind === "deny") {
1217
+ return {
1218
+ ok: false,
1219
+ error: {
1220
+ code: "PERMISSION_DENIED",
1221
+ message: `User denied Browser Bridge permission for ${siteKey}.`,
1222
+ retryable: false,
1223
+ details: {
1224
+ reason: "user_denied",
1225
+ site: siteKey,
1226
+ action,
1227
+ 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."
1228
+ }
1229
+ }
1230
+ };
1231
+ }
1232
+ if (decision.kind === "allow_always") {
1233
+ await allowSiteAlways(siteKey);
1234
+ return { ok: true, siteKey, touchOnSuccess: true };
1235
+ }
1236
+ return { ok: true, siteKey, touchOnSuccess: false };
1237
+ };
1238
+ const gated = await gateDriveAction();
1239
+ if (!gated.ok) {
1240
+ respondError(gated.error);
1241
+ return;
1242
+ }
1243
+ gatedSiteKey = gated.siteKey;
1244
+ touchGatedSiteOnSuccess = gated.touchOnSuccess;
612
1245
  switch (message.action) {
613
1246
  case "drive.ping": {
614
1247
  respondOk({ ok: true });
@@ -635,7 +1268,7 @@ var DriveSocket = class {
635
1268
  return;
636
1269
  }
637
1270
  if (tabId === void 0) {
638
- tabId = await getActiveTabId();
1271
+ tabId = await getDefaultTabId();
639
1272
  }
640
1273
  const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
641
1274
  await wrapChromeVoid(
@@ -672,7 +1305,7 @@ var DriveSocket = class {
672
1305
  return;
673
1306
  }
674
1307
  if (tabId === void 0) {
675
- tabId = await getActiveTabId();
1308
+ tabId = await getDefaultTabId();
676
1309
  }
677
1310
  try {
678
1311
  const isBack = message.action === "drive.go_back" || message.action === "drive.back";
@@ -753,6 +1386,9 @@ var DriveSocket = class {
753
1386
  await wrapChromeVoid(
754
1387
  (callback) => chrome.tabs.remove(tabId, () => callback())
755
1388
  );
1389
+ if (agentTabId === tabId) {
1390
+ clearAgentTarget();
1391
+ }
756
1392
  lastActiveAtByTab.delete(tabId);
757
1393
  respondOk({ ok: true });
758
1394
  this.sendTabReport();
@@ -788,7 +1424,7 @@ var DriveSocket = class {
788
1424
  return;
789
1425
  }
790
1426
  if (tabId === void 0) {
791
- tabId = await getActiveTabId();
1427
+ tabId = await getDefaultTabId();
792
1428
  }
793
1429
  const error = await this.ensureDebuggerAttached(tabId);
794
1430
  if (error) {
@@ -836,7 +1472,7 @@ var DriveSocket = class {
836
1472
  return;
837
1473
  }
838
1474
  if (tabId === void 0) {
839
- tabId = await getActiveTabId();
1475
+ tabId = await getDefaultTabId();
840
1476
  }
841
1477
  const result = await sendToTab(
842
1478
  tabId,
@@ -862,7 +1498,7 @@ var DriveSocket = class {
862
1498
  return;
863
1499
  }
864
1500
  if (tabId === void 0) {
865
- tabId = await getActiveTabId();
1501
+ tabId = await getDefaultTabId();
866
1502
  }
867
1503
  const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
868
1504
  const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
@@ -1600,6 +2236,13 @@ var DebuggerTimeoutError = class extends Error {
1600
2236
  }
1601
2237
  };
1602
2238
  var socket = new DriveSocket();
2239
+ var permissionPrompts = new PermissionPromptController();
2240
+ chrome.runtime.onConnect.addListener((port) => {
2241
+ permissionPrompts.handleConnect(port);
2242
+ });
2243
+ chrome.windows.onRemoved.addListener((windowId) => {
2244
+ permissionPrompts.handleWindowRemoved(windowId);
2245
+ });
1603
2246
  chrome.tabs.onActivated.addListener((activeInfo) => {
1604
2247
  markTabActive(activeInfo.tabId);
1605
2248
  socket.sendTabReport();
@@ -1623,6 +2266,9 @@ chrome.tabs.onUpdated.addListener(
1623
2266
  }
1624
2267
  );
1625
2268
  chrome.tabs.onRemoved.addListener((tabId) => {
2269
+ if (agentTabId === tabId) {
2270
+ clearAgentTarget();
2271
+ }
1626
2272
  lastActiveAtByTab.delete(tabId);
1627
2273
  socket.sendTabReport();
1628
2274
  });