@btraut/browser-bridge 0.14.0 → 0.15.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 +30 -0
- package/README.md +27 -2
- package/dist/api.js +893 -166
- package/dist/api.js.map +4 -4
- package/dist/index.js +349 -115
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +468 -18
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/content.js +71 -8
- package/extension/dist/content.js.map +2 -2
- package/extension/dist/permissions-request-ui.js +111 -0
- package/extension/dist/permissions-request-ui.js.map +7 -0
- package/extension/manifest.json +2 -2
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +9 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -181,6 +181,14 @@ var readSitePermissionsMode = async () => {
|
|
|
181
181
|
);
|
|
182
182
|
});
|
|
183
183
|
};
|
|
184
|
+
var writeSitePermissionsMode = async (mode) => {
|
|
185
|
+
return await new Promise((resolve) => {
|
|
186
|
+
chrome.storage.local.set(
|
|
187
|
+
{ [SITE_PERMISSIONS_MODE_KEY]: mode },
|
|
188
|
+
() => resolve()
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
184
192
|
var readPermissionPromptWaitMs = async () => {
|
|
185
193
|
return await new Promise((resolve) => {
|
|
186
194
|
chrome.storage.local.get(
|
|
@@ -212,6 +220,9 @@ var writeDebuggerCapabilityEnabled = async (enabled) => {
|
|
|
212
220
|
);
|
|
213
221
|
});
|
|
214
222
|
};
|
|
223
|
+
var getAllowlistedSites = async () => {
|
|
224
|
+
return await readAllowlistRaw();
|
|
225
|
+
};
|
|
215
226
|
var isSiteAllowed = async (siteKey) => {
|
|
216
227
|
const key = normalizeSiteKey(siteKey);
|
|
217
228
|
const allowlist = await readAllowlistRaw();
|
|
@@ -238,6 +249,15 @@ var touchSiteLastUsed = async (siteKey, now = /* @__PURE__ */ new Date()) => {
|
|
|
238
249
|
allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };
|
|
239
250
|
await writeAllowlistRaw(allowlist);
|
|
240
251
|
};
|
|
252
|
+
var revokeSite = async (siteKey) => {
|
|
253
|
+
const key = normalizeSiteKey(siteKey);
|
|
254
|
+
const allowlist = await readAllowlistRaw();
|
|
255
|
+
if (!allowlist[key]) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
delete allowlist[key];
|
|
259
|
+
await writeAllowlistRaw(allowlist);
|
|
260
|
+
};
|
|
241
261
|
|
|
242
262
|
// packages/extension/src/permission-prompt.ts
|
|
243
263
|
var PERMISSION_PROMPT_PORT_NAME = "permission_prompt";
|
|
@@ -423,6 +443,281 @@ var PermissionPromptController = class {
|
|
|
423
443
|
}
|
|
424
444
|
};
|
|
425
445
|
|
|
446
|
+
// packages/extension/src/permissions-request.ts
|
|
447
|
+
var PERMISSIONS_REQUEST_PORT_NAME = "permissions_request_prompt";
|
|
448
|
+
var defaultMakeRequestId2 = () => {
|
|
449
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
450
|
+
return crypto.randomUUID();
|
|
451
|
+
}
|
|
452
|
+
return `perm-change-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
453
|
+
};
|
|
454
|
+
var defaultOpenWindow2 = async (url) => {
|
|
455
|
+
return await new Promise((resolve, reject) => {
|
|
456
|
+
chrome.windows.create(
|
|
457
|
+
{
|
|
458
|
+
type: "popup",
|
|
459
|
+
url,
|
|
460
|
+
focused: true,
|
|
461
|
+
width: 500,
|
|
462
|
+
height: 470
|
|
463
|
+
},
|
|
464
|
+
(win) => {
|
|
465
|
+
const err = chrome.runtime.lastError;
|
|
466
|
+
if (err) {
|
|
467
|
+
reject(new Error(err.message));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const windowId = win?.id;
|
|
471
|
+
if (typeof windowId !== "number") {
|
|
472
|
+
reject(new Error("Prompt window id missing."));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
resolve(windowId);
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
var defaultCloseWindow2 = async (windowId) => {
|
|
481
|
+
return await new Promise((resolve) => {
|
|
482
|
+
chrome.windows.remove(windowId, () => resolve());
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
var delay2 = async (ms) => {
|
|
486
|
+
return await new Promise((resolve) => {
|
|
487
|
+
setTimeout(resolve, ms);
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
var normalizeSite = (site) => site.trim().toLowerCase();
|
|
491
|
+
var describeChange = (state) => {
|
|
492
|
+
switch (state.kind) {
|
|
493
|
+
case "allow_site":
|
|
494
|
+
return `Allow Browser Bridge actions on ${state.site ?? "this site"}.`;
|
|
495
|
+
case "revoke_site":
|
|
496
|
+
return `Revoke Browser Bridge actions on ${state.site ?? "this site"}.`;
|
|
497
|
+
case "set_mode":
|
|
498
|
+
return state.mode === "bypass" ? "Switch Browser Bridge to bypass mode." : "Switch Browser Bridge to granular mode.";
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var buildWarning = (kind, mode) => {
|
|
502
|
+
if (kind === "set_mode" && mode === "bypass") {
|
|
503
|
+
return "Bypass mode lets the agent act on any website without asking first.";
|
|
504
|
+
}
|
|
505
|
+
return void 0;
|
|
506
|
+
};
|
|
507
|
+
var PermissionsRequestController = class {
|
|
508
|
+
constructor(deps) {
|
|
509
|
+
this.stateByRequestId = /* @__PURE__ */ new Map();
|
|
510
|
+
this.stateByWindowId = /* @__PURE__ */ new Map();
|
|
511
|
+
this.deps = {
|
|
512
|
+
openWindow: deps?.openWindow ?? defaultOpenWindow2,
|
|
513
|
+
closeWindow: deps?.closeWindow ?? defaultCloseWindow2,
|
|
514
|
+
getDefaultWaitMs: deps?.getDefaultWaitMs ?? readPermissionPromptWaitMs,
|
|
515
|
+
makeRequestId: deps?.makeRequestId ?? defaultMakeRequestId2,
|
|
516
|
+
now: deps?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
517
|
+
allowSite: deps?.allowSite ?? allowSiteAlways,
|
|
518
|
+
revokeSite: deps?.revokeSite ?? revokeSite,
|
|
519
|
+
setMode: deps?.setMode ?? writeSitePermissionsMode
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async requestChange(request) {
|
|
523
|
+
const state = await this.createState(request);
|
|
524
|
+
const waitMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : await this.deps.getDefaultWaitMs();
|
|
525
|
+
const decision = await this.waitForDecisionOrTimeout(state, waitMs);
|
|
526
|
+
if (!decision) {
|
|
527
|
+
return {
|
|
528
|
+
request_id: state.requestId,
|
|
529
|
+
kind: state.kind,
|
|
530
|
+
status: "timed_out",
|
|
531
|
+
requested_at: state.requestedAt,
|
|
532
|
+
...state.site ? { site: state.site } : {},
|
|
533
|
+
...state.mode ? { mode: state.mode } : {},
|
|
534
|
+
...state.source ? { source: state.source } : {},
|
|
535
|
+
...state.warning ? { warning: state.warning } : {},
|
|
536
|
+
message: "Permission change request timed out waiting for approval."
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
request_id: state.requestId,
|
|
541
|
+
kind: state.kind,
|
|
542
|
+
status: decision === "approve" ? "approved" : "denied",
|
|
543
|
+
requested_at: state.requestedAt,
|
|
544
|
+
...state.site ? { site: state.site } : {},
|
|
545
|
+
...state.mode ? { mode: state.mode } : {},
|
|
546
|
+
...state.source ? { source: state.source } : {},
|
|
547
|
+
...state.warning ? { warning: state.warning } : {},
|
|
548
|
+
message: decision === "approve" ? describeChange(state) : "Permission change request was denied."
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
listPendingRequests() {
|
|
552
|
+
return [...this.stateByRequestId.values()].filter((state) => state.decided === null).map((state) => ({
|
|
553
|
+
request_id: state.requestId,
|
|
554
|
+
kind: state.kind,
|
|
555
|
+
status: "pending",
|
|
556
|
+
requested_at: state.requestedAt,
|
|
557
|
+
...state.site ? { site: state.site } : {},
|
|
558
|
+
...state.mode ? { mode: state.mode } : {},
|
|
559
|
+
...state.source ? { source: state.source } : {},
|
|
560
|
+
...state.warning ? { warning: state.warning } : {}
|
|
561
|
+
})).sort((a, b) => a.requested_at.localeCompare(b.requested_at));
|
|
562
|
+
}
|
|
563
|
+
handleConnect(port) {
|
|
564
|
+
if (!port || typeof port !== "object") {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const p = port;
|
|
568
|
+
if (p.name !== PERMISSIONS_REQUEST_PORT_NAME) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const onMessage = p.onMessage;
|
|
572
|
+
if (!onMessage || typeof onMessage.addListener !== "function") {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
onMessage.addListener((message) => {
|
|
576
|
+
void this.handlePortMessage(message).catch((error) => {
|
|
577
|
+
console.error(
|
|
578
|
+
"PermissionsRequestController handlePortMessage failed:",
|
|
579
|
+
error
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
handleWindowRemoved(windowId) {
|
|
585
|
+
const state = this.stateByWindowId.get(windowId);
|
|
586
|
+
if (!state) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.cleanupState(state);
|
|
590
|
+
}
|
|
591
|
+
async createState(request) {
|
|
592
|
+
const requestId = this.deps.makeRequestId();
|
|
593
|
+
const requestedAt = this.deps.now();
|
|
594
|
+
const kind = request.kind;
|
|
595
|
+
const warning = buildWarning(kind, request.mode);
|
|
596
|
+
const state = {
|
|
597
|
+
requestId,
|
|
598
|
+
kind,
|
|
599
|
+
requestedAt,
|
|
600
|
+
site: request.site ? normalizeSite(request.site) : void 0,
|
|
601
|
+
mode: request.mode,
|
|
602
|
+
source: request.source,
|
|
603
|
+
warning,
|
|
604
|
+
windowId: null,
|
|
605
|
+
decided: null,
|
|
606
|
+
waiters: /* @__PURE__ */ new Set()
|
|
607
|
+
};
|
|
608
|
+
this.stateByRequestId.set(requestId, state);
|
|
609
|
+
const url = this.buildPromptUrl(state);
|
|
610
|
+
const windowId = await this.deps.openWindow(url);
|
|
611
|
+
state.windowId = windowId;
|
|
612
|
+
this.stateByWindowId.set(windowId, state);
|
|
613
|
+
if (state.decided) {
|
|
614
|
+
await this.deps.closeWindow(windowId);
|
|
615
|
+
this.cleanupState(state);
|
|
616
|
+
}
|
|
617
|
+
return state;
|
|
618
|
+
}
|
|
619
|
+
buildPromptUrl(state) {
|
|
620
|
+
const base = chrome.runtime.getURL("permissions-request.html");
|
|
621
|
+
const url = new URL(base);
|
|
622
|
+
url.searchParams.set("requestId", state.requestId);
|
|
623
|
+
url.searchParams.set("kind", state.kind);
|
|
624
|
+
url.searchParams.set("requestedAt", state.requestedAt);
|
|
625
|
+
if (state.site) {
|
|
626
|
+
url.searchParams.set("site", state.site);
|
|
627
|
+
}
|
|
628
|
+
if (state.mode) {
|
|
629
|
+
url.searchParams.set("mode", state.mode);
|
|
630
|
+
}
|
|
631
|
+
if (state.source) {
|
|
632
|
+
url.searchParams.set("source", state.source);
|
|
633
|
+
}
|
|
634
|
+
if (state.warning) {
|
|
635
|
+
url.searchParams.set("warning", state.warning);
|
|
636
|
+
}
|
|
637
|
+
if (state.kind === "set_mode" && state.mode === "bypass") {
|
|
638
|
+
url.searchParams.set("requireAcknowledge", "1");
|
|
639
|
+
}
|
|
640
|
+
return url.toString();
|
|
641
|
+
}
|
|
642
|
+
async waitForDecisionOrTimeout(state, waitMs) {
|
|
643
|
+
if (state.decided) {
|
|
644
|
+
return state.decided;
|
|
645
|
+
}
|
|
646
|
+
let waiter = null;
|
|
647
|
+
const decisionPromise = new Promise((resolve) => {
|
|
648
|
+
waiter = resolve;
|
|
649
|
+
state.waiters.add(resolve);
|
|
650
|
+
});
|
|
651
|
+
const winner = await Promise.race([
|
|
652
|
+
decisionPromise,
|
|
653
|
+
delay2(waitMs).then(() => null)
|
|
654
|
+
]);
|
|
655
|
+
if (winner === null && waiter) {
|
|
656
|
+
state.waiters.delete(waiter);
|
|
657
|
+
}
|
|
658
|
+
return winner;
|
|
659
|
+
}
|
|
660
|
+
async handlePortMessage(message) {
|
|
661
|
+
if (!message || typeof message !== "object") {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const m = message;
|
|
665
|
+
if (m.type !== "decision") {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const requestId = m.requestId;
|
|
669
|
+
const decision = m.decision;
|
|
670
|
+
if (typeof requestId !== "string" || requestId.length === 0) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (decision !== "approve" && decision !== "deny") {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const state = this.stateByRequestId.get(requestId);
|
|
677
|
+
if (!state) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
state.decided = decision;
|
|
681
|
+
if (decision === "approve") {
|
|
682
|
+
await this.applyApprovedChange(state);
|
|
683
|
+
}
|
|
684
|
+
for (const waiter of state.waiters) {
|
|
685
|
+
waiter(decision);
|
|
686
|
+
}
|
|
687
|
+
state.waiters.clear();
|
|
688
|
+
if (typeof state.windowId === "number") {
|
|
689
|
+
await this.deps.closeWindow(state.windowId);
|
|
690
|
+
this.cleanupState(state);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async applyApprovedChange(state) {
|
|
694
|
+
if (state.kind === "allow_site") {
|
|
695
|
+
if (!state.site) {
|
|
696
|
+
throw new Error("allow_site request is missing site.");
|
|
697
|
+
}
|
|
698
|
+
await this.deps.allowSite(state.site);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (state.kind === "revoke_site") {
|
|
702
|
+
if (!state.site) {
|
|
703
|
+
throw new Error("revoke_site request is missing site.");
|
|
704
|
+
}
|
|
705
|
+
await this.deps.revokeSite(state.site);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (!state.mode) {
|
|
709
|
+
throw new Error("set_mode request is missing mode.");
|
|
710
|
+
}
|
|
711
|
+
await this.deps.setMode(state.mode);
|
|
712
|
+
}
|
|
713
|
+
cleanupState(state) {
|
|
714
|
+
this.stateByRequestId.delete(state.requestId);
|
|
715
|
+
if (typeof state.windowId === "number") {
|
|
716
|
+
this.stateByWindowId.delete(state.windowId);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
426
721
|
// packages/extension/src/restricted-url.ts
|
|
427
722
|
var RESTRICTED_URL_PREFIXES = [
|
|
428
723
|
"chrome://",
|
|
@@ -840,6 +1135,9 @@ var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
|
|
|
840
1135
|
"the message port closed before a response was received",
|
|
841
1136
|
"extension port is moved into back/forward cache"
|
|
842
1137
|
];
|
|
1138
|
+
var CONTENT_SCRIPT_RECOVERY_ERROR_PATTERNS = [
|
|
1139
|
+
"receiving end does not exist"
|
|
1140
|
+
];
|
|
843
1141
|
var TAB_CHANNEL_RETRY_DELAYS_MS = [120, 200, 320, 500, 750, 1e3, 1200];
|
|
844
1142
|
var normalizePathname = (pathname) => {
|
|
845
1143
|
if (pathname.length === 0) {
|
|
@@ -859,6 +1157,25 @@ var isTransientTabChannelError = (message) => {
|
|
|
859
1157
|
(pattern) => normalized.includes(pattern)
|
|
860
1158
|
);
|
|
861
1159
|
};
|
|
1160
|
+
var canInjectContentScriptForUrl = (url) => {
|
|
1161
|
+
if (typeof url !== "string" || url.trim().length === 0) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
const normalized = url.toLowerCase();
|
|
1165
|
+
if (normalized.startsWith("chrome://") || normalized.startsWith("chrome-extension://") || normalized.startsWith("devtools://") || normalized.startsWith("edge://") || normalized.startsWith("about:") || normalized.startsWith("file://")) {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
return normalized.startsWith("http://") || normalized.startsWith("https://");
|
|
1169
|
+
};
|
|
1170
|
+
var shouldReinjectContentScript = (message, tabUrl) => {
|
|
1171
|
+
if (typeof message !== "string") {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
const normalized = message.toLowerCase();
|
|
1175
|
+
return CONTENT_SCRIPT_RECOVERY_ERROR_PATTERNS.some(
|
|
1176
|
+
(pattern) => normalized.includes(pattern)
|
|
1177
|
+
) && canInjectContentScriptForUrl(tabUrl);
|
|
1178
|
+
};
|
|
862
1179
|
var shouldRetryTabChannelFailure = (action, error) => {
|
|
863
1180
|
if (!error || typeof error !== "object") {
|
|
864
1181
|
return false;
|
|
@@ -1224,7 +1541,13 @@ var BASE_NEGOTIATED_CAPABILITIES = Object.freeze({
|
|
|
1224
1541
|
"drive.tab_activate": true,
|
|
1225
1542
|
"drive.tab_close": true,
|
|
1226
1543
|
"drive.set_debugger_capability": true,
|
|
1227
|
-
"drive.ping": true
|
|
1544
|
+
"drive.ping": true,
|
|
1545
|
+
"permissions.list": true,
|
|
1546
|
+
"permissions.get_mode": true,
|
|
1547
|
+
"permissions.list_pending_requests": true,
|
|
1548
|
+
"permissions.request_allow_site": true,
|
|
1549
|
+
"permissions.request_revoke_site": true,
|
|
1550
|
+
"permissions.request_set_mode": true
|
|
1228
1551
|
});
|
|
1229
1552
|
var DEBUGGER_CAPABILITY_ACTIONS = [
|
|
1230
1553
|
"debugger.attach",
|
|
@@ -1297,6 +1620,28 @@ var delayMs = async (ms) => {
|
|
|
1297
1620
|
self.setTimeout(resolve, ms);
|
|
1298
1621
|
});
|
|
1299
1622
|
};
|
|
1623
|
+
var ensureTabContentScript = async (tabId) => {
|
|
1624
|
+
try {
|
|
1625
|
+
const tab = await getTab(tabId);
|
|
1626
|
+
const url = typeof tab.url === "string" ? tab.url : void 0;
|
|
1627
|
+
if (!canInjectContentScriptForUrl(url)) {
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
await wrapChromeVoid(
|
|
1631
|
+
(callback) => chrome.scripting.executeScript(
|
|
1632
|
+
{
|
|
1633
|
+
target: { tabId },
|
|
1634
|
+
files: ["dist/content.js"]
|
|
1635
|
+
},
|
|
1636
|
+
() => callback()
|
|
1637
|
+
)
|
|
1638
|
+
);
|
|
1639
|
+
return true;
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
console.debug("Failed to re-inject content script.", { tabId, error });
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1300
1645
|
var CAPTURE_VISIBLE_TAB_MIN_INTERVAL_MS = 400;
|
|
1301
1646
|
var CAPTURE_VISIBLE_TAB_MAX_RETRIES = 3;
|
|
1302
1647
|
var CAPTURE_VISIBLE_TAB_RETRY_BASE_DELAY_MS = 500;
|
|
@@ -1752,11 +2097,18 @@ var sendToTab = async (tabId, action, params, options) => {
|
|
|
1752
2097
|
});
|
|
1753
2098
|
});
|
|
1754
2099
|
};
|
|
2100
|
+
let attemptedContentRecovery = false;
|
|
1755
2101
|
for (let attempt = 1; ; attempt += 1) {
|
|
1756
2102
|
const result = await attemptSend();
|
|
1757
2103
|
if (result.ok) {
|
|
1758
2104
|
return result;
|
|
1759
2105
|
}
|
|
2106
|
+
if (!attemptedContentRecovery && shouldReinjectContentScript(
|
|
2107
|
+
result.error.message,
|
|
2108
|
+
(await getTab(tabId).catch(() => void 0))?.url
|
|
2109
|
+
)) {
|
|
2110
|
+
attemptedContentRecovery = await ensureTabContentScript(tabId);
|
|
2111
|
+
}
|
|
1760
2112
|
if (!shouldRetryTabChannelFailure(action, result.error)) {
|
|
1761
2113
|
return result;
|
|
1762
2114
|
}
|
|
@@ -1909,15 +2261,15 @@ var DriveSocket = class {
|
|
|
1909
2261
|
if (this.reconnectTimer !== null) {
|
|
1910
2262
|
return;
|
|
1911
2263
|
}
|
|
1912
|
-
const
|
|
1913
|
-
this.connection.markBackoff(
|
|
2264
|
+
const delay3 = this.reconnectDelayMs;
|
|
2265
|
+
this.connection.markBackoff(delay3);
|
|
1914
2266
|
this.reconnectTimer = self.setTimeout(() => {
|
|
1915
2267
|
this.reconnectTimer = null;
|
|
1916
2268
|
this.connection.markConnecting();
|
|
1917
2269
|
void this.connect().catch((error) => {
|
|
1918
2270
|
this.recordConnectionFailure("reconnect", error);
|
|
1919
2271
|
});
|
|
1920
|
-
},
|
|
2272
|
+
}, delay3);
|
|
1921
2273
|
this.reconnectDelayMs = Math.min(
|
|
1922
2274
|
this.maxReconnectDelayMs,
|
|
1923
2275
|
this.reconnectDelayMs * 2
|
|
@@ -2132,11 +2484,11 @@ var DriveSocket = class {
|
|
|
2132
2484
|
this.refreshCapabilities();
|
|
2133
2485
|
}
|
|
2134
2486
|
async handleRequest(message) {
|
|
2135
|
-
let
|
|
2487
|
+
let requestMessage = null;
|
|
2136
2488
|
let gatedSiteKey = null;
|
|
2137
2489
|
let touchGatedSiteOnSuccess = false;
|
|
2138
2490
|
const respondOk = (result) => {
|
|
2139
|
-
if (!
|
|
2491
|
+
if (!requestMessage) {
|
|
2140
2492
|
return;
|
|
2141
2493
|
}
|
|
2142
2494
|
if (touchGatedSiteOnSuccess && gatedSiteKey) {
|
|
@@ -2145,20 +2497,20 @@ var DriveSocket = class {
|
|
|
2145
2497
|
});
|
|
2146
2498
|
}
|
|
2147
2499
|
const response = {
|
|
2148
|
-
id:
|
|
2149
|
-
action:
|
|
2500
|
+
id: requestMessage.id,
|
|
2501
|
+
action: requestMessage.action,
|
|
2150
2502
|
status: "ok",
|
|
2151
2503
|
result
|
|
2152
2504
|
};
|
|
2153
2505
|
this.sendMessage(response);
|
|
2154
2506
|
};
|
|
2155
2507
|
const respondError = (error) => {
|
|
2156
|
-
if (!
|
|
2508
|
+
if (!requestMessage) {
|
|
2157
2509
|
return;
|
|
2158
2510
|
}
|
|
2159
2511
|
const response = {
|
|
2160
|
-
id:
|
|
2161
|
-
action:
|
|
2512
|
+
id: requestMessage.id,
|
|
2513
|
+
action: requestMessage.action,
|
|
2162
2514
|
status: "error",
|
|
2163
2515
|
error: sanitizeDriveErrorInfo(error)
|
|
2164
2516
|
};
|
|
@@ -2168,16 +2520,111 @@ var DriveSocket = class {
|
|
|
2168
2520
|
if (!message || typeof message !== "object" || typeof message.id !== "string" || typeof message.action !== "string") {
|
|
2169
2521
|
return;
|
|
2170
2522
|
}
|
|
2171
|
-
|
|
2523
|
+
const action = message.action;
|
|
2524
|
+
if (action.startsWith("debugger.")) {
|
|
2172
2525
|
await this.handleDebuggerRequest(message);
|
|
2173
2526
|
return;
|
|
2174
2527
|
}
|
|
2175
|
-
|
|
2528
|
+
requestMessage = message;
|
|
2529
|
+
if (action.startsWith("permissions.")) {
|
|
2530
|
+
const params = message.params ?? {};
|
|
2531
|
+
const rawTimeoutMs = params.timeout_ms;
|
|
2532
|
+
const timeoutMs = typeof rawTimeoutMs === "number" && Number.isFinite(rawTimeoutMs) && rawTimeoutMs > 0 ? Math.floor(rawTimeoutMs) : void 0;
|
|
2533
|
+
const rawSource = params.source;
|
|
2534
|
+
const source = rawSource === "cli" || rawSource === "mcp" || rawSource === "api" ? rawSource : void 0;
|
|
2535
|
+
switch (action) {
|
|
2536
|
+
case "permissions.list": {
|
|
2537
|
+
const allowlist = await getAllowlistedSites();
|
|
2538
|
+
const sites = Object.entries(allowlist).map(([site, entry]) => ({
|
|
2539
|
+
site,
|
|
2540
|
+
created_at: entry.createdAt,
|
|
2541
|
+
last_used_at: entry.lastUsedAt
|
|
2542
|
+
})).sort((a, b) => a.site.localeCompare(b.site));
|
|
2543
|
+
respondOk({ sites });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
case "permissions.get_mode": {
|
|
2547
|
+
respondOk({
|
|
2548
|
+
mode: await readSitePermissionsMode()
|
|
2549
|
+
});
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
case "permissions.list_pending_requests": {
|
|
2553
|
+
respondOk({
|
|
2554
|
+
requests: permissionsRequests.listPendingRequests()
|
|
2555
|
+
});
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
case "permissions.request_allow_site": {
|
|
2559
|
+
const site = params.site;
|
|
2560
|
+
if (typeof site !== "string" || site.trim().length === 0) {
|
|
2561
|
+
respondError({
|
|
2562
|
+
code: "INVALID_ARGUMENT",
|
|
2563
|
+
message: "site must be a non-empty string.",
|
|
2564
|
+
retryable: false,
|
|
2565
|
+
details: { field: "site" }
|
|
2566
|
+
});
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
respondOk(
|
|
2570
|
+
await permissionsRequests.requestChange({
|
|
2571
|
+
kind: "allow_site",
|
|
2572
|
+
site,
|
|
2573
|
+
timeoutMs,
|
|
2574
|
+
source
|
|
2575
|
+
})
|
|
2576
|
+
);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
case "permissions.request_revoke_site": {
|
|
2580
|
+
const site = params.site;
|
|
2581
|
+
if (typeof site !== "string" || site.trim().length === 0) {
|
|
2582
|
+
respondError({
|
|
2583
|
+
code: "INVALID_ARGUMENT",
|
|
2584
|
+
message: "site must be a non-empty string.",
|
|
2585
|
+
retryable: false,
|
|
2586
|
+
details: { field: "site" }
|
|
2587
|
+
});
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
respondOk(
|
|
2591
|
+
await permissionsRequests.requestChange({
|
|
2592
|
+
kind: "revoke_site",
|
|
2593
|
+
site,
|
|
2594
|
+
timeoutMs,
|
|
2595
|
+
source
|
|
2596
|
+
})
|
|
2597
|
+
);
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
case "permissions.request_set_mode": {
|
|
2601
|
+
const mode = params.mode;
|
|
2602
|
+
if (mode !== "granular" && mode !== "bypass") {
|
|
2603
|
+
respondError({
|
|
2604
|
+
code: "INVALID_ARGUMENT",
|
|
2605
|
+
message: "mode must be granular or bypass.",
|
|
2606
|
+
retryable: false,
|
|
2607
|
+
details: { field: "mode" }
|
|
2608
|
+
});
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
respondOk(
|
|
2612
|
+
await permissionsRequests.requestChange({
|
|
2613
|
+
kind: "set_mode",
|
|
2614
|
+
mode,
|
|
2615
|
+
timeoutMs,
|
|
2616
|
+
source
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
if (!action.startsWith("drive.")) {
|
|
2176
2624
|
return;
|
|
2177
2625
|
}
|
|
2178
|
-
driveMessage = message;
|
|
2179
2626
|
const gated = await gateDriveAction({
|
|
2180
|
-
action
|
|
2627
|
+
action,
|
|
2181
2628
|
params: message.params ?? {},
|
|
2182
2629
|
getDefaultTabId,
|
|
2183
2630
|
getTab,
|
|
@@ -2433,8 +2880,8 @@ var DriveSocket = class {
|
|
|
2433
2880
|
}
|
|
2434
2881
|
case "drive.handle_dialog": {
|
|
2435
2882
|
const params = message.params ?? {};
|
|
2436
|
-
const
|
|
2437
|
-
if (
|
|
2883
|
+
const action2 = params.action;
|
|
2884
|
+
if (action2 !== "accept" && action2 !== "dismiss") {
|
|
2438
2885
|
respondError({
|
|
2439
2886
|
code: "INVALID_ARGUMENT",
|
|
2440
2887
|
message: "action must be accept or dismiss.",
|
|
@@ -2467,7 +2914,7 @@ var DriveSocket = class {
|
|
|
2467
2914
|
tabId,
|
|
2468
2915
|
"Page.handleJavaScriptDialog",
|
|
2469
2916
|
{
|
|
2470
|
-
accept:
|
|
2917
|
+
accept: action2 === "accept",
|
|
2471
2918
|
...promptText ? { promptText } : {}
|
|
2472
2919
|
},
|
|
2473
2920
|
DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
@@ -3869,11 +4316,14 @@ var DebuggerTimeoutError = class extends Error {
|
|
|
3869
4316
|
};
|
|
3870
4317
|
var socket = new DriveSocket();
|
|
3871
4318
|
var permissionPrompts = new PermissionPromptController();
|
|
4319
|
+
var permissionsRequests = new PermissionsRequestController();
|
|
3872
4320
|
chrome.runtime.onConnect.addListener((port) => {
|
|
3873
4321
|
permissionPrompts.handleConnect(port);
|
|
4322
|
+
permissionsRequests.handleConnect(port);
|
|
3874
4323
|
});
|
|
3875
4324
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
3876
4325
|
permissionPrompts.handleWindowRemoved(windowId);
|
|
4326
|
+
permissionsRequests.handleWindowRemoved(windowId);
|
|
3877
4327
|
});
|
|
3878
4328
|
chrome.windows.onFocusChanged.addListener((windowId) => {
|
|
3879
4329
|
if (windowId === chrome.windows.WINDOW_ID_NONE) {
|