@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.
- package/CHANGELOG.md +48 -0
- package/README.md +6 -0
- package/dist/api.js +12 -2
- package/dist/api.js.map +2 -2
- package/dist/index.js +146 -106
- package/dist/index.js.map +3 -3
- package/extension/assets/ui.css +447 -0
- package/extension/dist/background.js +676 -30
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/options-ui.js +374 -0
- package/extension/dist/options-ui.js.map +7 -0
- package/extension/dist/permission-prompt-ui.js +62 -0
- package/extension/dist/permission-prompt-ui.js.map +7 -0
- package/extension/dist/popup-ui.js +51 -0
- package/extension/dist/popup-ui.js.map +7 -0
- package/extension/manifest.json +8 -3
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +1 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|