@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.
- package/CHANGELOG.md +42 -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 +353 -0
- package/extension/dist/background.js +656 -30
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/options-ui.js +273 -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,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
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|