@btraut/browser-bridge 0.13.2 → 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 +51 -0
- package/README.md +53 -37
- package/dist/api.js +1792 -678
- package/dist/api.js.map +4 -4
- package/dist/index.js +666 -417
- package/dist/index.js.map +4 -4
- package/extension/dist/background.js +1484 -693
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/content.js +534 -77
- package/extension/dist/content.js.map +4 -4
- package/extension/dist/options-ui.js +2 -113
- package/extension/dist/options-ui.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 +3 -3
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +18 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -102,7 +102,6 @@ var DEFAULT_PERMISSION_PROMPT_WAIT_MS = 3e4;
|
|
|
102
102
|
var SITE_PERMISSIONS_MODE_KEY = "sitePermissionsMode";
|
|
103
103
|
var DEBUGGER_CAPABILITY_ENABLED_KEY = "debuggerCapabilityEnabled";
|
|
104
104
|
var DEFAULT_SITE_PERMISSIONS_MODE = "granular";
|
|
105
|
-
var DEFAULT_DEBUGGER_CAPABILITY_ENABLED = false;
|
|
106
105
|
var siteKeyFromUrl = (rawUrl) => {
|
|
107
106
|
if (!rawUrl || typeof rawUrl !== "string") {
|
|
108
107
|
return null;
|
|
@@ -182,6 +181,14 @@ var readSitePermissionsMode = async () => {
|
|
|
182
181
|
);
|
|
183
182
|
});
|
|
184
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
|
+
};
|
|
185
192
|
var readPermissionPromptWaitMs = async () => {
|
|
186
193
|
return await new Promise((resolve) => {
|
|
187
194
|
chrome.storage.local.get(
|
|
@@ -204,27 +211,18 @@ var readPermissionPromptWaitMs = async () => {
|
|
|
204
211
|
);
|
|
205
212
|
});
|
|
206
213
|
};
|
|
207
|
-
var
|
|
214
|
+
var writeDebuggerCapabilityEnabled = async (enabled) => {
|
|
215
|
+
void enabled;
|
|
208
216
|
return await new Promise((resolve) => {
|
|
209
|
-
chrome.storage.local.
|
|
210
|
-
[DEBUGGER_CAPABILITY_ENABLED_KEY],
|
|
211
|
-
(
|
|
212
|
-
const raw = result?.[DEBUGGER_CAPABILITY_ENABLED_KEY];
|
|
213
|
-
if (typeof raw === "boolean") {
|
|
214
|
-
resolve(raw);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
try {
|
|
218
|
-
chrome.storage.local.set({
|
|
219
|
-
[DEBUGGER_CAPABILITY_ENABLED_KEY]: DEFAULT_DEBUGGER_CAPABILITY_ENABLED
|
|
220
|
-
});
|
|
221
|
-
} catch {
|
|
222
|
-
}
|
|
223
|
-
resolve(DEFAULT_DEBUGGER_CAPABILITY_ENABLED);
|
|
224
|
-
}
|
|
217
|
+
chrome.storage.local.set(
|
|
218
|
+
{ [DEBUGGER_CAPABILITY_ENABLED_KEY]: true },
|
|
219
|
+
() => resolve()
|
|
225
220
|
);
|
|
226
221
|
});
|
|
227
222
|
};
|
|
223
|
+
var getAllowlistedSites = async () => {
|
|
224
|
+
return await readAllowlistRaw();
|
|
225
|
+
};
|
|
228
226
|
var isSiteAllowed = async (siteKey) => {
|
|
229
227
|
const key = normalizeSiteKey(siteKey);
|
|
230
228
|
const allowlist = await readAllowlistRaw();
|
|
@@ -251,6 +249,15 @@ var touchSiteLastUsed = async (siteKey, now = /* @__PURE__ */ new Date()) => {
|
|
|
251
249
|
allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };
|
|
252
250
|
await writeAllowlistRaw(allowlist);
|
|
253
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
|
+
};
|
|
254
261
|
|
|
255
262
|
// packages/extension/src/permission-prompt.ts
|
|
256
263
|
var PERMISSION_PROMPT_PORT_NAME = "permission_prompt";
|
|
@@ -436,6 +443,691 @@ var PermissionPromptController = class {
|
|
|
436
443
|
}
|
|
437
444
|
};
|
|
438
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
|
+
|
|
721
|
+
// packages/extension/src/restricted-url.ts
|
|
722
|
+
var RESTRICTED_URL_PREFIXES = [
|
|
723
|
+
"chrome://",
|
|
724
|
+
"chrome-extension://",
|
|
725
|
+
"chrome-devtools://",
|
|
726
|
+
"devtools://",
|
|
727
|
+
"edge://",
|
|
728
|
+
"brave://",
|
|
729
|
+
"view-source:"
|
|
730
|
+
];
|
|
731
|
+
var isRestrictedUrl = (url) => {
|
|
732
|
+
if (!url || typeof url !== "string") {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
const lowered = url.toLowerCase();
|
|
736
|
+
if (RESTRICTED_URL_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
const parsed = new URL(url);
|
|
741
|
+
if (parsed.hostname === "chromewebstore.google.com") {
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
if (parsed.hostname === "chrome.google.com") {
|
|
745
|
+
return parsed.pathname.startsWith("/webstore");
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.debug("Ignoring invalid URL in restriction check.", error);
|
|
749
|
+
}
|
|
750
|
+
return false;
|
|
751
|
+
};
|
|
752
|
+
var getRestrictedUrlKind = (url) => {
|
|
753
|
+
const lowered = url.toLowerCase();
|
|
754
|
+
if (lowered.startsWith("chrome-extension://")) {
|
|
755
|
+
return "extension_internal";
|
|
756
|
+
}
|
|
757
|
+
if (lowered.startsWith("chrome://") || lowered.startsWith("edge://") || lowered.startsWith("brave://")) {
|
|
758
|
+
return "browser_internal";
|
|
759
|
+
}
|
|
760
|
+
if (lowered.includes("chromewebstore.google.com") || lowered.includes("chrome.google.com/webstore")) {
|
|
761
|
+
return "webstore";
|
|
762
|
+
}
|
|
763
|
+
return "restricted_url";
|
|
764
|
+
};
|
|
765
|
+
var getAlternativeCommands = (url) => {
|
|
766
|
+
const lowered = url.toLowerCase();
|
|
767
|
+
if (lowered.startsWith("chrome-extension://") || lowered.startsWith("chrome://extensions")) {
|
|
768
|
+
return ["browser-bridge diagnostics doctor", "browser-bridge dev info"];
|
|
769
|
+
}
|
|
770
|
+
return ["browser-bridge dev info", "browser-bridge diagnostics doctor"];
|
|
771
|
+
};
|
|
772
|
+
var buildRestrictedUrlError = (options) => {
|
|
773
|
+
const alternatives = getAlternativeCommands(options.url);
|
|
774
|
+
const operationLabel = options.operation === "navigate" ? "Navigation" : options.operation === "screenshot" ? "Screenshots" : options.operation === "debugger" ? "Debugger attach" : "This action";
|
|
775
|
+
return {
|
|
776
|
+
code: "NOT_SUPPORTED",
|
|
777
|
+
message: `${operationLabel} is not supported for browser internal URLs.`,
|
|
778
|
+
retryable: false,
|
|
779
|
+
details: {
|
|
780
|
+
reason: "restricted_internal_url",
|
|
781
|
+
url: options.url,
|
|
782
|
+
url_kind: getRestrictedUrlKind(options.url),
|
|
783
|
+
rationale: "Chrome restricts extension automation on internal browser surfaces (for example chrome:// and chrome-extension://).",
|
|
784
|
+
action: options.action,
|
|
785
|
+
next_step: alternatives[0],
|
|
786
|
+
alternatives
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// packages/extension/src/tab-resolution.ts
|
|
792
|
+
var invalidTabIdError = (message) => ({
|
|
793
|
+
code: "INVALID_ARGUMENT",
|
|
794
|
+
message,
|
|
795
|
+
retryable: false
|
|
796
|
+
});
|
|
797
|
+
var readOptionalTabId = (params, message = "tab_id must be a number when provided.") => {
|
|
798
|
+
const tabId = params.tab_id;
|
|
799
|
+
if (tabId !== void 0 && typeof tabId !== "number") {
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
error: invalidTabIdError(message)
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
ok: true,
|
|
807
|
+
tabId: typeof tabId === "number" ? tabId : void 0
|
|
808
|
+
};
|
|
809
|
+
};
|
|
810
|
+
var readRequiredTabId = (params, message = "tab_id must be a number.") => {
|
|
811
|
+
const tabId = params.tab_id;
|
|
812
|
+
if (typeof tabId !== "number") {
|
|
813
|
+
return {
|
|
814
|
+
ok: false,
|
|
815
|
+
error: invalidTabIdError(message)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
ok: true,
|
|
820
|
+
tabId
|
|
821
|
+
};
|
|
822
|
+
};
|
|
823
|
+
var resolveOptionalTabId = async (params, deps, message = "tab_id must be a number when provided.") => {
|
|
824
|
+
const parsed = readOptionalTabId(params, message);
|
|
825
|
+
if (!parsed.ok) {
|
|
826
|
+
return parsed;
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
ok: true,
|
|
830
|
+
tabId: parsed.tabId ?? await deps.getDefaultTabId()
|
|
831
|
+
};
|
|
832
|
+
};
|
|
833
|
+
var requireTab = async (tabId, getTab2) => {
|
|
834
|
+
try {
|
|
835
|
+
return {
|
|
836
|
+
ok: true,
|
|
837
|
+
tab: await getTab2(tabId)
|
|
838
|
+
};
|
|
839
|
+
} catch {
|
|
840
|
+
return {
|
|
841
|
+
ok: false,
|
|
842
|
+
error: {
|
|
843
|
+
code: "TAB_NOT_FOUND",
|
|
844
|
+
message: `tab_id ${tabId} was not found.`,
|
|
845
|
+
retryable: false,
|
|
846
|
+
details: { tab_id: tabId }
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// packages/extension/src/action-permissions.ts
|
|
853
|
+
var GATED_ACTIONS = /* @__PURE__ */ new Set([
|
|
854
|
+
"drive.navigate",
|
|
855
|
+
"drive.go_back",
|
|
856
|
+
"drive.go_forward",
|
|
857
|
+
"drive.click",
|
|
858
|
+
"drive.hover",
|
|
859
|
+
"drive.select",
|
|
860
|
+
"drive.type",
|
|
861
|
+
"drive.fill_form",
|
|
862
|
+
"drive.drag",
|
|
863
|
+
"drive.handle_dialog",
|
|
864
|
+
"drive.key",
|
|
865
|
+
"drive.key_press",
|
|
866
|
+
"drive.scroll",
|
|
867
|
+
"drive.screenshot",
|
|
868
|
+
"drive.wait_for"
|
|
869
|
+
]);
|
|
870
|
+
var gateDriveAction = async (options) => {
|
|
871
|
+
const { action, params, getDefaultTabId: getDefaultTabId2, getTab: getTab2, permissionPrompts: permissionPrompts2 } = options;
|
|
872
|
+
if (!GATED_ACTIONS.has(action)) {
|
|
873
|
+
return { ok: true, siteKey: null, touchOnSuccess: false };
|
|
874
|
+
}
|
|
875
|
+
let siteKey = null;
|
|
876
|
+
if (action === "drive.navigate") {
|
|
877
|
+
const url = params.url;
|
|
878
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
879
|
+
return { ok: true, siteKey: null, touchOnSuccess: false };
|
|
880
|
+
}
|
|
881
|
+
if (isRestrictedUrl(url)) {
|
|
882
|
+
return {
|
|
883
|
+
ok: false,
|
|
884
|
+
error: buildRestrictedUrlError({
|
|
885
|
+
url,
|
|
886
|
+
operation: "navigate",
|
|
887
|
+
action
|
|
888
|
+
})
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
siteKey = siteKeyFromUrl(url);
|
|
892
|
+
if (!siteKey) {
|
|
893
|
+
return {
|
|
894
|
+
ok: false,
|
|
895
|
+
error: {
|
|
896
|
+
code: "INVALID_ARGUMENT",
|
|
897
|
+
message: "Unable to resolve site permission key for url.",
|
|
898
|
+
retryable: false,
|
|
899
|
+
details: { url }
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
} else {
|
|
904
|
+
const parsedTabId = readOptionalTabId(params);
|
|
905
|
+
if (!parsedTabId.ok) {
|
|
906
|
+
return { ok: true, siteKey: null, touchOnSuccess: false };
|
|
907
|
+
}
|
|
908
|
+
const resolvedTabId = parsedTabId.tabId ?? await getDefaultTabId2();
|
|
909
|
+
const tab = await getTab2(resolvedTabId);
|
|
910
|
+
const url = tab.url;
|
|
911
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
912
|
+
return {
|
|
913
|
+
ok: false,
|
|
914
|
+
error: {
|
|
915
|
+
code: "FAILED_PRECONDITION",
|
|
916
|
+
message: "Active tab URL is unavailable for permission gating.",
|
|
917
|
+
retryable: false,
|
|
918
|
+
details: { tab_id: resolvedTabId }
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
if (isRestrictedUrl(url)) {
|
|
923
|
+
return {
|
|
924
|
+
ok: false,
|
|
925
|
+
error: buildRestrictedUrlError({
|
|
926
|
+
url,
|
|
927
|
+
operation: action === "drive.screenshot" ? "screenshot" : "action",
|
|
928
|
+
action
|
|
929
|
+
})
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
siteKey = siteKeyFromUrl(url);
|
|
933
|
+
if (!siteKey) {
|
|
934
|
+
return {
|
|
935
|
+
ok: false,
|
|
936
|
+
error: {
|
|
937
|
+
code: "FAILED_PRECONDITION",
|
|
938
|
+
message: "Unable to resolve site permission key for active tab.",
|
|
939
|
+
retryable: false,
|
|
940
|
+
details: { url, tab_id: resolvedTabId }
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (await readSitePermissionsMode() === "bypass") {
|
|
946
|
+
return { ok: true, siteKey, touchOnSuccess: false };
|
|
947
|
+
}
|
|
948
|
+
if (await isSiteAllowed(siteKey)) {
|
|
949
|
+
return { ok: true, siteKey, touchOnSuccess: true };
|
|
950
|
+
}
|
|
951
|
+
const decision = await permissionPrompts2.requestPermission({
|
|
952
|
+
siteKey,
|
|
953
|
+
action
|
|
954
|
+
});
|
|
955
|
+
if (decision.kind === "timed_out") {
|
|
956
|
+
return {
|
|
957
|
+
ok: false,
|
|
958
|
+
error: {
|
|
959
|
+
code: "PERMISSION_PROMPT_TIMEOUT",
|
|
960
|
+
message: `Permission prompt timed out for ${siteKey}.`,
|
|
961
|
+
retryable: true,
|
|
962
|
+
details: {
|
|
963
|
+
reason: "prompt_timed_out",
|
|
964
|
+
site: siteKey,
|
|
965
|
+
action,
|
|
966
|
+
wait_ms: decision.waitMs
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (decision.kind === "deny") {
|
|
972
|
+
return {
|
|
973
|
+
ok: false,
|
|
974
|
+
error: {
|
|
975
|
+
code: "PERMISSION_DENIED",
|
|
976
|
+
message: `User denied Browser Bridge permission for ${siteKey}.`,
|
|
977
|
+
retryable: false,
|
|
978
|
+
details: {
|
|
979
|
+
reason: "user_denied",
|
|
980
|
+
site: siteKey,
|
|
981
|
+
action,
|
|
982
|
+
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."
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (decision.kind === "allow_always") {
|
|
988
|
+
await allowSiteAlways(siteKey);
|
|
989
|
+
return { ok: true, siteKey, touchOnSuccess: true };
|
|
990
|
+
}
|
|
991
|
+
return { ok: true, siteKey, touchOnSuccess: false };
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// packages/extension/src/popup-trigger-state.ts
|
|
995
|
+
var popupTriggerStateChanged = (before, after) => {
|
|
996
|
+
if (!before || !after) {
|
|
997
|
+
return before !== after;
|
|
998
|
+
}
|
|
999
|
+
return before.ariaExpanded !== after.ariaExpanded || before.dataState !== after.dataState || before.open !== after.open;
|
|
1000
|
+
};
|
|
1001
|
+
var coercePopupTriggerState = (value) => {
|
|
1002
|
+
if (!value || typeof value !== "object") {
|
|
1003
|
+
return void 0;
|
|
1004
|
+
}
|
|
1005
|
+
const record = value;
|
|
1006
|
+
if (record.kind !== "popup_trigger") {
|
|
1007
|
+
return void 0;
|
|
1008
|
+
}
|
|
1009
|
+
const ariaHasPopup = typeof record.ariaHasPopup === "string" ? record.ariaHasPopup : void 0;
|
|
1010
|
+
const ariaExpanded = typeof record.ariaExpanded === "string" ? record.ariaExpanded : void 0;
|
|
1011
|
+
const dataState = typeof record.dataState === "string" ? record.dataState : void 0;
|
|
1012
|
+
const open = typeof record.open === "boolean" ? record.open : void 0;
|
|
1013
|
+
return {
|
|
1014
|
+
kind: "popup_trigger",
|
|
1015
|
+
...ariaHasPopup ? { ariaHasPopup } : {},
|
|
1016
|
+
...ariaExpanded !== void 0 ? { ariaExpanded } : {},
|
|
1017
|
+
...dataState !== void 0 ? { dataState } : {},
|
|
1018
|
+
...open !== void 0 ? { open } : {}
|
|
1019
|
+
};
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// packages/extension/src/popup-click-verification.ts
|
|
1023
|
+
var POPUP_TRIGGER_CLICK_SETTLE_MS = 50;
|
|
1024
|
+
var POPUP_TRIGGER_RECHECK_MS = 125;
|
|
1025
|
+
var readPopupTriggerAfterClick = async (options) => {
|
|
1026
|
+
const after = await options.resolveLocatorPoint(options.locator);
|
|
1027
|
+
if (!after.ok) {
|
|
1028
|
+
if (shouldTreatPostClickReadErrorAsSuccess(after.error)) {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: false,
|
|
1031
|
+
error: {
|
|
1032
|
+
code: "NOT_FOUND",
|
|
1033
|
+
message: "Popup trigger disappeared after click.",
|
|
1034
|
+
retryable: false,
|
|
1035
|
+
details: { reason: "popup_trigger_disappeared" }
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
return after;
|
|
1040
|
+
}
|
|
1041
|
+
return after;
|
|
1042
|
+
};
|
|
1043
|
+
var shouldTreatPostClickReadErrorAsSuccess = (error) => {
|
|
1044
|
+
return error.code === "LOCATOR_NOT_FOUND" || error.code === "NOT_FOUND" || error.retryable === true && (error.code === "TIMEOUT" || error.details?.reason === "transient_tab_channel_error");
|
|
1045
|
+
};
|
|
1046
|
+
var verifyPopupTriggerClick = async (options) => {
|
|
1047
|
+
if (options.prepareTarget) {
|
|
1048
|
+
await options.prepareTarget();
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
await options.dispatchCdpClick(
|
|
1052
|
+
options.point.x,
|
|
1053
|
+
options.point.y,
|
|
1054
|
+
options.clickCount
|
|
1055
|
+
);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
return { ok: false, error: options.mapDispatchError(error) };
|
|
1058
|
+
}
|
|
1059
|
+
await options.delayMs(options.settleMs ?? POPUP_TRIGGER_CLICK_SETTLE_MS);
|
|
1060
|
+
const after = await readPopupTriggerAfterClick(options);
|
|
1061
|
+
if (!after.ok) {
|
|
1062
|
+
if (after.error.details?.reason === "popup_trigger_disappeared") {
|
|
1063
|
+
return { ok: true };
|
|
1064
|
+
}
|
|
1065
|
+
return after;
|
|
1066
|
+
}
|
|
1067
|
+
if (popupTriggerStateChanged(
|
|
1068
|
+
options.point.targetState ?? null,
|
|
1069
|
+
after.point.targetState ?? null
|
|
1070
|
+
)) {
|
|
1071
|
+
return { ok: true };
|
|
1072
|
+
}
|
|
1073
|
+
if (options.clickCount === 1) {
|
|
1074
|
+
await options.delayMs(POPUP_TRIGGER_RECHECK_MS);
|
|
1075
|
+
const lateAfter = await readPopupTriggerAfterClick(options);
|
|
1076
|
+
if (!lateAfter.ok) {
|
|
1077
|
+
if (lateAfter.error.details?.reason === "popup_trigger_disappeared") {
|
|
1078
|
+
return { ok: true };
|
|
1079
|
+
}
|
|
1080
|
+
return lateAfter;
|
|
1081
|
+
}
|
|
1082
|
+
if (popupTriggerStateChanged(
|
|
1083
|
+
options.point.targetState ?? null,
|
|
1084
|
+
lateAfter.point.targetState ?? null
|
|
1085
|
+
)) {
|
|
1086
|
+
return { ok: true };
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
await options.dispatchCdpClick(
|
|
1090
|
+
options.point.x,
|
|
1091
|
+
options.point.y,
|
|
1092
|
+
options.clickCount
|
|
1093
|
+
);
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
return { ok: false, error: options.mapDispatchError(error) };
|
|
1096
|
+
}
|
|
1097
|
+
await options.delayMs(options.settleMs ?? POPUP_TRIGGER_CLICK_SETTLE_MS);
|
|
1098
|
+
const retryAfter = await readPopupTriggerAfterClick(options);
|
|
1099
|
+
if (!retryAfter.ok) {
|
|
1100
|
+
if (retryAfter.error.details?.reason === "popup_trigger_disappeared") {
|
|
1101
|
+
return { ok: true };
|
|
1102
|
+
}
|
|
1103
|
+
return retryAfter;
|
|
1104
|
+
}
|
|
1105
|
+
if (popupTriggerStateChanged(
|
|
1106
|
+
options.point.targetState ?? null,
|
|
1107
|
+
retryAfter.point.targetState ?? null
|
|
1108
|
+
)) {
|
|
1109
|
+
return { ok: true };
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
error: {
|
|
1115
|
+
code: "FAILED_PRECONDITION",
|
|
1116
|
+
message: "Click focused the popup trigger but did not change its open state.",
|
|
1117
|
+
retryable: false,
|
|
1118
|
+
details: {
|
|
1119
|
+
reason: "click_state_unchanged",
|
|
1120
|
+
control: "popup_trigger",
|
|
1121
|
+
aria_haspopup: options.point.targetState?.ariaHasPopup,
|
|
1122
|
+
aria_expanded_before: options.point.targetState?.ariaExpanded,
|
|
1123
|
+
aria_expanded_after: after.point.targetState?.ariaExpanded,
|
|
1124
|
+
data_state_before: options.point.targetState?.dataState,
|
|
1125
|
+
data_state_after: after.point.targetState?.dataState
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
};
|
|
1130
|
+
|
|
439
1131
|
// packages/extension/src/drive-reliability.ts
|
|
440
1132
|
var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
|
|
441
1133
|
"receiving end does not exist",
|
|
@@ -443,6 +1135,9 @@ var TRANSIENT_TAB_CHANNEL_ERROR_PATTERNS = [
|
|
|
443
1135
|
"the message port closed before a response was received",
|
|
444
1136
|
"extension port is moved into back/forward cache"
|
|
445
1137
|
];
|
|
1138
|
+
var CONTENT_SCRIPT_RECOVERY_ERROR_PATTERNS = [
|
|
1139
|
+
"receiving end does not exist"
|
|
1140
|
+
];
|
|
446
1141
|
var TAB_CHANNEL_RETRY_DELAYS_MS = [120, 200, 320, 500, 750, 1e3, 1200];
|
|
447
1142
|
var normalizePathname = (pathname) => {
|
|
448
1143
|
if (pathname.length === 0) {
|
|
@@ -462,6 +1157,34 @@ var isTransientTabChannelError = (message) => {
|
|
|
462
1157
|
(pattern) => normalized.includes(pattern)
|
|
463
1158
|
);
|
|
464
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
|
+
};
|
|
1179
|
+
var shouldRetryTabChannelFailure = (action, error) => {
|
|
1180
|
+
if (!error || typeof error !== "object") {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
if (isTransientTabChannelError(error.message)) {
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
return action === "drive.wait_for" && error.code === "TIMEOUT" && error.retryable === true && typeof error.message === "string" && error.message.includes("Timed out waiting for content response");
|
|
1187
|
+
};
|
|
465
1188
|
var getTabChannelRetryDelayMs = (attempt) => {
|
|
466
1189
|
if (!Number.isInteger(attempt) || attempt < 1) {
|
|
467
1190
|
return void 0;
|
|
@@ -544,23 +1267,242 @@ var ConnectionStateTracker = class {
|
|
|
544
1267
|
}
|
|
545
1268
|
getStatus() {
|
|
546
1269
|
return {
|
|
547
|
-
state: this.state,
|
|
548
|
-
endpoint: this.endpoint,
|
|
549
|
-
ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
|
|
550
|
-
reconnect_delay_ms: this.reconnectDelayMs,
|
|
551
|
-
retry_at: this.retryAt,
|
|
552
|
-
last_connected_at: this.lastConnectedAt,
|
|
553
|
-
last_disconnected_at: this.lastDisconnectedAt,
|
|
554
|
-
last_error_at: this.lastErrorAt,
|
|
555
|
-
last_error_message: this.lastErrorMessage,
|
|
556
|
-
consecutive_failures: this.consecutiveFailures
|
|
1270
|
+
state: this.state,
|
|
1271
|
+
endpoint: this.endpoint,
|
|
1272
|
+
ws_url: this.endpoint ? `ws://${this.endpoint.host}:${this.endpoint.port}/drive` : void 0,
|
|
1273
|
+
reconnect_delay_ms: this.reconnectDelayMs,
|
|
1274
|
+
retry_at: this.retryAt,
|
|
1275
|
+
last_connected_at: this.lastConnectedAt,
|
|
1276
|
+
last_disconnected_at: this.lastDisconnectedAt,
|
|
1277
|
+
last_error_at: this.lastErrorAt,
|
|
1278
|
+
last_error_message: this.lastErrorMessage,
|
|
1279
|
+
consecutive_failures: this.consecutiveFailures
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// packages/extension/src/debugger-dispatch.ts
|
|
1285
|
+
var dispatchDebuggerRequest = async (message, deps, responders) => {
|
|
1286
|
+
const { respondAck, respondError } = responders;
|
|
1287
|
+
try {
|
|
1288
|
+
switch (message.action) {
|
|
1289
|
+
case "debugger.attach": {
|
|
1290
|
+
const parsedTabId = readRequiredTabId(
|
|
1291
|
+
message.params ?? {}
|
|
1292
|
+
);
|
|
1293
|
+
if (!parsedTabId.ok) {
|
|
1294
|
+
respondError(parsedTabId.error);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const error = await deps.ensureDebuggerAttached(parsedTabId.tabId);
|
|
1298
|
+
if (error) {
|
|
1299
|
+
respondError(error);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
respondAck({ ok: true });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
case "debugger.detach": {
|
|
1306
|
+
const parsedTabId = readRequiredTabId(
|
|
1307
|
+
message.params ?? {}
|
|
1308
|
+
);
|
|
1309
|
+
if (!parsedTabId.ok) {
|
|
1310
|
+
respondError(parsedTabId.error);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const error = await deps.detachDebugger(parsedTabId.tabId);
|
|
1314
|
+
if (error) {
|
|
1315
|
+
respondError(error);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
respondAck({ ok: true });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
case "debugger.command": {
|
|
1322
|
+
const params = message.params ?? {};
|
|
1323
|
+
const parsedTabId = readRequiredTabId(
|
|
1324
|
+
params
|
|
1325
|
+
);
|
|
1326
|
+
if (!parsedTabId.ok) {
|
|
1327
|
+
respondError(parsedTabId.error);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (typeof params.method !== "string" || params.method.length === 0) {
|
|
1331
|
+
respondError({
|
|
1332
|
+
code: "INVALID_ARGUMENT",
|
|
1333
|
+
message: "method must be a non-empty string.",
|
|
1334
|
+
retryable: false
|
|
1335
|
+
});
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const session = deps.getSession(parsedTabId.tabId);
|
|
1339
|
+
if (session?.attachPromise) {
|
|
1340
|
+
try {
|
|
1341
|
+
await session.attachPromise;
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
const info = deps.mapDebuggerErrorMessage(
|
|
1344
|
+
error instanceof Error ? error.message : "Debugger attach failed."
|
|
1345
|
+
);
|
|
1346
|
+
deps.clearDebuggerSession(parsedTabId.tabId);
|
|
1347
|
+
respondError(info);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
const attachedSession = deps.getSession(parsedTabId.tabId);
|
|
1352
|
+
if (!attachedSession?.attached) {
|
|
1353
|
+
respondError({
|
|
1354
|
+
code: "FAILED_PRECONDITION",
|
|
1355
|
+
message: "Debugger is not attached to the requested tab.",
|
|
1356
|
+
retryable: false
|
|
1357
|
+
});
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
const result = await deps.sendDebuggerCommand(
|
|
1362
|
+
parsedTabId.tabId,
|
|
1363
|
+
params.method,
|
|
1364
|
+
params.params,
|
|
1365
|
+
deps.debuggerCommandTimeoutMs
|
|
1366
|
+
);
|
|
1367
|
+
deps.touchDebuggerSession(parsedTabId.tabId);
|
|
1368
|
+
respondAck(result);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const info = deps.mapDebuggerErrorMessage(
|
|
1371
|
+
error instanceof Error ? error.message : "Debugger command failed."
|
|
1372
|
+
);
|
|
1373
|
+
respondError(info);
|
|
1374
|
+
}
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
default:
|
|
1378
|
+
respondError({
|
|
1379
|
+
code: "NOT_IMPLEMENTED",
|
|
1380
|
+
message: `${message.action} not implemented in extension yet.`,
|
|
1381
|
+
retryable: false
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
const messageText = error instanceof Error ? error.message : "Unexpected debugger error.";
|
|
1386
|
+
respondError({
|
|
1387
|
+
code: "INSPECT_UNAVAILABLE",
|
|
1388
|
+
message: messageText,
|
|
1389
|
+
retryable: false
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// packages/extension/src/screenshot-errors.ts
|
|
1395
|
+
var isCaptureVisibleTabRateLimitedMessage = (message) => {
|
|
1396
|
+
const normalized = message.toLowerCase();
|
|
1397
|
+
const hasCaptureSignal = normalized.includes("capturevisibletab");
|
|
1398
|
+
const hasRateSignal = normalized.includes("max_capture_visible_tab_calls_per_second") || normalized.includes("too often") || normalized.includes("rate limit") || normalized.includes("rate-limit");
|
|
1399
|
+
return hasCaptureSignal && hasRateSignal;
|
|
1400
|
+
};
|
|
1401
|
+
var isCaptureVisibleTabPermissionMessage = (message) => {
|
|
1402
|
+
const normalized = message.toLowerCase();
|
|
1403
|
+
const hasCaptureSignal = normalized.includes("capturevisibletab");
|
|
1404
|
+
const hasPermissionSignal = normalized.includes("permission is required") || normalized.includes("requires permission") || normalized.includes("requires either");
|
|
1405
|
+
const hasPermissionTarget = normalized.includes("all_urls") || normalized.includes("activetab");
|
|
1406
|
+
return hasCaptureSignal && hasPermissionSignal && hasPermissionTarget;
|
|
1407
|
+
};
|
|
1408
|
+
var isCaptureVisibleTabRateLimitedError = (error) => error instanceof Error && isCaptureVisibleTabRateLimitedMessage(error.message);
|
|
1409
|
+
var isCaptureVisibleTabPermissionError = (error) => error instanceof Error && isCaptureVisibleTabPermissionMessage(error.message);
|
|
1410
|
+
var mapScreenshotCaptureError = (error, fallbackMessage) => {
|
|
1411
|
+
const message = error instanceof Error && error.message ? error.message : fallbackMessage;
|
|
1412
|
+
if (isCaptureVisibleTabRateLimitedError(error)) {
|
|
1413
|
+
return {
|
|
1414
|
+
code: "RATE_LIMITED",
|
|
1415
|
+
message: "Screenshot capture hit Chrome capture rate limits. Please retry shortly.",
|
|
1416
|
+
retryable: true,
|
|
1417
|
+
details: {
|
|
1418
|
+
reason: "capture_visible_tab_rate_limited",
|
|
1419
|
+
original_message: message
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
if (isCaptureVisibleTabPermissionError(error)) {
|
|
1424
|
+
return {
|
|
1425
|
+
code: "PERMISSION_REQUIRED",
|
|
1426
|
+
message: "Screenshot capture requires captureVisibleTab permission (<all_urls> or activeTab).",
|
|
1427
|
+
retryable: false,
|
|
1428
|
+
details: {
|
|
1429
|
+
reason: "capture_visible_tab_permission_required",
|
|
1430
|
+
required_any_of: ["<all_urls>", "activeTab"],
|
|
1431
|
+
next_step: "Reload the Browser Bridge extension in chrome://extensions and retry screenshot capture.",
|
|
1432
|
+
original_message: message
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
return {
|
|
1437
|
+
code: "ARTIFACT_IO_ERROR",
|
|
1438
|
+
message,
|
|
1439
|
+
retryable: false
|
|
1440
|
+
};
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// packages/extension/src/core-endpoint-config.ts
|
|
1444
|
+
var DEFAULT_CORE_HOST = "127.0.0.1";
|
|
1445
|
+
var DEFAULT_CORE_PORT = 3210;
|
|
1446
|
+
var LEGACY_CORE_PORT_KEY = "corePort";
|
|
1447
|
+
var hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
1448
|
+
var readCoreEndpointConfig = async () => ({
|
|
1449
|
+
host: DEFAULT_CORE_HOST,
|
|
1450
|
+
port: DEFAULT_CORE_PORT,
|
|
1451
|
+
portSource: "default"
|
|
1452
|
+
});
|
|
1453
|
+
var clearLegacyCorePort = async (storage) => {
|
|
1454
|
+
return await new Promise((resolve) => {
|
|
1455
|
+
storage.get([LEGACY_CORE_PORT_KEY], (items) => {
|
|
1456
|
+
if (!hasOwn(items, LEGACY_CORE_PORT_KEY)) {
|
|
1457
|
+
resolve(false);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
storage.remove([LEGACY_CORE_PORT_KEY], () => resolve(true));
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// packages/extension/src/tab-activation.ts
|
|
1466
|
+
var resolveTabActivationOutcome = ({
|
|
1467
|
+
tabId,
|
|
1468
|
+
windowId,
|
|
1469
|
+
activated,
|
|
1470
|
+
focusErrorMessage,
|
|
1471
|
+
windowFocused
|
|
1472
|
+
}) => {
|
|
1473
|
+
if (!activated) {
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
error: {
|
|
1477
|
+
code: "FAILED_PRECONDITION",
|
|
1478
|
+
message: `Failed to activate tab_id ${tabId}.`,
|
|
1479
|
+
retryable: true,
|
|
1480
|
+
details: { tab_id: tabId }
|
|
1481
|
+
}
|
|
557
1482
|
};
|
|
558
1483
|
}
|
|
1484
|
+
const warnings = [];
|
|
1485
|
+
if (typeof windowId === "number") {
|
|
1486
|
+
if (focusErrorMessage) {
|
|
1487
|
+
warnings.push(
|
|
1488
|
+
`Activated tab_id ${tabId}, but failed to focus window_id ${windowId}: ${focusErrorMessage}`
|
|
1489
|
+
);
|
|
1490
|
+
} else if (windowFocused === false) {
|
|
1491
|
+
warnings.push(
|
|
1492
|
+
`Activated tab_id ${tabId}, but window_id ${windowId} did not report focused state.`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
ok: true,
|
|
1498
|
+
result: {
|
|
1499
|
+
ok: true,
|
|
1500
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
559
1503
|
};
|
|
560
1504
|
|
|
561
1505
|
// packages/extension/src/background.ts
|
|
562
|
-
var DEFAULT_CORE_PORT = 3210;
|
|
563
|
-
var CORE_PORT_KEY = "corePort";
|
|
564
1506
|
var CORE_WS_PATH = "/drive";
|
|
565
1507
|
var CORE_HEALTH_PATH = "/health";
|
|
566
1508
|
var CORE_HEALTH_TIMEOUT_MS = 1200;
|
|
@@ -598,33 +1540,29 @@ var BASE_NEGOTIATED_CAPABILITIES = Object.freeze({
|
|
|
598
1540
|
"drive.tab_list": true,
|
|
599
1541
|
"drive.tab_activate": true,
|
|
600
1542
|
"drive.tab_close": true,
|
|
601
|
-
"drive.
|
|
1543
|
+
"drive.set_debugger_capability": 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
|
|
602
1551
|
});
|
|
603
1552
|
var DEBUGGER_CAPABILITY_ACTIONS = [
|
|
604
1553
|
"debugger.attach",
|
|
605
1554
|
"debugger.detach",
|
|
606
1555
|
"debugger.command"
|
|
607
1556
|
];
|
|
608
|
-
var buildNegotiatedCapabilities = (
|
|
1557
|
+
var buildNegotiatedCapabilities = () => {
|
|
609
1558
|
const capabilities = {
|
|
610
1559
|
...BASE_NEGOTIATED_CAPABILITIES
|
|
611
1560
|
};
|
|
612
1561
|
for (const action of DEBUGGER_CAPABILITY_ACTIONS) {
|
|
613
|
-
capabilities[action] =
|
|
1562
|
+
capabilities[action] = true;
|
|
614
1563
|
}
|
|
615
1564
|
return capabilities;
|
|
616
1565
|
};
|
|
617
|
-
var debuggerCapabilityDisabledError = () => {
|
|
618
|
-
return {
|
|
619
|
-
code: "ATTACH_DENIED",
|
|
620
|
-
message: "Debugger capability is disabled. Enable debugger-based inspect in extension options and retry.",
|
|
621
|
-
retryable: false,
|
|
622
|
-
details: {
|
|
623
|
-
reason: "debugger_capability_disabled",
|
|
624
|
-
next_step: "Open Browser Bridge extension options, enable debugger-based inspect, then retry."
|
|
625
|
-
}
|
|
626
|
-
};
|
|
627
|
-
};
|
|
628
1566
|
var getAgentTabBootstrapUrl = () => {
|
|
629
1567
|
return typeof chrome.runtime?.getURL === "function" ? chrome.runtime.getURL(AGENT_TAB_BOOTSTRAP_PATH) : AGENT_TAB_BOOTSTRAP_PATH;
|
|
630
1568
|
};
|
|
@@ -682,20 +1620,33 @@ var delayMs = async (ms) => {
|
|
|
682
1620
|
self.setTimeout(resolve, ms);
|
|
683
1621
|
});
|
|
684
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
|
+
};
|
|
685
1645
|
var CAPTURE_VISIBLE_TAB_MIN_INTERVAL_MS = 400;
|
|
686
1646
|
var CAPTURE_VISIBLE_TAB_MAX_RETRIES = 3;
|
|
687
1647
|
var CAPTURE_VISIBLE_TAB_RETRY_BASE_DELAY_MS = 500;
|
|
688
1648
|
var captureVisibleTabQueue = Promise.resolve();
|
|
689
1649
|
var captureVisibleTabLastCallAt = 0;
|
|
690
|
-
var isCaptureVisibleTabRateLimitedMessage = (message) => {
|
|
691
|
-
const normalized = message.toLowerCase();
|
|
692
|
-
const hasCaptureSignal = normalized.includes("capturevisibletab") || normalized.includes("max_capture_visible_tab_calls_per_second");
|
|
693
|
-
const hasRateSignal = normalized.includes("too often") || normalized.includes("rate limit") || normalized.includes("rate-limit");
|
|
694
|
-
return hasCaptureSignal && hasRateSignal;
|
|
695
|
-
};
|
|
696
|
-
var isCaptureVisibleTabRateLimitedError = (error) => {
|
|
697
|
-
return error instanceof Error && isCaptureVisibleTabRateLimitedMessage(error.message);
|
|
698
|
-
};
|
|
699
1650
|
var randomJitterMs = (maxMs) => {
|
|
700
1651
|
return Math.floor(Math.random() * Math.max(1, maxMs));
|
|
701
1652
|
};
|
|
@@ -737,25 +1688,6 @@ var captureVisibleTabWithThrottle = async (windowId) => {
|
|
|
737
1688
|
throw new Error("captureVisibleTab failed unexpectedly.");
|
|
738
1689
|
});
|
|
739
1690
|
};
|
|
740
|
-
var mapScreenshotCaptureError = (error, fallbackMessage) => {
|
|
741
|
-
const message = error instanceof Error && error.message ? error.message : fallbackMessage;
|
|
742
|
-
if (isCaptureVisibleTabRateLimitedError(error)) {
|
|
743
|
-
return {
|
|
744
|
-
code: "RATE_LIMITED",
|
|
745
|
-
message: "Screenshot capture hit Chrome capture rate limits. Please retry shortly.",
|
|
746
|
-
retryable: true,
|
|
747
|
-
details: {
|
|
748
|
-
reason: "capture_visible_tab_rate_limited",
|
|
749
|
-
original_message: message
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
return {
|
|
754
|
-
code: "ARTIFACT_IO_ERROR",
|
|
755
|
-
message,
|
|
756
|
-
retryable: false
|
|
757
|
-
};
|
|
758
|
-
};
|
|
759
1691
|
var parseDataUrl = (dataUrl) => {
|
|
760
1692
|
const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
|
|
761
1693
|
if (!match) {
|
|
@@ -801,40 +1733,6 @@ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
|
|
|
801
1733
|
bitmap.close();
|
|
802
1734
|
}
|
|
803
1735
|
};
|
|
804
|
-
var readCoreEndpointConfig = async () => {
|
|
805
|
-
return await new Promise((resolve) => {
|
|
806
|
-
chrome.storage.local.get(
|
|
807
|
-
[CORE_PORT_KEY],
|
|
808
|
-
(result) => {
|
|
809
|
-
const raw = result?.[CORE_PORT_KEY];
|
|
810
|
-
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
811
|
-
resolve({
|
|
812
|
-
host: "127.0.0.1",
|
|
813
|
-
port: raw,
|
|
814
|
-
portSource: "storage"
|
|
815
|
-
});
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
if (typeof raw === "string") {
|
|
819
|
-
const parsed = Number(raw);
|
|
820
|
-
if (Number.isFinite(parsed)) {
|
|
821
|
-
resolve({
|
|
822
|
-
host: "127.0.0.1",
|
|
823
|
-
port: parsed,
|
|
824
|
-
portSource: "storage"
|
|
825
|
-
});
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
resolve({
|
|
830
|
-
host: "127.0.0.1",
|
|
831
|
-
port: DEFAULT_CORE_PORT,
|
|
832
|
-
portSource: "default"
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
);
|
|
836
|
-
});
|
|
837
|
-
};
|
|
838
1736
|
var readDebuggerIdleTimeoutMs = async () => {
|
|
839
1737
|
return await new Promise((resolve) => {
|
|
840
1738
|
chrome.storage.local.get(
|
|
@@ -857,36 +1755,6 @@ var readDebuggerIdleTimeoutMs = async () => {
|
|
|
857
1755
|
);
|
|
858
1756
|
});
|
|
859
1757
|
};
|
|
860
|
-
var RESTRICTED_URL_PREFIXES = [
|
|
861
|
-
"chrome://",
|
|
862
|
-
"chrome-extension://",
|
|
863
|
-
"chrome-devtools://",
|
|
864
|
-
"devtools://",
|
|
865
|
-
"edge://",
|
|
866
|
-
"brave://",
|
|
867
|
-
"view-source:"
|
|
868
|
-
];
|
|
869
|
-
var isRestrictedUrl = (url) => {
|
|
870
|
-
if (!url || typeof url !== "string") {
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
const lowered = url.toLowerCase();
|
|
874
|
-
if (RESTRICTED_URL_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
|
|
875
|
-
return true;
|
|
876
|
-
}
|
|
877
|
-
try {
|
|
878
|
-
const parsed = new URL(url);
|
|
879
|
-
if (parsed.hostname === "chromewebstore.google.com") {
|
|
880
|
-
return true;
|
|
881
|
-
}
|
|
882
|
-
if (parsed.hostname === "chrome.google.com") {
|
|
883
|
-
return parsed.pathname.startsWith("/webstore");
|
|
884
|
-
}
|
|
885
|
-
} catch (error) {
|
|
886
|
-
console.debug("Ignoring invalid URL in restriction check.", error);
|
|
887
|
-
}
|
|
888
|
-
return false;
|
|
889
|
-
};
|
|
890
1758
|
var mapDebuggerErrorMessage = (message, fallbackCode = "INSPECT_UNAVAILABLE") => {
|
|
891
1759
|
const normalized = message.toLowerCase();
|
|
892
1760
|
if (normalized.includes("already attached") || normalized.includes("another debugger") || normalized.includes("attached to this target")) {
|
|
@@ -943,6 +1811,25 @@ var buildTabInfo = (tab) => {
|
|
|
943
1811
|
last_active_at: ensureLastActiveAt(tabId)
|
|
944
1812
|
};
|
|
945
1813
|
};
|
|
1814
|
+
var tabRecencyScore = (tab) => {
|
|
1815
|
+
const parsed = Date.parse(tab.last_active_at);
|
|
1816
|
+
return Number.isFinite(parsed) ? parsed : -Infinity;
|
|
1817
|
+
};
|
|
1818
|
+
var compareTabsForReport = (a, b) => {
|
|
1819
|
+
const aActive = a.active === true ? 1 : 0;
|
|
1820
|
+
const bActive = b.active === true ? 1 : 0;
|
|
1821
|
+
if (aActive !== bActive) {
|
|
1822
|
+
return bActive - aActive;
|
|
1823
|
+
}
|
|
1824
|
+
const recencyDelta = tabRecencyScore(b) - tabRecencyScore(a);
|
|
1825
|
+
if (recencyDelta !== 0) {
|
|
1826
|
+
return recencyDelta;
|
|
1827
|
+
}
|
|
1828
|
+
if (a.window_id !== b.window_id) {
|
|
1829
|
+
return a.window_id - b.window_id;
|
|
1830
|
+
}
|
|
1831
|
+
return a.tab_id - b.tab_id;
|
|
1832
|
+
};
|
|
946
1833
|
var queryTabs = async () => {
|
|
947
1834
|
const tabs = await wrapChromeCallback(
|
|
948
1835
|
(callback) => chrome.tabs.query({}, callback)
|
|
@@ -954,6 +1841,7 @@ var queryTabs = async () => {
|
|
|
954
1841
|
result.push(info);
|
|
955
1842
|
}
|
|
956
1843
|
}
|
|
1844
|
+
result.sort(compareTabsForReport);
|
|
957
1845
|
return result;
|
|
958
1846
|
};
|
|
959
1847
|
var getTab = async (tabId) => {
|
|
@@ -961,6 +1849,21 @@ var getTab = async (tabId) => {
|
|
|
961
1849
|
(callback) => chrome.tabs.get(tabId, callback)
|
|
962
1850
|
);
|
|
963
1851
|
};
|
|
1852
|
+
var getWindow = async (windowId) => {
|
|
1853
|
+
return await wrapChromeCallback(
|
|
1854
|
+
(callback) => chrome.windows.get(windowId, callback)
|
|
1855
|
+
);
|
|
1856
|
+
};
|
|
1857
|
+
var withResolvedTabTarget = async (tabId, result) => {
|
|
1858
|
+
const payload = result && typeof result === "object" && !Array.isArray(result) ? { ...result } : {};
|
|
1859
|
+
const tab = await getTab(tabId).catch(() => void 0);
|
|
1860
|
+
const windowId = tab && typeof tab.windowId === "number" ? tab.windowId : void 0;
|
|
1861
|
+
return {
|
|
1862
|
+
...payload,
|
|
1863
|
+
tab_id: tabId,
|
|
1864
|
+
...typeof windowId === "number" ? { window_id: windowId } : {}
|
|
1865
|
+
};
|
|
1866
|
+
};
|
|
964
1867
|
var getActiveTabId = async () => {
|
|
965
1868
|
const tabs = await wrapChromeCallback(
|
|
966
1869
|
(callback) => chrome.tabs.query({ active: true, lastFocusedWindow: true }, callback)
|
|
@@ -1194,12 +2097,19 @@ var sendToTab = async (tabId, action, params, options) => {
|
|
|
1194
2097
|
});
|
|
1195
2098
|
});
|
|
1196
2099
|
};
|
|
2100
|
+
let attemptedContentRecovery = false;
|
|
1197
2101
|
for (let attempt = 1; ; attempt += 1) {
|
|
1198
2102
|
const result = await attemptSend();
|
|
1199
2103
|
if (result.ok) {
|
|
1200
2104
|
return result;
|
|
1201
2105
|
}
|
|
1202
|
-
if (!
|
|
2106
|
+
if (!attemptedContentRecovery && shouldReinjectContentScript(
|
|
2107
|
+
result.error.message,
|
|
2108
|
+
(await getTab(tabId).catch(() => void 0))?.url
|
|
2109
|
+
)) {
|
|
2110
|
+
attemptedContentRecovery = await ensureTabContentScript(tabId);
|
|
2111
|
+
}
|
|
2112
|
+
if (!shouldRetryTabChannelFailure(action, result.error)) {
|
|
1203
2113
|
return result;
|
|
1204
2114
|
}
|
|
1205
2115
|
const retryDelayMs = getTabChannelRetryDelayMs(attempt);
|
|
@@ -1351,15 +2261,15 @@ var DriveSocket = class {
|
|
|
1351
2261
|
if (this.reconnectTimer !== null) {
|
|
1352
2262
|
return;
|
|
1353
2263
|
}
|
|
1354
|
-
const
|
|
1355
|
-
this.connection.markBackoff(
|
|
2264
|
+
const delay3 = this.reconnectDelayMs;
|
|
2265
|
+
this.connection.markBackoff(delay3);
|
|
1356
2266
|
this.reconnectTimer = self.setTimeout(() => {
|
|
1357
2267
|
this.reconnectTimer = null;
|
|
1358
2268
|
this.connection.markConnecting();
|
|
1359
2269
|
void this.connect().catch((error) => {
|
|
1360
2270
|
this.recordConnectionFailure("reconnect", error);
|
|
1361
2271
|
});
|
|
1362
|
-
},
|
|
2272
|
+
}, delay3);
|
|
1363
2273
|
this.reconnectDelayMs = Math.min(
|
|
1364
2274
|
this.maxReconnectDelayMs,
|
|
1365
2275
|
this.reconnectDelayMs * 2
|
|
@@ -1480,7 +2390,6 @@ var DriveSocket = class {
|
|
|
1480
2390
|
async sendHello() {
|
|
1481
2391
|
const manifest = chrome.runtime.getManifest();
|
|
1482
2392
|
const endpoint = await readCoreEndpointConfig();
|
|
1483
|
-
const debuggerCapabilityEnabled = await readDebuggerCapabilityEnabled();
|
|
1484
2393
|
let tabs = [];
|
|
1485
2394
|
try {
|
|
1486
2395
|
tabs = await queryTabs();
|
|
@@ -1489,9 +2398,10 @@ var DriveSocket = class {
|
|
|
1489
2398
|
tabs = [];
|
|
1490
2399
|
}
|
|
1491
2400
|
const params = {
|
|
2401
|
+
extension_id: chrome.runtime.id,
|
|
1492
2402
|
version: manifest.version,
|
|
1493
2403
|
protocol_version: DRIVE_WS_PROTOCOL_VERSION,
|
|
1494
|
-
capabilities: buildNegotiatedCapabilities(
|
|
2404
|
+
capabilities: buildNegotiatedCapabilities(),
|
|
1495
2405
|
core_host: endpoint.host,
|
|
1496
2406
|
core_port: endpoint.port,
|
|
1497
2407
|
core_port_source: endpoint.portSource,
|
|
@@ -1567,24 +2477,18 @@ var DriveSocket = class {
|
|
|
1567
2477
|
}
|
|
1568
2478
|
}
|
|
1569
2479
|
async refreshDebuggerCapabilityState() {
|
|
1570
|
-
const enabled = await readDebuggerCapabilityEnabled();
|
|
1571
|
-
if (!enabled) {
|
|
1572
|
-
await this.detachAllDebuggerSessions();
|
|
1573
|
-
}
|
|
1574
2480
|
this.refreshCapabilities();
|
|
1575
2481
|
}
|
|
1576
2482
|
async handleDebuggerCapabilityChange(enabled) {
|
|
1577
|
-
|
|
1578
|
-
await this.detachAllDebuggerSessions();
|
|
1579
|
-
}
|
|
2483
|
+
void enabled;
|
|
1580
2484
|
this.refreshCapabilities();
|
|
1581
2485
|
}
|
|
1582
2486
|
async handleRequest(message) {
|
|
1583
|
-
let
|
|
2487
|
+
let requestMessage = null;
|
|
1584
2488
|
let gatedSiteKey = null;
|
|
1585
2489
|
let touchGatedSiteOnSuccess = false;
|
|
1586
2490
|
const respondOk = (result) => {
|
|
1587
|
-
if (!
|
|
2491
|
+
if (!requestMessage) {
|
|
1588
2492
|
return;
|
|
1589
2493
|
}
|
|
1590
2494
|
if (touchGatedSiteOnSuccess && gatedSiteKey) {
|
|
@@ -1593,20 +2497,20 @@ var DriveSocket = class {
|
|
|
1593
2497
|
});
|
|
1594
2498
|
}
|
|
1595
2499
|
const response = {
|
|
1596
|
-
id:
|
|
1597
|
-
action:
|
|
2500
|
+
id: requestMessage.id,
|
|
2501
|
+
action: requestMessage.action,
|
|
1598
2502
|
status: "ok",
|
|
1599
2503
|
result
|
|
1600
2504
|
};
|
|
1601
2505
|
this.sendMessage(response);
|
|
1602
2506
|
};
|
|
1603
2507
|
const respondError = (error) => {
|
|
1604
|
-
if (!
|
|
2508
|
+
if (!requestMessage) {
|
|
1605
2509
|
return;
|
|
1606
2510
|
}
|
|
1607
2511
|
const response = {
|
|
1608
|
-
id:
|
|
1609
|
-
action:
|
|
2512
|
+
id: requestMessage.id,
|
|
2513
|
+
action: requestMessage.action,
|
|
1610
2514
|
status: "error",
|
|
1611
2515
|
error: sanitizeDriveErrorInfo(error)
|
|
1612
2516
|
};
|
|
@@ -1616,179 +2520,161 @@ var DriveSocket = class {
|
|
|
1616
2520
|
if (!message || typeof message !== "object" || typeof message.id !== "string" || typeof message.action !== "string") {
|
|
1617
2521
|
return;
|
|
1618
2522
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
this.sendMessage({
|
|
1622
|
-
id: message.id,
|
|
1623
|
-
action: message.action,
|
|
1624
|
-
status: "error",
|
|
1625
|
-
error: sanitizeDriveErrorInfo(debuggerCapabilityDisabledError())
|
|
1626
|
-
});
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
2523
|
+
const action = message.action;
|
|
2524
|
+
if (action.startsWith("debugger.")) {
|
|
1629
2525
|
await this.handleDebuggerRequest(message);
|
|
1630
2526
|
return;
|
|
1631
2527
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
}
|
|
1635
|
-
driveMessage = message;
|
|
1636
|
-
const gatedActions = /* @__PURE__ */ new Set([
|
|
1637
|
-
"drive.navigate",
|
|
1638
|
-
"drive.go_back",
|
|
1639
|
-
"drive.go_forward",
|
|
1640
|
-
"drive.click",
|
|
1641
|
-
"drive.hover",
|
|
1642
|
-
"drive.select",
|
|
1643
|
-
"drive.type",
|
|
1644
|
-
"drive.fill_form",
|
|
1645
|
-
"drive.drag",
|
|
1646
|
-
"drive.handle_dialog",
|
|
1647
|
-
"drive.key",
|
|
1648
|
-
"drive.key_press",
|
|
1649
|
-
"drive.scroll",
|
|
1650
|
-
"drive.screenshot",
|
|
1651
|
-
"drive.wait_for"
|
|
1652
|
-
]);
|
|
1653
|
-
const gateDriveAction = async () => {
|
|
1654
|
-
const action = message.action;
|
|
1655
|
-
if (!gatedActions.has(action)) {
|
|
1656
|
-
return { ok: true, siteKey: null, touchOnSuccess: false };
|
|
1657
|
-
}
|
|
2528
|
+
requestMessage = message;
|
|
2529
|
+
if (action.startsWith("permissions.")) {
|
|
1658
2530
|
const params = message.params ?? {};
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1674
|
-
};
|
|
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;
|
|
1675
2545
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
code: "INVALID_ARGUMENT",
|
|
1682
|
-
message: "Unable to resolve site permission key for url.",
|
|
1683
|
-
retryable: false,
|
|
1684
|
-
details: { url }
|
|
1685
|
-
}
|
|
1686
|
-
};
|
|
2546
|
+
case "permissions.get_mode": {
|
|
2547
|
+
respondOk({
|
|
2548
|
+
mode: await readSitePermissionsMode()
|
|
2549
|
+
});
|
|
2550
|
+
return;
|
|
1687
2551
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2552
|
+
case "permissions.list_pending_requests": {
|
|
2553
|
+
respondOk({
|
|
2554
|
+
requests: permissionsRequests.listPendingRequests()
|
|
2555
|
+
});
|
|
2556
|
+
return;
|
|
1692
2557
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
error: {
|
|
1700
|
-
code: "FAILED_PRECONDITION",
|
|
1701
|
-
message: "Active tab URL is unavailable for permission gating.",
|
|
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.",
|
|
1702
2564
|
retryable: false,
|
|
1703
|
-
details: {
|
|
1704
|
-
}
|
|
1705
|
-
|
|
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;
|
|
1706
2578
|
}
|
|
1707
|
-
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
message: message2,
|
|
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.",
|
|
1714
2585
|
retryable: false,
|
|
1715
|
-
details: {
|
|
1716
|
-
}
|
|
1717
|
-
|
|
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;
|
|
1718
2599
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
message: "Unable to resolve site permission key for active tab.",
|
|
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.",
|
|
1726
2606
|
retryable: false,
|
|
1727
|
-
details: {
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
if (await readSitePermissionsMode() === "bypass") {
|
|
1733
|
-
return { ok: true, siteKey, touchOnSuccess: false };
|
|
1734
|
-
}
|
|
1735
|
-
if (await isSiteAllowed(siteKey)) {
|
|
1736
|
-
return { ok: true, siteKey, touchOnSuccess: true };
|
|
1737
|
-
}
|
|
1738
|
-
const decision = await permissionPrompts.requestPermission({
|
|
1739
|
-
siteKey,
|
|
1740
|
-
action
|
|
1741
|
-
});
|
|
1742
|
-
if (decision.kind === "timed_out") {
|
|
1743
|
-
return {
|
|
1744
|
-
ok: false,
|
|
1745
|
-
error: {
|
|
1746
|
-
code: "PERMISSION_PROMPT_TIMEOUT",
|
|
1747
|
-
message: `Permission prompt timed out for ${siteKey}.`,
|
|
1748
|
-
retryable: true,
|
|
1749
|
-
details: {
|
|
1750
|
-
reason: "prompt_timed_out",
|
|
1751
|
-
site: siteKey,
|
|
1752
|
-
action,
|
|
1753
|
-
wait_ms: decision.waitMs
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
if (decision.kind === "deny") {
|
|
1759
|
-
return {
|
|
1760
|
-
ok: false,
|
|
1761
|
-
error: {
|
|
1762
|
-
code: "PERMISSION_DENIED",
|
|
1763
|
-
message: `User denied Browser Bridge permission for ${siteKey}.`,
|
|
1764
|
-
retryable: false,
|
|
1765
|
-
details: {
|
|
1766
|
-
reason: "user_denied",
|
|
1767
|
-
site: siteKey,
|
|
1768
|
-
action,
|
|
1769
|
-
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."
|
|
1770
|
-
}
|
|
2607
|
+
details: { field: "mode" }
|
|
2608
|
+
});
|
|
2609
|
+
return;
|
|
1771
2610
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2611
|
+
respondOk(
|
|
2612
|
+
await permissionsRequests.requestChange({
|
|
2613
|
+
kind: "set_mode",
|
|
2614
|
+
mode,
|
|
2615
|
+
timeoutMs,
|
|
2616
|
+
source
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
1777
2621
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2622
|
+
}
|
|
2623
|
+
if (!action.startsWith("drive.")) {
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
const gated = await gateDriveAction({
|
|
2627
|
+
action,
|
|
2628
|
+
params: message.params ?? {},
|
|
2629
|
+
getDefaultTabId,
|
|
2630
|
+
getTab,
|
|
2631
|
+
permissionPrompts
|
|
2632
|
+
});
|
|
1781
2633
|
if (!gated.ok) {
|
|
1782
2634
|
respondError(gated.error);
|
|
1783
2635
|
return;
|
|
1784
2636
|
}
|
|
1785
2637
|
gatedSiteKey = gated.siteKey;
|
|
1786
2638
|
touchGatedSiteOnSuccess = gated.touchOnSuccess;
|
|
2639
|
+
const resolveActionTabId = async (params) => await resolveOptionalTabId(params, { getDefaultTabId });
|
|
1787
2640
|
switch (message.action) {
|
|
1788
2641
|
case "drive.ping": {
|
|
1789
2642
|
respondOk({ ok: true });
|
|
1790
2643
|
return;
|
|
1791
2644
|
}
|
|
2645
|
+
case "drive.set_debugger_capability": {
|
|
2646
|
+
const params = message.params ?? {};
|
|
2647
|
+
const enabled = typeof params.enabled === "boolean" ? params.enabled : true;
|
|
2648
|
+
const expectedExtensionId = params.extension_id;
|
|
2649
|
+
if (expectedExtensionId !== void 0 && typeof expectedExtensionId !== "string") {
|
|
2650
|
+
respondError({
|
|
2651
|
+
code: "INVALID_ARGUMENT",
|
|
2652
|
+
message: "extension_id must be a string when provided.",
|
|
2653
|
+
retryable: false
|
|
2654
|
+
});
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
if (typeof expectedExtensionId === "string" && expectedExtensionId.length > 0 && expectedExtensionId !== chrome.runtime.id) {
|
|
2658
|
+
respondError({
|
|
2659
|
+
code: "FAILED_PRECONDITION",
|
|
2660
|
+
message: "Connected extension id does not match the requested extension.",
|
|
2661
|
+
retryable: false,
|
|
2662
|
+
details: {
|
|
2663
|
+
expected_extension_id: expectedExtensionId,
|
|
2664
|
+
connected_extension_id: chrome.runtime.id
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
await writeDebuggerCapabilityEnabled(enabled);
|
|
2670
|
+
await this.refreshDebuggerCapabilityState();
|
|
2671
|
+
respondOk({
|
|
2672
|
+
ok: true,
|
|
2673
|
+
enabled,
|
|
2674
|
+
extension_id: chrome.runtime.id
|
|
2675
|
+
});
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
1792
2678
|
case "drive.navigate": {
|
|
1793
2679
|
const params = message.params ?? {};
|
|
1794
2680
|
const url = params.url;
|
|
@@ -1800,21 +2686,38 @@ var DriveSocket = class {
|
|
|
1800
2686
|
});
|
|
1801
2687
|
return;
|
|
1802
2688
|
}
|
|
1803
|
-
|
|
1804
|
-
if (
|
|
2689
|
+
const tabTarget = await resolveActionTabId(params);
|
|
2690
|
+
if (!tabTarget.ok) {
|
|
2691
|
+
respondError(tabTarget.error);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const tabId = tabTarget.tabId;
|
|
2695
|
+
const requestedWaitMode = params.wait;
|
|
2696
|
+
if (requestedWaitMode !== void 0 && requestedWaitMode !== "none" && requestedWaitMode !== "domcontentloaded" && requestedWaitMode !== "networkidle") {
|
|
1805
2697
|
respondError({
|
|
1806
2698
|
code: "INVALID_ARGUMENT",
|
|
1807
|
-
message:
|
|
1808
|
-
retryable: false
|
|
2699
|
+
message: `Unsupported wait mode: ${String(requestedWaitMode)}.`,
|
|
2700
|
+
retryable: false,
|
|
2701
|
+
details: {
|
|
2702
|
+
field: "wait",
|
|
2703
|
+
supported_wait_modes: [
|
|
2704
|
+
"none",
|
|
2705
|
+
"domcontentloaded",
|
|
2706
|
+
"networkidle"
|
|
2707
|
+
],
|
|
2708
|
+
mapped_wait_mode: "domcontentloaded"
|
|
2709
|
+
}
|
|
1809
2710
|
});
|
|
1810
2711
|
return;
|
|
1811
2712
|
}
|
|
1812
|
-
|
|
1813
|
-
tabId = await getDefaultTabId();
|
|
1814
|
-
}
|
|
1815
|
-
const waitMode = params.wait === "none" || params.wait === "domcontentloaded" ? params.wait : "domcontentloaded";
|
|
2713
|
+
const waitMode = requestedWaitMode === "none" ? "none" : "domcontentloaded";
|
|
1816
2714
|
const domContentLoadedSignal = waitMode === "domcontentloaded" ? waitForDomContentLoaded(tabId, 3e4) : null;
|
|
1817
2715
|
const warnings = [];
|
|
2716
|
+
if (requestedWaitMode === "networkidle") {
|
|
2717
|
+
warnings.push(
|
|
2718
|
+
"wait=networkidle is mapped to domcontentloaded in this runtime."
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
1818
2721
|
await wrapChromeVoid(
|
|
1819
2722
|
(callback) => chrome.tabs.update(tabId, { url }, () => callback())
|
|
1820
2723
|
);
|
|
@@ -1841,39 +2744,30 @@ var DriveSocket = class {
|
|
|
1841
2744
|
if (tabId === agentTabId) {
|
|
1842
2745
|
void refreshAgentTabBranding(tabId);
|
|
1843
2746
|
}
|
|
1844
|
-
respondOk(
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2747
|
+
respondOk(
|
|
2748
|
+
await withResolvedTabTarget(tabId, {
|
|
2749
|
+
ok: true,
|
|
2750
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2751
|
+
})
|
|
2752
|
+
);
|
|
1848
2753
|
return;
|
|
1849
2754
|
}
|
|
1850
2755
|
case "drive.go_back":
|
|
1851
2756
|
case "drive.go_forward": {
|
|
1852
2757
|
const params = message.params ?? {};
|
|
1853
|
-
|
|
1854
|
-
if (
|
|
1855
|
-
respondError(
|
|
1856
|
-
code: "INVALID_ARGUMENT",
|
|
1857
|
-
message: "tab_id must be a number when provided.",
|
|
1858
|
-
retryable: false
|
|
1859
|
-
});
|
|
2758
|
+
const tabTarget = await resolveActionTabId(params);
|
|
2759
|
+
if (!tabTarget.ok) {
|
|
2760
|
+
respondError(tabTarget.error);
|
|
1860
2761
|
return;
|
|
1861
2762
|
}
|
|
1862
|
-
|
|
1863
|
-
tabId = await getDefaultTabId();
|
|
1864
|
-
}
|
|
2763
|
+
const tabId = tabTarget.tabId;
|
|
1865
2764
|
const navigationSignal = waitForHistoryNavigationSignal(
|
|
1866
2765
|
tabId,
|
|
1867
2766
|
HISTORY_NAVIGATION_SIGNAL_TIMEOUT_MS
|
|
1868
2767
|
);
|
|
1869
|
-
const result = await sendToTab(
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
void 0,
|
|
1873
|
-
{
|
|
1874
|
-
timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
|
|
1875
|
-
}
|
|
1876
|
-
);
|
|
2768
|
+
const result = await sendToTab(tabId, message.action, void 0, {
|
|
2769
|
+
timeoutMs: HISTORY_DISPATCH_TIMEOUT_MS
|
|
2770
|
+
});
|
|
1877
2771
|
if (!result.ok && result.error.code !== "TIMEOUT") {
|
|
1878
2772
|
respondError(result.error);
|
|
1879
2773
|
return;
|
|
@@ -1899,9 +2793,11 @@ var DriveSocket = class {
|
|
|
1899
2793
|
if (tabId === agentTabId) {
|
|
1900
2794
|
void refreshAgentTabBranding(tabId);
|
|
1901
2795
|
}
|
|
1902
|
-
respondOk(
|
|
1903
|
-
|
|
1904
|
-
|
|
2796
|
+
respondOk(
|
|
2797
|
+
await withResolvedTabTarget(tabId, {
|
|
2798
|
+
ok: true
|
|
2799
|
+
})
|
|
2800
|
+
);
|
|
1905
2801
|
return;
|
|
1906
2802
|
}
|
|
1907
2803
|
case "drive.tab_list": {
|
|
@@ -1911,44 +2807,66 @@ var DriveSocket = class {
|
|
|
1911
2807
|
return;
|
|
1912
2808
|
}
|
|
1913
2809
|
case "drive.tab_activate": {
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
retryable: false
|
|
1920
|
-
});
|
|
2810
|
+
const parsedTabId = readRequiredTabId(
|
|
2811
|
+
message.params ?? {}
|
|
2812
|
+
);
|
|
2813
|
+
if (!parsedTabId.ok) {
|
|
2814
|
+
respondError(parsedTabId.error);
|
|
1921
2815
|
return;
|
|
1922
2816
|
}
|
|
1923
|
-
const
|
|
2817
|
+
const tabId = parsedTabId.tabId;
|
|
2818
|
+
const tabLookup = await requireTab(tabId, getTab);
|
|
2819
|
+
if (!tabLookup.ok) {
|
|
2820
|
+
respondError(tabLookup.error);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
const tab = tabLookup.tab;
|
|
1924
2824
|
await wrapChromeVoid(
|
|
1925
2825
|
(callback) => chrome.tabs.update(tabId, { active: true }, () => callback())
|
|
1926
2826
|
);
|
|
1927
2827
|
const windowId = tab.windowId;
|
|
2828
|
+
let focusErrorMessage;
|
|
1928
2829
|
if (typeof windowId === "number") {
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2830
|
+
try {
|
|
2831
|
+
await wrapChromeVoid(
|
|
2832
|
+
(callback) => chrome.windows.update(
|
|
2833
|
+
windowId,
|
|
2834
|
+
{ focused: true },
|
|
2835
|
+
() => callback()
|
|
2836
|
+
)
|
|
2837
|
+
);
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
focusErrorMessage = error instanceof Error ? error.message : "Unknown focus error.";
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
const activatedTab = await getTab(tabId).catch(() => void 0);
|
|
2843
|
+
const focusedWindow = typeof windowId === "number" ? await getWindow(windowId).catch(() => void 0) : void 0;
|
|
2844
|
+
const windowFocused = focusedWindow && typeof focusedWindow.focused === "boolean" ? focusedWindow.focused : void 0;
|
|
2845
|
+
const outcome = resolveTabActivationOutcome({
|
|
2846
|
+
tabId,
|
|
2847
|
+
windowId: typeof windowId === "number" ? windowId : void 0,
|
|
2848
|
+
activated: Boolean(activatedTab?.active === true),
|
|
2849
|
+
focusErrorMessage,
|
|
2850
|
+
windowFocused
|
|
2851
|
+
});
|
|
2852
|
+
if (!outcome.ok) {
|
|
2853
|
+
respondError(outcome.error);
|
|
2854
|
+
return;
|
|
1936
2855
|
}
|
|
1937
2856
|
markTabActive(tabId);
|
|
1938
|
-
respondOk(
|
|
2857
|
+
respondOk(outcome.result);
|
|
1939
2858
|
this.sendTabReport();
|
|
1940
2859
|
return;
|
|
1941
2860
|
}
|
|
1942
2861
|
case "drive.tab_close": {
|
|
1943
|
-
const
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
retryable: false
|
|
1949
|
-
});
|
|
2862
|
+
const parsedTabId = readRequiredTabId(
|
|
2863
|
+
message.params ?? {}
|
|
2864
|
+
);
|
|
2865
|
+
if (!parsedTabId.ok) {
|
|
2866
|
+
respondError(parsedTabId.error);
|
|
1950
2867
|
return;
|
|
1951
2868
|
}
|
|
2869
|
+
const tabId = parsedTabId.tabId;
|
|
1952
2870
|
await wrapChromeVoid(
|
|
1953
2871
|
(callback) => chrome.tabs.remove(tabId, () => callback())
|
|
1954
2872
|
);
|
|
@@ -1962,8 +2880,8 @@ var DriveSocket = class {
|
|
|
1962
2880
|
}
|
|
1963
2881
|
case "drive.handle_dialog": {
|
|
1964
2882
|
const params = message.params ?? {};
|
|
1965
|
-
const
|
|
1966
|
-
if (
|
|
2883
|
+
const action2 = params.action;
|
|
2884
|
+
if (action2 !== "accept" && action2 !== "dismiss") {
|
|
1967
2885
|
respondError({
|
|
1968
2886
|
code: "INVALID_ARGUMENT",
|
|
1969
2887
|
message: "action must be accept or dismiss.",
|
|
@@ -1980,18 +2898,12 @@ var DriveSocket = class {
|
|
|
1980
2898
|
});
|
|
1981
2899
|
return;
|
|
1982
2900
|
}
|
|
1983
|
-
|
|
1984
|
-
if (
|
|
1985
|
-
respondError(
|
|
1986
|
-
code: "INVALID_ARGUMENT",
|
|
1987
|
-
message: "tab_id must be a number when provided.",
|
|
1988
|
-
retryable: false
|
|
1989
|
-
});
|
|
2901
|
+
const tabTarget = await resolveActionTabId(params);
|
|
2902
|
+
if (!tabTarget.ok) {
|
|
2903
|
+
respondError(tabTarget.error);
|
|
1990
2904
|
return;
|
|
1991
2905
|
}
|
|
1992
|
-
|
|
1993
|
-
tabId = await getDefaultTabId();
|
|
1994
|
-
}
|
|
2906
|
+
const tabId = tabTarget.tabId;
|
|
1995
2907
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
1996
2908
|
if (error) {
|
|
1997
2909
|
respondError(error);
|
|
@@ -2002,13 +2914,13 @@ var DriveSocket = class {
|
|
|
2002
2914
|
tabId,
|
|
2003
2915
|
"Page.handleJavaScriptDialog",
|
|
2004
2916
|
{
|
|
2005
|
-
accept:
|
|
2917
|
+
accept: action2 === "accept",
|
|
2006
2918
|
...promptText ? { promptText } : {}
|
|
2007
2919
|
},
|
|
2008
2920
|
DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
2009
2921
|
);
|
|
2010
2922
|
this.touchDebuggerSession(tabId);
|
|
2011
|
-
respondOk({ ok: true });
|
|
2923
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2012
2924
|
} catch (error2) {
|
|
2013
2925
|
const info = mapDebuggerErrorMessage(
|
|
2014
2926
|
error2 instanceof Error ? error2.message : "Dialog handling failed."
|
|
@@ -2019,18 +2931,12 @@ var DriveSocket = class {
|
|
|
2019
2931
|
}
|
|
2020
2932
|
case "drive.click": {
|
|
2021
2933
|
const params = message.params ?? {};
|
|
2022
|
-
|
|
2023
|
-
if (
|
|
2024
|
-
respondError(
|
|
2025
|
-
code: "INVALID_ARGUMENT",
|
|
2026
|
-
message: "tab_id must be a number when provided.",
|
|
2027
|
-
retryable: false
|
|
2028
|
-
});
|
|
2934
|
+
const tabTarget = await resolveActionTabId(params);
|
|
2935
|
+
if (!tabTarget.ok) {
|
|
2936
|
+
respondError(tabTarget.error);
|
|
2029
2937
|
return;
|
|
2030
2938
|
}
|
|
2031
|
-
|
|
2032
|
-
tabId = await getDefaultTabId();
|
|
2033
|
-
}
|
|
2939
|
+
const tabId = tabTarget.tabId;
|
|
2034
2940
|
const clickCount = params.click_count;
|
|
2035
2941
|
const count = typeof clickCount === "number" && Number.isFinite(clickCount) ? Math.max(1, Math.floor(clickCount)) : 1;
|
|
2036
2942
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
@@ -2046,31 +2952,48 @@ var DriveSocket = class {
|
|
|
2046
2952
|
respondError(pointResult.error);
|
|
2047
2953
|
return;
|
|
2048
2954
|
}
|
|
2049
|
-
const { x, y } = pointResult.point;
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2955
|
+
const { x, y, targetState } = pointResult.point;
|
|
2956
|
+
if (targetState) {
|
|
2957
|
+
const verified = await verifyPopupTriggerClick({
|
|
2958
|
+
clickCount: count,
|
|
2959
|
+
locator: params.locator,
|
|
2960
|
+
point: pointResult.point,
|
|
2961
|
+
prepareTarget: async () => await this.focusLocator(tabId, params.locator),
|
|
2962
|
+
resolveLocatorPoint: async (locator) => await this.resolveLocatorPoint(tabId, locator),
|
|
2963
|
+
dispatchCdpClick: async (clickX, clickY, clickCount2) => await this.dispatchCdpClick(tabId, clickX, clickY, clickCount2),
|
|
2964
|
+
mapDispatchError: (error2) => mapDebuggerErrorMessage(
|
|
2965
|
+
error2 instanceof Error ? error2.message : "Click dispatch failed."
|
|
2966
|
+
),
|
|
2967
|
+
delayMs
|
|
2968
|
+
});
|
|
2969
|
+
if (!verified.ok) {
|
|
2970
|
+
respondError(verified.error);
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
try {
|
|
2977
|
+
await this.dispatchCdpClick(tabId, x, y, count);
|
|
2978
|
+
} catch (error2) {
|
|
2979
|
+
respondError(
|
|
2980
|
+
mapDebuggerErrorMessage(
|
|
2981
|
+
error2 instanceof Error ? error2.message : "Click dispatch failed."
|
|
2982
|
+
)
|
|
2055
2983
|
);
|
|
2056
|
-
|
|
2057
|
-
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2058
2987
|
return;
|
|
2059
2988
|
}
|
|
2060
2989
|
case "drive.hover": {
|
|
2061
2990
|
const params = message.params ?? {};
|
|
2062
|
-
|
|
2063
|
-
if (
|
|
2064
|
-
respondError(
|
|
2065
|
-
code: "INVALID_ARGUMENT",
|
|
2066
|
-
message: "tab_id must be a number when provided.",
|
|
2067
|
-
retryable: false
|
|
2068
|
-
});
|
|
2991
|
+
const tabTarget = await resolveActionTabId(params);
|
|
2992
|
+
if (!tabTarget.ok) {
|
|
2993
|
+
respondError(tabTarget.error);
|
|
2069
2994
|
return;
|
|
2070
2995
|
}
|
|
2071
|
-
|
|
2072
|
-
tabId = await getDefaultTabId();
|
|
2073
|
-
}
|
|
2996
|
+
const tabId = tabTarget.tabId;
|
|
2074
2997
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2075
2998
|
if (error) {
|
|
2076
2999
|
respondError(error);
|
|
@@ -2091,15 +3014,17 @@ var DriveSocket = class {
|
|
|
2091
3014
|
if (waitMs > 0) {
|
|
2092
3015
|
await delayMs(waitMs);
|
|
2093
3016
|
}
|
|
2094
|
-
const snapshot = await sendToTab(
|
|
2095
|
-
tabId,
|
|
2096
|
-
"drive.snapshot_html"
|
|
2097
|
-
);
|
|
3017
|
+
const snapshot = await sendToTab(tabId, "drive.snapshot_html");
|
|
2098
3018
|
if (!snapshot.ok) {
|
|
2099
3019
|
respondError(snapshot.error);
|
|
2100
3020
|
return;
|
|
2101
3021
|
}
|
|
2102
|
-
respondOk(
|
|
3022
|
+
respondOk(
|
|
3023
|
+
await withResolvedTabTarget(
|
|
3024
|
+
tabId,
|
|
3025
|
+
snapshot.result ?? { format: "html", snapshot: "" }
|
|
3026
|
+
)
|
|
3027
|
+
);
|
|
2103
3028
|
} catch (error2) {
|
|
2104
3029
|
const info = mapDebuggerErrorMessage(
|
|
2105
3030
|
error2 instanceof Error ? error2.message : "Hover dispatch failed."
|
|
@@ -2110,35 +3035,23 @@ var DriveSocket = class {
|
|
|
2110
3035
|
}
|
|
2111
3036
|
case "drive.drag": {
|
|
2112
3037
|
const params = message.params ?? {};
|
|
2113
|
-
|
|
2114
|
-
if (
|
|
2115
|
-
respondError(
|
|
2116
|
-
code: "INVALID_ARGUMENT",
|
|
2117
|
-
message: "tab_id must be a number when provided.",
|
|
2118
|
-
retryable: false
|
|
2119
|
-
});
|
|
3038
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3039
|
+
if (!tabTarget.ok) {
|
|
3040
|
+
respondError(tabTarget.error);
|
|
2120
3041
|
return;
|
|
2121
3042
|
}
|
|
2122
|
-
|
|
2123
|
-
tabId = await getDefaultTabId();
|
|
2124
|
-
}
|
|
3043
|
+
const tabId = tabTarget.tabId;
|
|
2125
3044
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2126
3045
|
if (error) {
|
|
2127
3046
|
respondError(error);
|
|
2128
3047
|
return;
|
|
2129
3048
|
}
|
|
2130
|
-
const fromResult = await this.resolveLocatorPoint(
|
|
2131
|
-
tabId,
|
|
2132
|
-
params.from
|
|
2133
|
-
);
|
|
3049
|
+
const fromResult = await this.resolveLocatorPoint(tabId, params.from);
|
|
2134
3050
|
if (!fromResult.ok) {
|
|
2135
3051
|
respondError(fromResult.error);
|
|
2136
3052
|
return;
|
|
2137
3053
|
}
|
|
2138
|
-
const toResult = await this.resolveLocatorPoint(
|
|
2139
|
-
tabId,
|
|
2140
|
-
params.to
|
|
2141
|
-
);
|
|
3054
|
+
const toResult = await this.resolveLocatorPoint(tabId, params.to);
|
|
2142
3055
|
if (!toResult.ok) {
|
|
2143
3056
|
respondError(toResult.error);
|
|
2144
3057
|
return;
|
|
@@ -2151,7 +3064,7 @@ var DriveSocket = class {
|
|
|
2151
3064
|
toResult.point,
|
|
2152
3065
|
steps
|
|
2153
3066
|
);
|
|
2154
|
-
respondOk({ ok: true });
|
|
3067
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2155
3068
|
} catch (error2) {
|
|
2156
3069
|
const info = mapDebuggerErrorMessage(
|
|
2157
3070
|
error2 instanceof Error ? error2.message : "Drag dispatch failed."
|
|
@@ -2171,30 +3084,20 @@ var DriveSocket = class {
|
|
|
2171
3084
|
});
|
|
2172
3085
|
return;
|
|
2173
3086
|
}
|
|
2174
|
-
|
|
2175
|
-
if (
|
|
2176
|
-
respondError(
|
|
2177
|
-
code: "INVALID_ARGUMENT",
|
|
2178
|
-
message: "tab_id must be a number when provided.",
|
|
2179
|
-
retryable: false
|
|
2180
|
-
});
|
|
3087
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3088
|
+
if (!tabTarget.ok) {
|
|
3089
|
+
respondError(tabTarget.error);
|
|
2181
3090
|
return;
|
|
2182
3091
|
}
|
|
2183
|
-
|
|
2184
|
-
tabId = await getDefaultTabId();
|
|
2185
|
-
}
|
|
3092
|
+
const tabId = tabTarget.tabId;
|
|
2186
3093
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2187
3094
|
if (error) {
|
|
2188
3095
|
respondError(error);
|
|
2189
3096
|
return;
|
|
2190
3097
|
}
|
|
2191
3098
|
try {
|
|
2192
|
-
await this.dispatchCdpKeyPress(
|
|
2193
|
-
|
|
2194
|
-
key,
|
|
2195
|
-
params.modifiers
|
|
2196
|
-
);
|
|
2197
|
-
respondOk({ ok: true });
|
|
3099
|
+
await this.dispatchCdpKeyPress(tabId, key, params.modifiers);
|
|
3100
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2198
3101
|
} catch (error2) {
|
|
2199
3102
|
const info = mapDebuggerErrorMessage(
|
|
2200
3103
|
error2 instanceof Error ? error2.message : "Keyboard dispatch failed."
|
|
@@ -2214,18 +3117,12 @@ var DriveSocket = class {
|
|
|
2214
3117
|
});
|
|
2215
3118
|
return;
|
|
2216
3119
|
}
|
|
2217
|
-
|
|
2218
|
-
if (
|
|
2219
|
-
respondError(
|
|
2220
|
-
code: "INVALID_ARGUMENT",
|
|
2221
|
-
message: "tab_id must be a number when provided.",
|
|
2222
|
-
retryable: false
|
|
2223
|
-
});
|
|
3120
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3121
|
+
if (!tabTarget.ok) {
|
|
3122
|
+
respondError(tabTarget.error);
|
|
2224
3123
|
return;
|
|
2225
3124
|
}
|
|
2226
|
-
|
|
2227
|
-
tabId = await getDefaultTabId();
|
|
2228
|
-
}
|
|
3125
|
+
const tabId = tabTarget.tabId;
|
|
2229
3126
|
const count = typeof params.repeat === "number" && Number.isFinite(params.repeat) ? Math.max(1, Math.min(50, Math.floor(params.repeat))) : 1;
|
|
2230
3127
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2231
3128
|
if (error) {
|
|
@@ -2234,13 +3131,9 @@ var DriveSocket = class {
|
|
|
2234
3131
|
}
|
|
2235
3132
|
try {
|
|
2236
3133
|
for (let i = 0; i < count; i += 1) {
|
|
2237
|
-
await this.dispatchCdpKeyPress(
|
|
2238
|
-
tabId,
|
|
2239
|
-
key,
|
|
2240
|
-
params.modifiers
|
|
2241
|
-
);
|
|
3134
|
+
await this.dispatchCdpKeyPress(tabId, key, params.modifiers);
|
|
2242
3135
|
}
|
|
2243
|
-
respondOk({ ok: true });
|
|
3136
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2244
3137
|
} catch (error2) {
|
|
2245
3138
|
const info = mapDebuggerErrorMessage(
|
|
2246
3139
|
error2 instanceof Error ? error2.message : "Keyboard dispatch failed."
|
|
@@ -2260,18 +3153,12 @@ var DriveSocket = class {
|
|
|
2260
3153
|
});
|
|
2261
3154
|
return;
|
|
2262
3155
|
}
|
|
2263
|
-
|
|
2264
|
-
if (
|
|
2265
|
-
respondError(
|
|
2266
|
-
code: "INVALID_ARGUMENT",
|
|
2267
|
-
message: "tab_id must be a number when provided.",
|
|
2268
|
-
retryable: false
|
|
2269
|
-
});
|
|
3156
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3157
|
+
if (!tabTarget.ok) {
|
|
3158
|
+
respondError(tabTarget.error);
|
|
2270
3159
|
return;
|
|
2271
3160
|
}
|
|
2272
|
-
|
|
2273
|
-
tabId = await getDefaultTabId();
|
|
2274
|
-
}
|
|
3161
|
+
const tabId = tabTarget.tabId;
|
|
2275
3162
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2276
3163
|
if (error) {
|
|
2277
3164
|
respondError(error);
|
|
@@ -2287,23 +3174,17 @@ var DriveSocket = class {
|
|
|
2287
3174
|
respondError(result.error);
|
|
2288
3175
|
return;
|
|
2289
3176
|
}
|
|
2290
|
-
respondOk({ ok: true });
|
|
3177
|
+
respondOk(await withResolvedTabTarget(tabId, { ok: true }));
|
|
2291
3178
|
return;
|
|
2292
3179
|
}
|
|
2293
3180
|
case "drive.select": {
|
|
2294
3181
|
const params = message.params ?? {};
|
|
2295
|
-
|
|
2296
|
-
if (
|
|
2297
|
-
respondError(
|
|
2298
|
-
code: "INVALID_ARGUMENT",
|
|
2299
|
-
message: "tab_id must be a number when provided.",
|
|
2300
|
-
retryable: false
|
|
2301
|
-
});
|
|
3182
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3183
|
+
if (!tabTarget.ok) {
|
|
3184
|
+
respondError(tabTarget.error);
|
|
2302
3185
|
return;
|
|
2303
3186
|
}
|
|
2304
|
-
|
|
2305
|
-
tabId = await getDefaultTabId();
|
|
2306
|
-
}
|
|
3187
|
+
const tabId = tabTarget.tabId;
|
|
2307
3188
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2308
3189
|
if (error) {
|
|
2309
3190
|
respondError(error);
|
|
@@ -2331,16 +3212,17 @@ var DriveSocket = class {
|
|
|
2331
3212
|
respondError(info);
|
|
2332
3213
|
return;
|
|
2333
3214
|
}
|
|
2334
|
-
const selectResult = await sendToTab(
|
|
2335
|
-
tabId,
|
|
2336
|
-
"drive.select",
|
|
2337
|
-
params
|
|
2338
|
-
);
|
|
3215
|
+
const selectResult = await sendToTab(tabId, "drive.select", params);
|
|
2339
3216
|
if (!selectResult.ok) {
|
|
2340
3217
|
respondError(selectResult.error);
|
|
2341
3218
|
return;
|
|
2342
3219
|
}
|
|
2343
|
-
respondOk(
|
|
3220
|
+
respondOk(
|
|
3221
|
+
await withResolvedTabTarget(
|
|
3222
|
+
tabId,
|
|
3223
|
+
selectResult.result ?? { ok: true }
|
|
3224
|
+
)
|
|
3225
|
+
);
|
|
2344
3226
|
return;
|
|
2345
3227
|
}
|
|
2346
3228
|
case "drive.fill_form": {
|
|
@@ -2354,18 +3236,12 @@ var DriveSocket = class {
|
|
|
2354
3236
|
});
|
|
2355
3237
|
return;
|
|
2356
3238
|
}
|
|
2357
|
-
|
|
2358
|
-
if (
|
|
2359
|
-
respondError(
|
|
2360
|
-
code: "INVALID_ARGUMENT",
|
|
2361
|
-
message: "tab_id must be a number when provided.",
|
|
2362
|
-
retryable: false
|
|
2363
|
-
});
|
|
3239
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3240
|
+
if (!tabTarget.ok) {
|
|
3241
|
+
respondError(tabTarget.error);
|
|
2364
3242
|
return;
|
|
2365
3243
|
}
|
|
2366
|
-
|
|
2367
|
-
tabId = await getDefaultTabId();
|
|
2368
|
-
}
|
|
3244
|
+
const tabId = tabTarget.tabId;
|
|
2369
3245
|
const error = await this.ensureDebuggerAttached(tabId);
|
|
2370
3246
|
if (error) {
|
|
2371
3247
|
respondError(error);
|
|
@@ -2426,13 +3302,9 @@ var DriveSocket = class {
|
|
|
2426
3302
|
filled += 1;
|
|
2427
3303
|
continue;
|
|
2428
3304
|
}
|
|
2429
|
-
const fallback = await sendToTab(
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
{
|
|
2433
|
-
fields: [field]
|
|
2434
|
-
}
|
|
2435
|
-
);
|
|
3305
|
+
const fallback = await sendToTab(tabId, "drive.fill_form", {
|
|
3306
|
+
fields: [field]
|
|
3307
|
+
});
|
|
2436
3308
|
if (!fallback.ok) {
|
|
2437
3309
|
errors.push(
|
|
2438
3310
|
`Field ${index} could not be filled: ${fallback.error.message}`
|
|
@@ -2451,39 +3323,32 @@ var DriveSocket = class {
|
|
|
2451
3323
|
}
|
|
2452
3324
|
errors.push(`Field ${index} could not be filled.`);
|
|
2453
3325
|
}
|
|
2454
|
-
respondOk(
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
3326
|
+
respondOk(
|
|
3327
|
+
await withResolvedTabTarget(tabId, {
|
|
3328
|
+
filled,
|
|
3329
|
+
attempted: fields.length,
|
|
3330
|
+
errors: errors.length > 0 ? errors : []
|
|
3331
|
+
})
|
|
3332
|
+
);
|
|
2459
3333
|
return;
|
|
2460
3334
|
}
|
|
2461
3335
|
case "drive.scroll":
|
|
2462
3336
|
case "drive.wait_for": {
|
|
2463
3337
|
const params = message.params ?? {};
|
|
2464
|
-
|
|
2465
|
-
if (
|
|
2466
|
-
respondError(
|
|
2467
|
-
code: "INVALID_ARGUMENT",
|
|
2468
|
-
message: "tab_id must be a number when provided.",
|
|
2469
|
-
retryable: false
|
|
2470
|
-
});
|
|
3338
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3339
|
+
if (!tabTarget.ok) {
|
|
3340
|
+
respondError(tabTarget.error);
|
|
2471
3341
|
return;
|
|
2472
3342
|
}
|
|
2473
|
-
|
|
2474
|
-
tabId = await getDefaultTabId();
|
|
2475
|
-
}
|
|
3343
|
+
const tabId = tabTarget.tabId;
|
|
2476
3344
|
const timeoutMs = message.action === "drive.wait_for" && typeof params.timeout_ms === "number" && Number.isFinite(params.timeout_ms) ? Math.max(1, Math.floor(params.timeout_ms) + 1e3) : void 0;
|
|
2477
|
-
const result = await sendToTab(
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
params,
|
|
2481
|
-
{
|
|
2482
|
-
timeoutMs
|
|
2483
|
-
}
|
|
2484
|
-
);
|
|
3345
|
+
const result = await sendToTab(tabId, message.action, params, {
|
|
3346
|
+
timeoutMs
|
|
3347
|
+
});
|
|
2485
3348
|
if (result.ok) {
|
|
2486
|
-
respondOk(
|
|
3349
|
+
respondOk(
|
|
3350
|
+
await withResolvedTabTarget(tabId, result.result ?? { ok: true })
|
|
3351
|
+
);
|
|
2487
3352
|
} else {
|
|
2488
3353
|
respondError(result.error);
|
|
2489
3354
|
}
|
|
@@ -2491,30 +3356,25 @@ var DriveSocket = class {
|
|
|
2491
3356
|
}
|
|
2492
3357
|
case "drive.screenshot": {
|
|
2493
3358
|
const params = message.params ?? {};
|
|
2494
|
-
|
|
2495
|
-
if (
|
|
2496
|
-
respondError(
|
|
2497
|
-
code: "INVALID_ARGUMENT",
|
|
2498
|
-
message: "tab_id must be a number when provided.",
|
|
2499
|
-
retryable: false
|
|
2500
|
-
});
|
|
3359
|
+
const tabTarget = await resolveActionTabId(params);
|
|
3360
|
+
if (!tabTarget.ok) {
|
|
3361
|
+
respondError(tabTarget.error);
|
|
2501
3362
|
return;
|
|
2502
3363
|
}
|
|
2503
|
-
|
|
2504
|
-
tabId = await getDefaultTabId();
|
|
2505
|
-
}
|
|
3364
|
+
const tabId = tabTarget.tabId;
|
|
2506
3365
|
const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
|
|
2507
3366
|
const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
|
|
2508
3367
|
const quality = typeof params.quality === "number" && Number.isFinite(params.quality) ? Math.max(0, Math.min(100, Math.floor(params.quality))) : void 0;
|
|
2509
3368
|
const tab = await getTab(tabId);
|
|
2510
3369
|
const url = tab.url;
|
|
2511
3370
|
if (typeof url === "string" && isRestrictedUrl(url)) {
|
|
2512
|
-
respondError(
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
3371
|
+
respondError(
|
|
3372
|
+
buildRestrictedUrlError({
|
|
3373
|
+
url,
|
|
3374
|
+
operation: "screenshot",
|
|
3375
|
+
action: "drive.screenshot"
|
|
3376
|
+
})
|
|
3377
|
+
);
|
|
2518
3378
|
return;
|
|
2519
3379
|
}
|
|
2520
3380
|
const windowId = tab.windowId;
|
|
@@ -2679,7 +3539,7 @@ var DriveSocket = class {
|
|
|
2679
3539
|
format,
|
|
2680
3540
|
quality
|
|
2681
3541
|
);
|
|
2682
|
-
respondOk(rendered);
|
|
3542
|
+
respondOk(await withResolvedTabTarget(tabId, rendered));
|
|
2683
3543
|
} catch (error) {
|
|
2684
3544
|
respondError(
|
|
2685
3545
|
mapScreenshotCaptureError(
|
|
@@ -2786,7 +3646,12 @@ var DriveSocket = class {
|
|
|
2786
3646
|
srcW2,
|
|
2787
3647
|
srcH2
|
|
2788
3648
|
);
|
|
2789
|
-
respondOk(
|
|
3649
|
+
respondOk(
|
|
3650
|
+
await withResolvedTabTarget(
|
|
3651
|
+
tabId,
|
|
3652
|
+
await canvasToResult(cropCanvas2)
|
|
3653
|
+
)
|
|
3654
|
+
);
|
|
2790
3655
|
} finally {
|
|
2791
3656
|
bitmap.close();
|
|
2792
3657
|
}
|
|
@@ -2816,7 +3681,12 @@ var DriveSocket = class {
|
|
|
2816
3681
|
srcW,
|
|
2817
3682
|
srcH
|
|
2818
3683
|
);
|
|
2819
|
-
respondOk(
|
|
3684
|
+
respondOk(
|
|
3685
|
+
await withResolvedTabTarget(
|
|
3686
|
+
tabId,
|
|
3687
|
+
await canvasToResult(cropCanvas)
|
|
3688
|
+
)
|
|
3689
|
+
);
|
|
2820
3690
|
} catch (error) {
|
|
2821
3691
|
respondError(
|
|
2822
3692
|
mapScreenshotCaptureError(
|
|
@@ -2835,7 +3705,12 @@ var DriveSocket = class {
|
|
|
2835
3705
|
try {
|
|
2836
3706
|
const metaInfo = await getMetaInfo();
|
|
2837
3707
|
const canvas = await captureFullPageCanvas(metaInfo);
|
|
2838
|
-
respondOk(
|
|
3708
|
+
respondOk(
|
|
3709
|
+
await withResolvedTabTarget(
|
|
3710
|
+
tabId,
|
|
3711
|
+
await canvasToResult(canvas)
|
|
3712
|
+
)
|
|
3713
|
+
);
|
|
2839
3714
|
} catch (error) {
|
|
2840
3715
|
respondError(
|
|
2841
3716
|
mapScreenshotCaptureError(
|
|
@@ -2973,7 +3848,23 @@ var DriveSocket = class {
|
|
|
2973
3848
|
}
|
|
2974
3849
|
};
|
|
2975
3850
|
}
|
|
2976
|
-
|
|
3851
|
+
const targetState = coercePopupTriggerState(record.target_state);
|
|
3852
|
+
return {
|
|
3853
|
+
ok: true,
|
|
3854
|
+
point: {
|
|
3855
|
+
x,
|
|
3856
|
+
y,
|
|
3857
|
+
...targetState ? { targetState } : {}
|
|
3858
|
+
}
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
async focusLocator(tabId, locator) {
|
|
3862
|
+
const focused = await sendToTab(tabId, "drive.focus_locator", {
|
|
3863
|
+
locator
|
|
3864
|
+
});
|
|
3865
|
+
if (!focused.ok) {
|
|
3866
|
+
throw new Error(focused.error.message);
|
|
3867
|
+
}
|
|
2977
3868
|
}
|
|
2978
3869
|
async performCdpType(tabId, options) {
|
|
2979
3870
|
const targetPoint = await sendToTab(tabId, "drive.type_target_point", {
|
|
@@ -3163,132 +4054,25 @@ var DriveSocket = class {
|
|
|
3163
4054
|
error: sanitizeDriveErrorInfo(error)
|
|
3164
4055
|
});
|
|
3165
4056
|
};
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
respondError(error);
|
|
3182
|
-
return;
|
|
3183
|
-
}
|
|
3184
|
-
respondAck({ ok: true });
|
|
3185
|
-
return;
|
|
3186
|
-
}
|
|
3187
|
-
case "debugger.detach": {
|
|
3188
|
-
const params = message.params ?? {};
|
|
3189
|
-
const tabId = params.tab_id;
|
|
3190
|
-
if (typeof tabId !== "number") {
|
|
3191
|
-
respondError({
|
|
3192
|
-
code: "INVALID_ARGUMENT",
|
|
3193
|
-
message: "tab_id must be a number.",
|
|
3194
|
-
retryable: false
|
|
3195
|
-
});
|
|
3196
|
-
return;
|
|
3197
|
-
}
|
|
3198
|
-
const error = await this.detachDebugger(tabId);
|
|
3199
|
-
if (error) {
|
|
3200
|
-
respondError(error);
|
|
3201
|
-
return;
|
|
3202
|
-
}
|
|
3203
|
-
respondAck({ ok: true });
|
|
3204
|
-
return;
|
|
3205
|
-
}
|
|
3206
|
-
case "debugger.command": {
|
|
3207
|
-
const params = message.params ?? {};
|
|
3208
|
-
const tabId = params.tab_id;
|
|
3209
|
-
if (typeof tabId !== "number") {
|
|
3210
|
-
respondError({
|
|
3211
|
-
code: "INVALID_ARGUMENT",
|
|
3212
|
-
message: "tab_id must be a number.",
|
|
3213
|
-
retryable: false
|
|
3214
|
-
});
|
|
3215
|
-
return;
|
|
3216
|
-
}
|
|
3217
|
-
if (typeof params.method !== "string" || params.method.length === 0) {
|
|
3218
|
-
respondError({
|
|
3219
|
-
code: "INVALID_ARGUMENT",
|
|
3220
|
-
message: "method must be a non-empty string.",
|
|
3221
|
-
retryable: false
|
|
3222
|
-
});
|
|
3223
|
-
return;
|
|
3224
|
-
}
|
|
3225
|
-
const session = this.debuggerSessions.get(tabId);
|
|
3226
|
-
if (session?.attachPromise) {
|
|
3227
|
-
try {
|
|
3228
|
-
await session.attachPromise;
|
|
3229
|
-
} catch (error) {
|
|
3230
|
-
const info = mapDebuggerErrorMessage(
|
|
3231
|
-
error instanceof Error ? error.message : "Debugger attach failed."
|
|
3232
|
-
);
|
|
3233
|
-
this.clearDebuggerSession(tabId);
|
|
3234
|
-
respondError(info);
|
|
3235
|
-
return;
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
const attachedSession = this.debuggerSessions.get(tabId);
|
|
3239
|
-
if (!attachedSession?.attached) {
|
|
3240
|
-
respondError({
|
|
3241
|
-
code: "FAILED_PRECONDITION",
|
|
3242
|
-
message: "Debugger is not attached to the requested tab.",
|
|
3243
|
-
retryable: false
|
|
3244
|
-
});
|
|
3245
|
-
return;
|
|
3246
|
-
}
|
|
3247
|
-
try {
|
|
3248
|
-
const result = await this.sendDebuggerCommand(
|
|
3249
|
-
tabId,
|
|
3250
|
-
params.method,
|
|
3251
|
-
params.params,
|
|
3252
|
-
DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
3253
|
-
);
|
|
3254
|
-
this.touchDebuggerSession(tabId);
|
|
3255
|
-
respondAck(result);
|
|
3256
|
-
} catch (error) {
|
|
3257
|
-
if (error instanceof DebuggerTimeoutError) {
|
|
3258
|
-
respondError({
|
|
3259
|
-
code: "TIMEOUT",
|
|
3260
|
-
message: error.message,
|
|
3261
|
-
retryable: true
|
|
3262
|
-
});
|
|
3263
|
-
return;
|
|
3264
|
-
}
|
|
3265
|
-
const info = mapDebuggerErrorMessage(
|
|
3266
|
-
error instanceof Error ? error.message : "Debugger command failed."
|
|
3267
|
-
);
|
|
3268
|
-
respondError(info);
|
|
3269
|
-
}
|
|
3270
|
-
return;
|
|
3271
|
-
}
|
|
3272
|
-
default:
|
|
3273
|
-
respondError({
|
|
3274
|
-
code: "NOT_IMPLEMENTED",
|
|
3275
|
-
message: `${message.action} not implemented in extension yet.`,
|
|
3276
|
-
retryable: false
|
|
3277
|
-
});
|
|
4057
|
+
await dispatchDebuggerRequest(
|
|
4058
|
+
message,
|
|
4059
|
+
{
|
|
4060
|
+
getSession: (tabId) => this.debuggerSessions.get(tabId),
|
|
4061
|
+
ensureDebuggerAttached: async (tabId) => await this.ensureDebuggerAttached(tabId),
|
|
4062
|
+
detachDebugger: async (tabId) => await this.detachDebugger(tabId),
|
|
4063
|
+
sendDebuggerCommand: async (tabId, method, params, timeoutMs) => await this.sendDebuggerCommand(tabId, method, params, timeoutMs),
|
|
4064
|
+
touchDebuggerSession: (tabId) => this.touchDebuggerSession(tabId),
|
|
4065
|
+
clearDebuggerSession: (tabId) => this.clearDebuggerSession(tabId),
|
|
4066
|
+
mapDebuggerErrorMessage,
|
|
4067
|
+
debuggerCommandTimeoutMs: DEFAULT_DEBUGGER_COMMAND_TIMEOUT_MS
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
respondAck,
|
|
4071
|
+
respondError
|
|
3278
4072
|
}
|
|
3279
|
-
|
|
3280
|
-
const messageText = error instanceof Error ? error.message : "Unexpected debugger error.";
|
|
3281
|
-
respondError({
|
|
3282
|
-
code: "INSPECT_UNAVAILABLE",
|
|
3283
|
-
message: messageText,
|
|
3284
|
-
retryable: false
|
|
3285
|
-
});
|
|
3286
|
-
}
|
|
4073
|
+
);
|
|
3287
4074
|
}
|
|
3288
4075
|
async handleDebuggerEvent(source, method, params) {
|
|
3289
|
-
if (!await readDebuggerCapabilityEnabled()) {
|
|
3290
|
-
return;
|
|
3291
|
-
}
|
|
3292
4076
|
const tabId = source.tabId;
|
|
3293
4077
|
if (typeof tabId !== "number") {
|
|
3294
4078
|
return;
|
|
@@ -3307,9 +4091,6 @@ var DriveSocket = class {
|
|
|
3307
4091
|
return;
|
|
3308
4092
|
}
|
|
3309
4093
|
this.clearDebuggerSession(tabId);
|
|
3310
|
-
if (!await readDebuggerCapabilityEnabled()) {
|
|
3311
|
-
return;
|
|
3312
|
-
}
|
|
3313
4094
|
this.sendDebuggerEvent({
|
|
3314
4095
|
tab_id: tabId,
|
|
3315
4096
|
method: "Debugger.detached",
|
|
@@ -3402,12 +4183,11 @@ var DriveSocket = class {
|
|
|
3402
4183
|
const tab = await getTab(tabId);
|
|
3403
4184
|
const url = typeof tab.url === "string" ? tab.url : void 0;
|
|
3404
4185
|
if (isRestrictedUrl(url)) {
|
|
3405
|
-
return {
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
};
|
|
4186
|
+
return buildRestrictedUrlError({
|
|
4187
|
+
url: url ?? "about:blank",
|
|
4188
|
+
operation: "debugger",
|
|
4189
|
+
action: "debugger.attach"
|
|
4190
|
+
});
|
|
3411
4191
|
}
|
|
3412
4192
|
} catch (error) {
|
|
3413
4193
|
return mapDebuggerErrorMessage(
|
|
@@ -3536,11 +4316,25 @@ var DebuggerTimeoutError = class extends Error {
|
|
|
3536
4316
|
};
|
|
3537
4317
|
var socket = new DriveSocket();
|
|
3538
4318
|
var permissionPrompts = new PermissionPromptController();
|
|
4319
|
+
var permissionsRequests = new PermissionsRequestController();
|
|
3539
4320
|
chrome.runtime.onConnect.addListener((port) => {
|
|
3540
4321
|
permissionPrompts.handleConnect(port);
|
|
4322
|
+
permissionsRequests.handleConnect(port);
|
|
3541
4323
|
});
|
|
3542
4324
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
3543
4325
|
permissionPrompts.handleWindowRemoved(windowId);
|
|
4326
|
+
permissionsRequests.handleWindowRemoved(windowId);
|
|
4327
|
+
});
|
|
4328
|
+
chrome.windows.onFocusChanged.addListener((windowId) => {
|
|
4329
|
+
if (windowId === chrome.windows.WINDOW_ID_NONE) {
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
void queryActiveTabIdInWindow(windowId).then((tabId) => {
|
|
4333
|
+
markTabActive(tabId);
|
|
4334
|
+
socket.sendTabReport();
|
|
4335
|
+
}).catch(() => {
|
|
4336
|
+
socket.sendTabReport();
|
|
4337
|
+
});
|
|
3544
4338
|
});
|
|
3545
4339
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
3546
4340
|
markTabActive(activeInfo.tabId);
|
|
@@ -3615,26 +4409,23 @@ chrome.storage.onChanged.addListener(
|
|
|
3615
4409
|
if (areaName !== "local") {
|
|
3616
4410
|
return;
|
|
3617
4411
|
}
|
|
3618
|
-
const
|
|
3619
|
-
if (!
|
|
4412
|
+
const corePortChange = changes[LEGACY_CORE_PORT_KEY];
|
|
4413
|
+
if (!corePortChange) {
|
|
3620
4414
|
return;
|
|
3621
4415
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
error
|
|
3627
|
-
);
|
|
4416
|
+
let work = Promise.resolve();
|
|
4417
|
+
if (corePortChange) {
|
|
4418
|
+
work = work.then(async () => {
|
|
4419
|
+
await clearLegacyCorePort(chrome.storage.local);
|
|
3628
4420
|
});
|
|
3629
|
-
return;
|
|
3630
4421
|
}
|
|
3631
|
-
void
|
|
3632
|
-
console.error(
|
|
3633
|
-
"DriveSocket refreshDebuggerCapabilityState failed:",
|
|
3634
|
-
error
|
|
3635
|
-
);
|
|
4422
|
+
void work.catch((error) => {
|
|
4423
|
+
console.error("DriveSocket storage change handling failed:", error);
|
|
3636
4424
|
});
|
|
3637
4425
|
}
|
|
3638
4426
|
);
|
|
4427
|
+
void clearLegacyCorePort(chrome.storage.local).catch((error) => {
|
|
4428
|
+
console.warn("Failed to clear legacy corePort storage.", error);
|
|
4429
|
+
});
|
|
3639
4430
|
socket.start();
|
|
3640
4431
|
//# sourceMappingURL=background.js.map
|