mcp-browser-extension 0.9.35 → 0.9.38
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/README.md +33 -15
- package/dist/shared/client-auth.d.ts +2 -0
- package/dist/shared/phone-pairing.d.ts +35 -0
- package/dist/shared/phone-pairing.js +10 -0
- package/dist/shared/policy.js +7 -0
- package/dist/shared/protocol.d.ts +13 -2
- package/dist/shared/protocol.js +13 -1
- package/dist/shared/site-risk.d.ts +31 -0
- package/dist/shared/site-risk.js +190 -0
- package/dist/src/bridge/client-identity.d.ts +11 -5
- package/dist/src/bridge/client-identity.js +7 -4
- package/dist/src/bridge/connection.js +1 -0
- package/dist/src/bridge/server.d.ts +1 -0
- package/dist/src/bridge/server.js +38 -19
- package/dist/src/bridge/workspace.d.ts +1 -0
- package/dist/src/bridge/workspace.js +11 -3
- package/dist/src/cli.js +61 -18
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +26 -1
- package/dist/src/executor/extension-executor.d.ts +1 -0
- package/dist/src/executor/extension-executor.js +10 -1
- package/dist/src/executor/types.d.ts +3 -1
- package/dist/src/mcp/helpers.js +18 -6
- package/dist/src/mcp/person-wait.js +2 -1
- package/dist/src/mcp/server.d.ts +2 -1
- package/dist/src/mcp/server.js +4 -4
- package/dist/src/mcp/tools.d.ts +2 -2
- package/dist/src/mcp/tools.js +46 -9
- package/dist/src/native-host.js +11 -1
- package/dist/src/remote/claude-chat.d.ts +21 -0
- package/dist/src/remote/claude-chat.js +209 -0
- package/dist/src/remote/codex-rpc.d.ts +19 -0
- package/dist/src/remote/codex-rpc.js +94 -0
- package/dist/src/remote/http.d.ts +5 -0
- package/dist/src/remote/http.js +99 -7
- package/dist/src/remote/key.d.ts +4 -0
- package/dist/src/remote/key.js +6 -2
- package/dist/src/remote/phone-chat.d.ts +57 -0
- package/dist/src/remote/phone-chat.js +366 -0
- package/dist/src/remote/phone-control.d.ts +6 -0
- package/dist/src/remote/phone-control.js +132 -0
- package/dist/src/remote/phone-pairing.d.ts +41 -0
- package/dist/src/remote/phone-pairing.js +180 -0
- package/dist/src/remote/relay.d.ts +37 -0
- package/dist/src/remote/relay.js +207 -0
- package/docs/CLIENT-APPROVAL.md +44 -9
- package/docs/PHONE-PAIRING.md +97 -0
- package/extension-dist/THIRD-PARTY-NOTICES.txt +35 -0
- package/extension-dist/activity-dashboard.css +4 -9
- package/extension-dist/background.js +725 -127
- package/extension-dist/brand.css +63 -0
- package/extension-dist/manifest.json +1 -1
- package/extension-dist/options.html +25 -22
- package/extension-dist/options.js +179 -42
- package/extension-dist/popup.html +33 -41
- package/extension-dist/popup.js +2529 -126
- package/extension-dist/site-protection.css +11 -0
- package/package.json +13 -2
|
@@ -38,10 +38,16 @@
|
|
|
38
38
|
|
|
39
39
|
// shared/protocol.ts
|
|
40
40
|
var PROTOCOL_VERSION = 1;
|
|
41
|
+
var MAX_BRIDGE_FRAME_BYTES = 4 * 1024 * 1024;
|
|
41
42
|
var CLOSE_SUPERSEDED = 4e3;
|
|
42
43
|
var WIRE_CAP_TAB_URL = "tab-url";
|
|
44
|
+
var WIRE_CAP_SITE_CHECKS = "site-checks-v1";
|
|
43
45
|
var WIRE_CAP_FILL_FORM = "fill-form";
|
|
44
46
|
var WIRE_CAP_PROXY = "proxy";
|
|
47
|
+
function drawTimeoutMs(points, strokes) {
|
|
48
|
+
const events = Math.max(0, points) + 2 * Math.max(0, strokes);
|
|
49
|
+
return Math.min(24e4, Math.max(3e4, 1e4 + events * 60));
|
|
50
|
+
}
|
|
45
51
|
var WIRE_METHODS = [
|
|
46
52
|
"tabs_list",
|
|
47
53
|
"tab_select",
|
|
@@ -73,7 +79,8 @@
|
|
|
73
79
|
"print_pdf",
|
|
74
80
|
"fill_form",
|
|
75
81
|
"proxy_status",
|
|
76
|
-
"ping_probe"
|
|
82
|
+
"ping_probe",
|
|
83
|
+
"site_check"
|
|
77
84
|
];
|
|
78
85
|
|
|
79
86
|
// extension/src/sw/cmd-error.ts
|
|
@@ -85,6 +92,11 @@
|
|
|
85
92
|
code;
|
|
86
93
|
};
|
|
87
94
|
|
|
95
|
+
// shared/phone-pairing.ts
|
|
96
|
+
var PHONE_INVITATION_MS = 2 * 6e4;
|
|
97
|
+
var PHONE_ACCESS_MS = 30 * 24 * 60 * 6e4;
|
|
98
|
+
var PHONE_ACTIONS = ["state", "create", "cancel", "approve", "deny", "remove"];
|
|
99
|
+
|
|
88
100
|
// extension/src/sw/client-approvals.ts
|
|
89
101
|
var MAX_CLIENTS = 64;
|
|
90
102
|
var MAX_NONCES = 4096;
|
|
@@ -223,6 +235,33 @@
|
|
|
223
235
|
}
|
|
224
236
|
});
|
|
225
237
|
}
|
|
238
|
+
/** A human approved a scanned phone in our own popup. Validate its local helper identity. */
|
|
239
|
+
approvePairedPhone(phone) {
|
|
240
|
+
return this.serial(async () => {
|
|
241
|
+
await this.load();
|
|
242
|
+
if (phone.state !== "pending" || phone.expiresAt <= this.now() || phone.name !== clientName(phone.name)) {
|
|
243
|
+
return denied("This phone request expired. Generate a new QR code.");
|
|
244
|
+
}
|
|
245
|
+
const der = decode(phone.publicKey);
|
|
246
|
+
await crypto.subtle.importKey("spki", der, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
247
|
+
const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", der)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
248
|
+
if (id !== phone.clientId) return denied("Phone identity is invalid.");
|
|
249
|
+
const clients = this.clients.filter((c) => c.id !== id && c.expiresAt > this.now());
|
|
250
|
+
if (clients.length >= MAX_CLIENTS) return denied("Remove an unused app connection before pairing a phone.");
|
|
251
|
+
await this.save([...clients, {
|
|
252
|
+
id,
|
|
253
|
+
publicKey: phone.publicKey,
|
|
254
|
+
name: phone.name,
|
|
255
|
+
kind: "phone",
|
|
256
|
+
phoneId: phone.id,
|
|
257
|
+
state: "approved",
|
|
258
|
+
requestedAt: this.now(),
|
|
259
|
+
expiresAt: this.now() + PHONE_ACCESS_MS
|
|
260
|
+
}]);
|
|
261
|
+
this.storageFault = false;
|
|
262
|
+
this.revoking.delete(id);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
226
265
|
/** Called exclusively by messages from the extension's own approval UI. */
|
|
227
266
|
decide(id, allow) {
|
|
228
267
|
if (!allow) this.revoking.add(id);
|
|
@@ -253,6 +292,99 @@
|
|
|
253
292
|
return sender.id === chrome.runtime.id && [chrome.runtime.getURL("options.html"), chrome.runtime.getURL("popup.html")].includes(sender.url ?? "") && (sender.frameId === void 0 || sender.frameId === 0);
|
|
254
293
|
}
|
|
255
294
|
|
|
295
|
+
// extension/src/connect.ts
|
|
296
|
+
var NATIVE_HOST = "com.mehmoodqureshi.mcp_browser_extension";
|
|
297
|
+
async function fetchPairing() {
|
|
298
|
+
if (typeof chrome.runtime.sendNativeMessage !== "function") {
|
|
299
|
+
return { ok: false, error: "restart_needed", message: "Finishing setup. Click Connect again if this stays." };
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const reply = await chrome.runtime.sendNativeMessage(NATIVE_HOST, { type: "pairing" });
|
|
303
|
+
return reply ?? { ok: false, message: "The helper gave no answer." };
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
306
|
+
if (/not found|not registered|Specified native messaging host/i.test(why)) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: "no_helper",
|
|
310
|
+
message: "Add the MCP Browser Extension server to your AI client (Claude Code, Cursor\u2026) and start it once; it sets this up. Then click Connect again."
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { ok: false, message: why };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function applyPairing(reply) {
|
|
317
|
+
await chrome.storage.local.set({
|
|
318
|
+
wsPort: reply.port,
|
|
319
|
+
token: reply.token,
|
|
320
|
+
// The server's own --profile, as a suggestion only: another browser may
|
|
321
|
+
// already use that name, and taking it over would disconnect that one.
|
|
322
|
+
suggestedProfile: reply.profile ?? "",
|
|
323
|
+
pairingSource: "native"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
async function requestPairingFromClick() {
|
|
327
|
+
let granted = false;
|
|
328
|
+
try {
|
|
329
|
+
granted = await chrome.permissions.request({ permissions: ["nativeMessaging"] });
|
|
330
|
+
} catch (err) {
|
|
331
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
332
|
+
}
|
|
333
|
+
if (!granted) return { ok: false, message: "Chrome needs that permission to fetch the pairing for you. You can also pair by hand in Settings." };
|
|
334
|
+
return fetchPairing();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// extension/src/sw/phone-pairing.ts
|
|
338
|
+
async function phonePairingAction(action, id, deps) {
|
|
339
|
+
if (!PHONE_ACTIONS.includes(action)) return { ok: false, message: "Invalid phone pairing action." };
|
|
340
|
+
if (["create", "approve"].includes(action) && (deps.stopped() || !deps.connected())) {
|
|
341
|
+
return { ok: false, message: "Connect this browser and resume access before pairing a phone." };
|
|
342
|
+
}
|
|
343
|
+
const { pairedProfile } = await chrome.storage.local.get("pairedProfile");
|
|
344
|
+
if (typeof pairedProfile !== "string" || !pairedProfile) return { ok: false, message: "Connect this browser first, then pair your phone." };
|
|
345
|
+
if (typeof chrome.runtime.sendNativeMessage !== "function") return { ok: false, message: "Click Connect in the extension, then reopen Pair phone." };
|
|
346
|
+
const send = async (action2) => {
|
|
347
|
+
const reply = await chrome.runtime.sendNativeMessage(NATIVE_HOST, { type: "phone_pairing", profile: pairedProfile, action: action2, id });
|
|
348
|
+
if (reply?.ok) {
|
|
349
|
+
const stored = (await chrome.storage.local.get(CLIENT_STORAGE_KEY))[CLIENT_STORAGE_KEY];
|
|
350
|
+
const clients = Array.isArray(stored) ? stored : [];
|
|
351
|
+
for (const phone of reply.state.devices) {
|
|
352
|
+
if (phone.state === "paired") phone.browserApproved = clients.some((c) => c.id === phone.clientId && c.state === "approved" && c.expiresAt > Date.now());
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return reply;
|
|
356
|
+
};
|
|
357
|
+
try {
|
|
358
|
+
if (action === "approve" || action === "remove") {
|
|
359
|
+
const current = await send("state");
|
|
360
|
+
if (!current?.ok) return current ?? { ok: false, message: "Phone pairing did not answer." };
|
|
361
|
+
const phone = current.state.devices.find((d) => d.id === id);
|
|
362
|
+
if (!phone) return { ok: false, message: "That phone request has expired or was removed." };
|
|
363
|
+
if (action === "approve") {
|
|
364
|
+
if (deps.stopped() || !deps.connected()) return { ok: false, message: "Resume browser access before approving this phone." };
|
|
365
|
+
await deps.approvals.approvePairedPhone(phone);
|
|
366
|
+
if (deps.stopped() || !deps.connected()) {
|
|
367
|
+
await deps.approvals.decide(phone.clientId, false);
|
|
368
|
+
return { ok: false, message: "Browser access stopped. Pair your phone again after resuming." };
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const result = await send("approve");
|
|
372
|
+
if (!result?.ok) await deps.approvals.decide(phone.clientId, false);
|
|
373
|
+
return result ?? { ok: false, message: "Could not approve this phone." };
|
|
374
|
+
} catch (err) {
|
|
375
|
+
await deps.approvals.decide(phone.clientId, false).catch(() => void 0);
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
await deps.approvals.decide(phone.clientId, false).catch(() => void 0);
|
|
380
|
+
return await send("remove");
|
|
381
|
+
}
|
|
382
|
+
return await send(action);
|
|
383
|
+
} catch {
|
|
384
|
+
return { ok: false, message: "Could not reach phone pairing. Update the local server and try again." };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
256
388
|
// shared/activity.ts
|
|
257
389
|
var ACCESS_STOPPED_KEY = "accessStopped";
|
|
258
390
|
var ACTIVITY_KEY = "recentActivity";
|
|
@@ -375,12 +507,12 @@
|
|
|
375
507
|
token() {
|
|
376
508
|
return this.generation;
|
|
377
509
|
}
|
|
378
|
-
record(entry,
|
|
510
|
+
record(entry, generation2 = this.generation) {
|
|
379
511
|
if (this.pending >= 256) return Promise.reject(new Error("Activity history is busy."));
|
|
380
512
|
this.pending++;
|
|
381
513
|
const safe = activityEntry(entry);
|
|
382
514
|
return this.serial(async () => {
|
|
383
|
-
if (
|
|
515
|
+
if (generation2 !== this.generation) return;
|
|
384
516
|
await this.load();
|
|
385
517
|
const next = this.entries.filter((e) => e.id !== safe.id);
|
|
386
518
|
next.push(safe);
|
|
@@ -453,7 +585,7 @@
|
|
|
453
585
|
// This build gates fail-closed and reports tab URLs on results, so the
|
|
454
586
|
// server may skip its pre-flight tabs_list. An older build omits this and
|
|
455
587
|
// the server keeps fetching the URL itself.
|
|
456
|
-
caps: [WIRE_CAP_TAB_URL, WIRE_CAP_FILL_FORM, WIRE_CAP_PROXY, CLIENT_APPROVAL_CAP]
|
|
588
|
+
caps: [WIRE_CAP_TAB_URL, WIRE_CAP_FILL_FORM, WIRE_CAP_PROXY, CLIENT_APPROVAL_CAP, WIRE_CAP_SITE_CHECKS]
|
|
457
589
|
};
|
|
458
590
|
ws2.send(JSON.stringify(hello));
|
|
459
591
|
};
|
|
@@ -467,6 +599,7 @@
|
|
|
467
599
|
}
|
|
468
600
|
switch (frame.type) {
|
|
469
601
|
case "welcome":
|
|
602
|
+
this.deps.onSiteChecks?.(frame.siteChecks === true);
|
|
470
603
|
this.deps.onPolicy(frame.policy);
|
|
471
604
|
if (typeof frame.profile === "string") this.deps.onProfile?.(frame.profile);
|
|
472
605
|
this.deps.onGranted?.(Array.isArray(frame.granted) ? frame.granted : null);
|
|
@@ -485,7 +618,17 @@
|
|
|
485
618
|
if (this.state === "connected" && frame.v === PROTOCOL_VERSION) {
|
|
486
619
|
this.deps.onCommand(frame, (result) => {
|
|
487
620
|
if (this.ws === ws2 && this.state === "connected" && ws2.readyState === WebSocket.OPEN) {
|
|
488
|
-
|
|
621
|
+
const payload = JSON.stringify(result);
|
|
622
|
+
if (new TextEncoder().encode(payload).byteLength > MAX_BRIDGE_FRAME_BYTES) {
|
|
623
|
+
const error = {
|
|
624
|
+
type: "error",
|
|
625
|
+
v: PROTOCOL_VERSION,
|
|
626
|
+
id: frame.id,
|
|
627
|
+
ok: false,
|
|
628
|
+
error: { code: "RESULT_TOO_LARGE", message: "[RESULT_TOO_LARGE] The browser result is too large to return. Request a smaller page section or a lower-resolution image." }
|
|
629
|
+
};
|
|
630
|
+
ws2.send(JSON.stringify(error));
|
|
631
|
+
} else ws2.send(payload);
|
|
489
632
|
}
|
|
490
633
|
});
|
|
491
634
|
}
|
|
@@ -610,6 +753,9 @@
|
|
|
610
753
|
function isDomainAllowed(url, policy) {
|
|
611
754
|
const host = hostOf(url);
|
|
612
755
|
if (!host) return false;
|
|
756
|
+
if (policy.automaticSites && /^https?:\/\//i.test(url)) {
|
|
757
|
+
return !(policy.blockedDomains ?? []).some((pat) => globMatches(host, pat));
|
|
758
|
+
}
|
|
613
759
|
return policy.allowDomains.some((pat) => globMatches(host, pat));
|
|
614
760
|
}
|
|
615
761
|
function evaluatePolicy(url, method, policy) {
|
|
@@ -641,6 +787,10 @@
|
|
|
641
787
|
if (isAboutBlank(url) && NAVIGATION.has(method)) return { ok: true };
|
|
642
788
|
if (!isDomainAllowed(url, policy)) {
|
|
643
789
|
const host = hostOf(url) || url;
|
|
790
|
+
if (policy.automaticSites && /^https?:\/\//i.test(url)) return {
|
|
791
|
+
ok: false,
|
|
792
|
+
reason: `Access to ${host} was removed. Restore it under Blocked websites in extension Settings.`
|
|
793
|
+
};
|
|
644
794
|
return { ok: false, reason: blockedDomainMessage(host) };
|
|
645
795
|
}
|
|
646
796
|
return { ok: true };
|
|
@@ -1533,11 +1683,354 @@
|
|
|
1533
1683
|
};
|
|
1534
1684
|
}
|
|
1535
1685
|
|
|
1686
|
+
// shared/site-risk.ts
|
|
1687
|
+
var SITE_CHECKS_KEY = "localSiteChecksV1";
|
|
1688
|
+
var SITE_CHECKS_PENDING_KEY = "localSiteWarnings";
|
|
1689
|
+
var SITE_CHECKS_APPROVALS_KEY = "localSiteWarningApprovals";
|
|
1690
|
+
var SITE_CHECKS_BLOCKED_KEY = "localBlockedSites";
|
|
1691
|
+
var AUTOMATIC_SITE_ORIGINS = ["http://*/*", "https://*/*"];
|
|
1692
|
+
var BRANDS = {
|
|
1693
|
+
google: ["google.com", "google.co.uk", "google.com.pk", "google.de", "google.fr", "google.co.in", "google.com.au", "google.ca", "google.co.jp", "youtube.com"],
|
|
1694
|
+
microsoft: ["microsoft.com", "microsoftonline.com", "live.com", "office.com", "outlook.com"],
|
|
1695
|
+
apple: ["apple.com", "icloud.com"],
|
|
1696
|
+
paypal: ["paypal.com"],
|
|
1697
|
+
amazon: ["amazon.com", "amazon.co.uk", "amazon.de", "amazon.fr", "amazon.in", "amazon.ca", "amazon.com.au", "amazon.co.jp", "amazon.es", "amazon.it"],
|
|
1698
|
+
github: ["github.com", "github.dev"],
|
|
1699
|
+
facebook: ["facebook.com", "meta.com"],
|
|
1700
|
+
instagram: ["instagram.com", "meta.com"],
|
|
1701
|
+
linkedin: ["linkedin.com"],
|
|
1702
|
+
dropbox: ["dropbox.com"],
|
|
1703
|
+
coinbase: ["coinbase.com"]
|
|
1704
|
+
};
|
|
1705
|
+
var AUTH_WORD = /(?:^|[.\-])(login|signin|sign-in|verify|verification|secure|account|password|recover|support)(?:[.\-]|$)/;
|
|
1706
|
+
function hostIn(host, domains) {
|
|
1707
|
+
return domains.some((domain) => host === domain || host.endsWith(`.${domain}`));
|
|
1708
|
+
}
|
|
1709
|
+
function unicodeLabel(label) {
|
|
1710
|
+
if (!label.startsWith("xn--") || label.length > 63) return label;
|
|
1711
|
+
try {
|
|
1712
|
+
const input = label.slice(4);
|
|
1713
|
+
const split = input.lastIndexOf("-");
|
|
1714
|
+
const points = split < 0 ? [] : Array.from(input.slice(0, split), (c) => c.codePointAt(0));
|
|
1715
|
+
let pos = split < 0 ? 0 : split + 1;
|
|
1716
|
+
let n = 128, i = 0, bias = 72;
|
|
1717
|
+
while (pos < input.length) {
|
|
1718
|
+
const old = i;
|
|
1719
|
+
let weight = 1;
|
|
1720
|
+
for (let k2 = 36; ; k2 += 36) {
|
|
1721
|
+
const code = input.charCodeAt(pos++);
|
|
1722
|
+
const digit = code >= 97 && code <= 122 ? code - 97 : code >= 48 && code <= 57 ? code - 22 : 36;
|
|
1723
|
+
if (digit >= 36 || !Number.isSafeInteger(i + digit * weight)) return label;
|
|
1724
|
+
i += digit * weight;
|
|
1725
|
+
const t = k2 <= bias ? 1 : k2 >= bias + 26 ? 26 : k2 - bias;
|
|
1726
|
+
if (digit < t) break;
|
|
1727
|
+
weight *= 36 - t;
|
|
1728
|
+
}
|
|
1729
|
+
const size = points.length + 1;
|
|
1730
|
+
let delta = Math.floor((i - old) / (old === 0 ? 700 : 2));
|
|
1731
|
+
delta += Math.floor(delta / size);
|
|
1732
|
+
let k = 0;
|
|
1733
|
+
while (delta > 455) {
|
|
1734
|
+
delta = Math.floor(delta / 35);
|
|
1735
|
+
k += 36;
|
|
1736
|
+
}
|
|
1737
|
+
bias = k + Math.floor(36 * delta / (delta + 38));
|
|
1738
|
+
n += Math.floor(i / size);
|
|
1739
|
+
i %= size;
|
|
1740
|
+
if (n > 1114111 || n >= 55296 && n <= 57343) return label;
|
|
1741
|
+
points.splice(i++, 0, n);
|
|
1742
|
+
}
|
|
1743
|
+
return String.fromCodePoint(...points);
|
|
1744
|
+
} catch {
|
|
1745
|
+
return label;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function skeleton(label) {
|
|
1749
|
+
const lookalikes = {
|
|
1750
|
+
"\u0430": "a",
|
|
1751
|
+
"\u0435": "e",
|
|
1752
|
+
"\u043E": "o",
|
|
1753
|
+
"\u0440": "p",
|
|
1754
|
+
"\u0441": "c",
|
|
1755
|
+
"\u0443": "y",
|
|
1756
|
+
"\u0445": "x",
|
|
1757
|
+
"\u0456": "i",
|
|
1758
|
+
"\u0458": "j",
|
|
1759
|
+
"\u04CF": "l",
|
|
1760
|
+
"\u0455": "s",
|
|
1761
|
+
"\u0501": "d",
|
|
1762
|
+
"\u051B": "q",
|
|
1763
|
+
"\u03B1": "a",
|
|
1764
|
+
"\u03BF": "o",
|
|
1765
|
+
"\u03C1": "p",
|
|
1766
|
+
"\u03BD": "v",
|
|
1767
|
+
"\u03B9": "i",
|
|
1768
|
+
"0": "o",
|
|
1769
|
+
"1": "l"
|
|
1770
|
+
};
|
|
1771
|
+
return Array.from(unicodeLabel(label).toLowerCase(), (c) => lookalikes[c] ?? c).join("");
|
|
1772
|
+
}
|
|
1773
|
+
function isLocalDevelopmentHost(host) {
|
|
1774
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
1775
|
+
}
|
|
1776
|
+
function siteUrlWarnings(input) {
|
|
1777
|
+
let url;
|
|
1778
|
+
try {
|
|
1779
|
+
url = new URL(input);
|
|
1780
|
+
} catch {
|
|
1781
|
+
return [];
|
|
1782
|
+
}
|
|
1783
|
+
if (!/^https?:$/.test(url.protocol)) return [];
|
|
1784
|
+
const host = url.hostname.toLowerCase().replace(/\.$/, "");
|
|
1785
|
+
const warnings = [];
|
|
1786
|
+
if (url.username && /[.]/.test(decodeURIComponentSafe(url.username))) {
|
|
1787
|
+
warnings.push({ id: "hidden-destination", reason: `The address puts a website name before @. The actual destination is ${host}.` });
|
|
1788
|
+
}
|
|
1789
|
+
const labels = host.split(".");
|
|
1790
|
+
for (const [brand, domains] of Object.entries(BRANDS)) {
|
|
1791
|
+
if (hostIn(host, domains)) continue;
|
|
1792
|
+
if (domains.some((domain) => host.includes(`${domain}.`))) {
|
|
1793
|
+
warnings.push({ id: `misleading-domain:${brand}`, reason: `The address includes ${brand}'s domain but actually belongs to ${host}.` });
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (labels.some((label) => label !== brand && skeleton(label) === brand)) {
|
|
1797
|
+
warnings.push({ id: `lookalike:${brand}`, reason: `The address uses lookalike characters to resemble ${brand}.` });
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (labels.some((label) => label.split("-").includes(brand)) && AUTH_WORD.test(host)) {
|
|
1801
|
+
warnings.push({ id: `brand-login:${brand}`, reason: `The address combines ${brand}'s name with login or account wording outside its recognised domains.` });
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return warnings;
|
|
1805
|
+
}
|
|
1806
|
+
function decodeURIComponentSafe(value) {
|
|
1807
|
+
try {
|
|
1808
|
+
return decodeURIComponent(value);
|
|
1809
|
+
} catch {
|
|
1810
|
+
return value;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function loginWarnings(signals) {
|
|
1814
|
+
const warnings = siteUrlWarnings(signals.url);
|
|
1815
|
+
if (!signals.passwords) return warnings;
|
|
1816
|
+
let page;
|
|
1817
|
+
try {
|
|
1818
|
+
page = new URL(signals.url);
|
|
1819
|
+
} catch {
|
|
1820
|
+
return warnings;
|
|
1821
|
+
}
|
|
1822
|
+
if (!/^https?:$/.test(page.protocol)) return warnings;
|
|
1823
|
+
const local = isLocalDevelopmentHost(page.hostname);
|
|
1824
|
+
if (page.protocol === "http:" && !local) warnings.push({
|
|
1825
|
+
id: "unencrypted-login",
|
|
1826
|
+
reason: "This page asks for a password over an unencrypted HTTP connection."
|
|
1827
|
+
});
|
|
1828
|
+
for (const form of signals.forms.slice(0, 32)) {
|
|
1829
|
+
let action;
|
|
1830
|
+
try {
|
|
1831
|
+
action = new URL(form.action || signals.url, signals.url);
|
|
1832
|
+
} catch {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
if (action.protocol === "http:" && !(local && isLocalDevelopmentHost(action.hostname))) warnings.push({
|
|
1836
|
+
id: `unencrypted-password-destination:${action.origin}`,
|
|
1837
|
+
reason: `A password form sends to an unencrypted address at ${action.hostname}.`
|
|
1838
|
+
});
|
|
1839
|
+
if (form.method === "get") warnings.push({
|
|
1840
|
+
id: "password-in-address",
|
|
1841
|
+
reason: "A password form is configured to put its fields in the website address."
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
const title = signals.title.slice(0, 300).toLowerCase();
|
|
1845
|
+
if (/\b(sign[ -]?in|log[ -]?in|verify|account|password)\b/.test(title)) {
|
|
1846
|
+
for (const [brand, domains] of Object.entries(BRANDS)) {
|
|
1847
|
+
if (new RegExp(`\\b${brand}\\b`).test(title) && !hostIn(page.hostname, domains)) {
|
|
1848
|
+
warnings.push({ id: `login-brand:${brand}`, reason: `This password page presents itself as ${brand}, but its address is ${page.hostname}.` });
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return uniqueWarnings(warnings);
|
|
1853
|
+
}
|
|
1854
|
+
function uniqueWarnings(warnings) {
|
|
1855
|
+
return [...new Map(warnings.map((warning) => [warning.id, warning])).values()];
|
|
1856
|
+
}
|
|
1857
|
+
function collectLoginSignals() {
|
|
1858
|
+
const inputs = Array.from(document.querySelectorAll('input[type="password"]')).filter((input) => !input.disabled && input.getClientRects().length > 0).slice(0, 32);
|
|
1859
|
+
const forms = [...new Set(inputs.map((input) => input.form).filter((form) => !!form))];
|
|
1860
|
+
return {
|
|
1861
|
+
url: location.href,
|
|
1862
|
+
title: document.title.slice(0, 300),
|
|
1863
|
+
passwords: inputs.length,
|
|
1864
|
+
forms: forms.map((form) => ({ action: form.action, method: form.getAttribute("method")?.toLowerCase() ?? "" }))
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// extension/src/sw/site-protection.ts
|
|
1869
|
+
var enabled = false;
|
|
1870
|
+
var configured = false;
|
|
1871
|
+
var supported = true;
|
|
1872
|
+
var requesting = false;
|
|
1873
|
+
var blocked = [];
|
|
1874
|
+
var generation = 0;
|
|
1875
|
+
var queue = Promise.resolve();
|
|
1876
|
+
var changed = () => {
|
|
1877
|
+
};
|
|
1878
|
+
function onSiteWarnings(callback) {
|
|
1879
|
+
changed = callback;
|
|
1880
|
+
}
|
|
1881
|
+
function setSiteChecksSupported(value) {
|
|
1882
|
+
supported = value;
|
|
1883
|
+
}
|
|
1884
|
+
function siteChecksEnabled() {
|
|
1885
|
+
return supported && enabled;
|
|
1886
|
+
}
|
|
1887
|
+
function siteChecksConfigured() {
|
|
1888
|
+
return supported && configured;
|
|
1889
|
+
}
|
|
1890
|
+
function requireSiteChecksReady() {
|
|
1891
|
+
if (siteChecksConfigured() && !enabled) throw new CmdError("POLICY_DENIED", "Automatic website access needs Chrome permission. Open MCP Browser Extension and click Allow website access.");
|
|
1892
|
+
}
|
|
1893
|
+
async function keepAutomaticPermissions() {
|
|
1894
|
+
return requesting || (await chrome.storage.local.get(SITE_CHECKS_KEY))[SITE_CHECKS_KEY] === true;
|
|
1895
|
+
}
|
|
1896
|
+
async function refreshSiteChecks() {
|
|
1897
|
+
const mine = ++generation;
|
|
1898
|
+
const saved = await chrome.storage.local.get([SITE_CHECKS_KEY, SITE_CHECKS_BLOCKED_KEY, SITE_CHECKS_PENDING_KEY]);
|
|
1899
|
+
const wantsChecks = saved[SITE_CHECKS_KEY] !== false;
|
|
1900
|
+
const permitted = wantsChecks && await chrome.permissions.contains({ origins: AUTOMATIC_SITE_ORIGINS });
|
|
1901
|
+
if (mine !== generation) return;
|
|
1902
|
+
configured = wantsChecks;
|
|
1903
|
+
enabled = permitted;
|
|
1904
|
+
blocked = Array.isArray(saved[SITE_CHECKS_BLOCKED_KEY]) ? saved[SITE_CHECKS_BLOCKED_KEY].filter((s) => typeof s === "string") : [];
|
|
1905
|
+
changed(siteChecksEnabled() && Array.isArray(saved[SITE_CHECKS_PENDING_KEY]) ? saved[SITE_CHECKS_PENDING_KEY].length : 0, false);
|
|
1906
|
+
}
|
|
1907
|
+
async function changeSiteChecks(allow) {
|
|
1908
|
+
if (requesting) throw new Error("A website permission request is already open.");
|
|
1909
|
+
requesting = allow;
|
|
1910
|
+
try {
|
|
1911
|
+
const permission = allow ? chrome.permissions.request({ origins: AUTOMATIC_SITE_ORIGINS }) : Promise.resolve(true);
|
|
1912
|
+
if (!await permission) throw new Error("Automatic website access was not enabled. Accept Chrome\u2019s permission request to turn it on.");
|
|
1913
|
+
await chrome.storage.local.set({ [SITE_CHECKS_KEY]: allow });
|
|
1914
|
+
if (!allow) {
|
|
1915
|
+
enabled = false;
|
|
1916
|
+
await chrome.permissions.remove({ origins: AUTOMATIC_SITE_ORIGINS });
|
|
1917
|
+
}
|
|
1918
|
+
await refreshSiteChecks();
|
|
1919
|
+
} finally {
|
|
1920
|
+
requesting = false;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function localSitePolicy(policy) {
|
|
1924
|
+
const active = siteChecksEnabled();
|
|
1925
|
+
return policy ? { ...policy, automaticSites: active, blockedDomains: active ? blocked : [] } : null;
|
|
1926
|
+
}
|
|
1927
|
+
function serialized(operation) {
|
|
1928
|
+
const next = queue.then(operation);
|
|
1929
|
+
queue = next.catch(() => {
|
|
1930
|
+
});
|
|
1931
|
+
return next;
|
|
1932
|
+
}
|
|
1933
|
+
async function warn(url, warnings) {
|
|
1934
|
+
return serialized(async () => {
|
|
1935
|
+
const saved = await chrome.storage.local.get([SITE_CHECKS_PENDING_KEY, SITE_CHECKS_APPROVALS_KEY]);
|
|
1936
|
+
const approved = saved[SITE_CHECKS_APPROVALS_KEY]?.[url.origin];
|
|
1937
|
+
warnings = warnings.filter((warning) => !Array.isArray(approved) || !approved.includes(warning.id));
|
|
1938
|
+
if (!warnings.length) return [];
|
|
1939
|
+
const prior = Array.isArray(saved[SITE_CHECKS_PENDING_KEY]) ? saved[SITE_CHECKS_PENDING_KEY] : [];
|
|
1940
|
+
const old = prior.find((entry) => entry.origin === url.origin);
|
|
1941
|
+
const fresh = !old || warnings.some((warning) => !old.warnings.some((w) => w.id === warning.id));
|
|
1942
|
+
const request = { origin: url.origin, host: url.hostname, warnings, at: Date.now() };
|
|
1943
|
+
const next = [request, ...prior.filter((entry) => entry.origin !== url.origin)].slice(0, 100);
|
|
1944
|
+
await chrome.storage.local.set({ [SITE_CHECKS_PENDING_KEY]: next });
|
|
1945
|
+
changed(next.length, fresh);
|
|
1946
|
+
return warnings;
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
async function requireWarningApproval(url, warnings) {
|
|
1950
|
+
if (!warnings.length) return;
|
|
1951
|
+
const missing = await warn(url, warnings);
|
|
1952
|
+
if (!missing.length) return;
|
|
1953
|
+
throw new CmdError("POLICY_DENIED", `Suspicious website ${url.hostname}: ${missing.map((w) => w.reason).join(" ")} Review the warning in the MCP Browser Extension popup or Settings, then click Allow if you want to continue.`);
|
|
1954
|
+
}
|
|
1955
|
+
async function checkLocalSite(input, tabId, frameId = 0) {
|
|
1956
|
+
requireSiteChecksReady();
|
|
1957
|
+
if (!siteChecksEnabled() || input === "about:blank") return;
|
|
1958
|
+
let url;
|
|
1959
|
+
try {
|
|
1960
|
+
url = new URL(input);
|
|
1961
|
+
} catch {
|
|
1962
|
+
throw new CmdError("POLICY_DENIED", "This website address cannot be checked.");
|
|
1963
|
+
}
|
|
1964
|
+
if (!/^https?:$/.test(url.protocol)) throw new CmdError("POLICY_DENIED", "Automatic website access only covers HTTP and HTTPS pages.");
|
|
1965
|
+
if (blocked.some((host) => url.hostname === host || host.startsWith("*.") && (url.hostname === host.slice(2) || url.hostname.endsWith(`.${host.slice(2)}`)))) {
|
|
1966
|
+
throw new CmdError("POLICY_DENIED", `Access to ${url.hostname} was removed. Restore it under Blocked websites in extension Settings.`);
|
|
1967
|
+
}
|
|
1968
|
+
await requireWarningApproval(url, siteUrlWarnings(input));
|
|
1969
|
+
if (tabId !== void 0) {
|
|
1970
|
+
let warnings;
|
|
1971
|
+
try {
|
|
1972
|
+
const results = await chrome.scripting.executeScript({ target: { tabId, frameIds: [frameId] }, func: collectLoginSignals });
|
|
1973
|
+
const signals = results[0]?.result;
|
|
1974
|
+
if (!signals || signals.url !== input) throw new Error("The page changed during the check.");
|
|
1975
|
+
warnings = loginWarnings(signals);
|
|
1976
|
+
} catch {
|
|
1977
|
+
throw new CmdError("CDP_ERROR", "Could not finish the local website check. The page may have changed; retry the request.");
|
|
1978
|
+
}
|
|
1979
|
+
await requireWarningApproval(url, warnings);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
function allowSiteWarning(origin, expected) {
|
|
1983
|
+
return serialized(async () => {
|
|
1984
|
+
const saved = await chrome.storage.local.get([SITE_CHECKS_PENDING_KEY, SITE_CHECKS_APPROVALS_KEY]);
|
|
1985
|
+
const pending = Array.isArray(saved[SITE_CHECKS_PENDING_KEY]) ? saved[SITE_CHECKS_PENDING_KEY] : [];
|
|
1986
|
+
const request = pending.find((entry) => entry.origin === origin);
|
|
1987
|
+
if (!request || request.warnings.some((w) => !expected.includes(w.id)) || expected.some((id) => !request.warnings.some((w) => w.id === id))) {
|
|
1988
|
+
throw new Error("The warning changed. Review the current reasons and try again.");
|
|
1989
|
+
}
|
|
1990
|
+
const approvals2 = saved[SITE_CHECKS_APPROVALS_KEY] ?? {};
|
|
1991
|
+
const previous = Array.isArray(approvals2[origin]) ? approvals2[origin] : [];
|
|
1992
|
+
const next = pending.filter((entry) => entry.origin !== origin);
|
|
1993
|
+
await chrome.storage.local.set({
|
|
1994
|
+
[SITE_CHECKS_APPROVALS_KEY]: { ...approvals2, [origin]: [.../* @__PURE__ */ new Set([...previous, ...expected])] },
|
|
1995
|
+
[SITE_CHECKS_PENDING_KEY]: next
|
|
1996
|
+
});
|
|
1997
|
+
changed(next.length, false);
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
function blockLocalSite(host, block) {
|
|
2001
|
+
return serialized(async () => {
|
|
2002
|
+
const saved = await chrome.storage.local.get([SITE_CHECKS_BLOCKED_KEY, SITE_CHECKS_APPROVALS_KEY, SITE_CHECKS_PENDING_KEY]);
|
|
2003
|
+
const list = Array.isArray(saved[SITE_CHECKS_BLOCKED_KEY]) ? saved[SITE_CHECKS_BLOCKED_KEY] : [];
|
|
2004
|
+
const next = list.filter((entry) => entry !== host);
|
|
2005
|
+
if (block) next.push(host);
|
|
2006
|
+
const approvals2 = { ...saved[SITE_CHECKS_APPROVALS_KEY] ?? {} };
|
|
2007
|
+
for (const origin of Object.keys(approvals2)) {
|
|
2008
|
+
if (new URL(origin).hostname === host) delete approvals2[origin];
|
|
2009
|
+
}
|
|
2010
|
+
const pending = Array.isArray(saved[SITE_CHECKS_PENDING_KEY]) ? saved[SITE_CHECKS_PENDING_KEY] : [];
|
|
2011
|
+
await chrome.storage.local.set({
|
|
2012
|
+
[SITE_CHECKS_BLOCKED_KEY]: next,
|
|
2013
|
+
[SITE_CHECKS_APPROVALS_KEY]: approvals2,
|
|
2014
|
+
[SITE_CHECKS_PENDING_KEY]: pending.filter((entry) => entry.host !== host)
|
|
2015
|
+
});
|
|
2016
|
+
await refreshSiteChecks();
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
function revokeSiteWarning(origin) {
|
|
2020
|
+
return serialized(async () => {
|
|
2021
|
+
const saved = await chrome.storage.local.get(SITE_CHECKS_APPROVALS_KEY);
|
|
2022
|
+
const approvals2 = { ...saved[SITE_CHECKS_APPROVALS_KEY] ?? {} };
|
|
2023
|
+
delete approvals2[origin];
|
|
2024
|
+
await chrome.storage.local.set({ [SITE_CHECKS_APPROVALS_KEY]: approvals2 });
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1536
2028
|
// extension/src/sw/browser-permissions.ts
|
|
1537
2029
|
var initialization;
|
|
1538
2030
|
function initializeBrowserPermissions() {
|
|
1539
2031
|
return initialization ??= (async () => {
|
|
1540
2032
|
await removeGlobalOrigins();
|
|
2033
|
+
await refreshSiteChecks();
|
|
1541
2034
|
const saved = await chrome.storage.local.get([PERMISSIONS_MIGRATION_KEY, PROXY_STORAGE_KEY]);
|
|
1542
2035
|
if (!saved[PERMISSIONS_MIGRATION_KEY]) {
|
|
1543
2036
|
const permissions = ["cookies", "downloads"];
|
|
@@ -1548,17 +2041,22 @@
|
|
|
1548
2041
|
})();
|
|
1549
2042
|
}
|
|
1550
2043
|
async function removeGlobalOrigins() {
|
|
2044
|
+
if (await keepAutomaticPermissions()) return;
|
|
1551
2045
|
const all = await chrome.permissions.getAll();
|
|
1552
2046
|
const origins = (all.origins ?? []).filter(isGlobalOrigin);
|
|
1553
2047
|
if (origins.length) await chrome.permissions.remove({ origins });
|
|
1554
2048
|
const remaining = await chrome.permissions.getAll();
|
|
1555
2049
|
if (remaining.origins?.some(isGlobalOrigin)) throw new Error("Please remove all-site access in Chrome before continuing.");
|
|
1556
2050
|
}
|
|
1557
|
-
async function requireSitePermission(url) {
|
|
2051
|
+
async function requireSitePermission(url, tabId, frameId = 0) {
|
|
1558
2052
|
await initializeBrowserPermissions();
|
|
1559
2053
|
if (url === "about:blank") return;
|
|
2054
|
+
requireSiteChecksReady();
|
|
1560
2055
|
const origin = originForUrl(url);
|
|
1561
|
-
if (origin && await chrome.permissions.contains({ origins: [origin] }))
|
|
2056
|
+
if (origin && await chrome.permissions.contains({ origins: [origin] })) {
|
|
2057
|
+
await checkLocalSite(url, tabId, frameId);
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
1562
2060
|
throw new CmdError("POLICY_DENIED", blockedDomainMessage(hostOf(url) || url));
|
|
1563
2061
|
}
|
|
1564
2062
|
var requestWrite = Promise.resolve();
|
|
@@ -1648,7 +2146,7 @@
|
|
|
1648
2146
|
async function targetTab(cmd) {
|
|
1649
2147
|
return cmd.tabId ? parseTabId(cmd.tabId) : currentTabId();
|
|
1650
2148
|
}
|
|
1651
|
-
var TABLESS = /* @__PURE__ */ new Set(["tabs_list", "tab_new", "ping_probe", "download_file", "proxy_status"]);
|
|
2149
|
+
var TABLESS = /* @__PURE__ */ new Set(["tabs_list", "tab_new", "ping_probe", "site_check", "download_file", "proxy_status"]);
|
|
1652
2150
|
async function resolveTab(cmd) {
|
|
1653
2151
|
if (cmd.method === "download_file" && typeof cmd.params.url !== "string") return targetTab(cmd);
|
|
1654
2152
|
if (TABLESS.has(cmd.method)) return null;
|
|
@@ -1867,11 +2365,11 @@
|
|
|
1867
2365
|
chrome.debugger.detach({ tabId }).catch(() => void 0);
|
|
1868
2366
|
});
|
|
1869
2367
|
}
|
|
1870
|
-
async function withDebugger(tabId, fn, timeoutMs = DEBUGGER_OP_MS) {
|
|
2368
|
+
async function withDebugger(tabId, fn, timeoutMs = DEBUGGER_OP_MS, retryDetached = true) {
|
|
1871
2369
|
return locks.run(`dbg:${tabId}`, async () => {
|
|
1872
2370
|
const target = { tabId };
|
|
1873
2371
|
const once = async () => {
|
|
1874
|
-
await requireSitePermission((await chrome.tabs.get(tabId)).url ?? "");
|
|
2372
|
+
await requireSitePermission((await chrome.tabs.get(tabId)).url ?? "", tabId);
|
|
1875
2373
|
await ensureAttached(tabId);
|
|
1876
2374
|
return fn(target);
|
|
1877
2375
|
};
|
|
@@ -1879,7 +2377,7 @@
|
|
|
1879
2377
|
return await withDeadline(tabId, once(), timeoutMs);
|
|
1880
2378
|
} catch (err) {
|
|
1881
2379
|
if (err instanceof CmdError && err.code === "TIMEOUT") throw err;
|
|
1882
|
-
if (!DETACHED_RE.test(String(err?.message ?? err))) throw err;
|
|
2380
|
+
if (!retryDetached || !DETACHED_RE.test(String(err?.message ?? err))) throw err;
|
|
1883
2381
|
forgetDebugger(tabId);
|
|
1884
2382
|
return await withDeadline(tabId, once(), timeoutMs);
|
|
1885
2383
|
} finally {
|
|
@@ -2092,6 +2590,7 @@
|
|
|
2092
2590
|
"download_file",
|
|
2093
2591
|
"upload_file",
|
|
2094
2592
|
"ping_probe",
|
|
2593
|
+
"site_check",
|
|
2095
2594
|
"frames_list",
|
|
2096
2595
|
"observers",
|
|
2097
2596
|
"print_pdf",
|
|
@@ -2194,8 +2693,15 @@
|
|
|
2194
2693
|
const allowed = [];
|
|
2195
2694
|
const denied2 = [];
|
|
2196
2695
|
for (const f of seen) {
|
|
2197
|
-
if (evaluatePolicy(f.url, cmd.method, policy).ok)
|
|
2198
|
-
|
|
2696
|
+
if (evaluatePolicy(f.url, cmd.method, policy).ok) {
|
|
2697
|
+
try {
|
|
2698
|
+
await requireSitePermission(f.url, tabId, f.frameId);
|
|
2699
|
+
allowed.push(f.frameId);
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
if (err instanceof CmdError && err.code === "POLICY_DENIED") denied2.push(f.url);
|
|
2702
|
+
else throw err;
|
|
2703
|
+
}
|
|
2704
|
+
} else denied2.push(f.url || `frame ${f.frameId}`);
|
|
2199
2705
|
}
|
|
2200
2706
|
if (allowed.length === 0) {
|
|
2201
2707
|
throw new CmdError(
|
|
@@ -2224,7 +2730,9 @@
|
|
|
2224
2730
|
case "tab_select": {
|
|
2225
2731
|
const id = parseTabId(String(cmd.tabId));
|
|
2226
2732
|
const t = await chrome.tabs.update(id, { active: true });
|
|
2227
|
-
|
|
2733
|
+
const selected = t ?? await chrome.tabs.get(id);
|
|
2734
|
+
await chrome.windows.update(selected.windowId, { focused: true });
|
|
2735
|
+
return tabInfo(selected);
|
|
2228
2736
|
}
|
|
2229
2737
|
case "tab_new": {
|
|
2230
2738
|
const url = typeof cmd.params.url === "string" ? cmd.params.url : void 0;
|
|
@@ -2438,43 +2946,57 @@
|
|
|
2438
2946
|
});
|
|
2439
2947
|
const pointCount = strokes.reduce((sum, stroke) => sum + stroke.length, 0);
|
|
2440
2948
|
if (pointCount > 5e3) throw new CmdError("BAD_ARGS", "draw is limited to 5000 total points per call");
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
button: "left",
|
|
2455
|
-
buttons: 1,
|
|
2456
|
-
clickCount: 1
|
|
2457
|
-
});
|
|
2458
|
-
for (const point of stroke.slice(1)) {
|
|
2459
|
-
await chrome.debugger.sendCommand(t, "Input.dispatchMouseEvent", {
|
|
2949
|
+
const budget = Math.max(1, Math.min(drawTimeoutMs(pointCount, strokes.length), cmd.timeoutMs) - 1e3);
|
|
2950
|
+
const deadline = Date.now() + budget;
|
|
2951
|
+
let completed = 0;
|
|
2952
|
+
try {
|
|
2953
|
+
await withDebugger(id, async (t) => {
|
|
2954
|
+
const sendMouse = async (params) => {
|
|
2955
|
+
const remaining = deadline - Date.now();
|
|
2956
|
+
if (remaining <= 0) throw new CmdError("TIMEOUT", "Drawing ran out of time");
|
|
2957
|
+
await withDeadline(id, chrome.debugger.sendCommand(t, "Input.dispatchMouseEvent", params), Math.min(DEBUGGER_OP_MS, remaining));
|
|
2958
|
+
};
|
|
2959
|
+
for (const stroke of strokes) {
|
|
2960
|
+
const first = stroke[0];
|
|
2961
|
+
await sendMouse({
|
|
2460
2962
|
type: "mouseMoved",
|
|
2461
|
-
x:
|
|
2462
|
-
y:
|
|
2963
|
+
x: first.x,
|
|
2964
|
+
y: first.y,
|
|
2965
|
+
buttons: 0
|
|
2966
|
+
});
|
|
2967
|
+
await sendMouse({
|
|
2968
|
+
type: "mousePressed",
|
|
2969
|
+
x: first.x,
|
|
2970
|
+
y: first.y,
|
|
2971
|
+
button: "left",
|
|
2972
|
+
buttons: 1,
|
|
2973
|
+
clickCount: 1
|
|
2974
|
+
});
|
|
2975
|
+
for (const point of stroke.slice(1)) {
|
|
2976
|
+
await sendMouse({
|
|
2977
|
+
type: "mouseMoved",
|
|
2978
|
+
x: point.x,
|
|
2979
|
+
y: point.y,
|
|
2980
|
+
button: "left",
|
|
2981
|
+
buttons: 1
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
const last = stroke[stroke.length - 1];
|
|
2985
|
+
await sendMouse({
|
|
2986
|
+
type: "mouseReleased",
|
|
2987
|
+
x: last.x,
|
|
2988
|
+
y: last.y,
|
|
2463
2989
|
button: "left",
|
|
2464
|
-
buttons:
|
|
2990
|
+
buttons: 0,
|
|
2991
|
+
clickCount: 1
|
|
2465
2992
|
});
|
|
2993
|
+
completed++;
|
|
2466
2994
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
button: "left",
|
|
2473
|
-
buttons: 0,
|
|
2474
|
-
clickCount: 1
|
|
2475
|
-
});
|
|
2476
|
-
}
|
|
2477
|
-
});
|
|
2995
|
+
}, budget, false);
|
|
2996
|
+
} catch (err) {
|
|
2997
|
+
const code = err instanceof CmdError ? err.code : "CDP_ERROR";
|
|
2998
|
+
throw new CmdError(code, `Drawing interrupted after ${completed} of ${strokes.length} complete strokes. Some marks may already be visible; inspect the canvas before continuing. ${err instanceof Error ? err.message : "Browser input failed."}`);
|
|
2999
|
+
}
|
|
2478
3000
|
return { ok: true, strokes: strokes.length, points: pointCount };
|
|
2479
3001
|
}
|
|
2480
3002
|
case "type": {
|
|
@@ -2512,6 +3034,7 @@
|
|
|
2512
3034
|
// Frame ids are re-probed per field too: an iframe that navigates
|
|
2513
3035
|
// mid-batch must not keep a grant the policy would no longer give it.
|
|
2514
3036
|
write: async (op) => {
|
|
3037
|
+
await requireSitePermission(await observedTabUrl(cmd, id), id);
|
|
2515
3038
|
const out = await execOp(
|
|
2516
3039
|
id,
|
|
2517
3040
|
withWait(
|
|
@@ -2897,7 +3420,7 @@
|
|
|
2897
3420
|
deps;
|
|
2898
3421
|
async dispatch(cmd, send = this.deps.send) {
|
|
2899
3422
|
const record = this.deps.activity?.();
|
|
2900
|
-
const entry = cmd.method === "ping_probe" ? null : {
|
|
3423
|
+
const entry = cmd.method === "ping_probe" || cmd.method === "site_check" ? null : {
|
|
2901
3424
|
id: crypto.randomUUID(),
|
|
2902
3425
|
at: Date.now(),
|
|
2903
3426
|
method: cmd.method,
|
|
@@ -2914,6 +3437,7 @@
|
|
|
2914
3437
|
send({ type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data: { ok: true } });
|
|
2915
3438
|
return;
|
|
2916
3439
|
}
|
|
3440
|
+
await initializeBrowserPermissions();
|
|
2917
3441
|
const policy = this.deps.getPolicy();
|
|
2918
3442
|
if (!policy && cmd.method !== "ping_probe") {
|
|
2919
3443
|
throw new CmdError(
|
|
@@ -2932,6 +3456,24 @@
|
|
|
2932
3456
|
checkStopped?.();
|
|
2933
3457
|
checkApproval();
|
|
2934
3458
|
};
|
|
3459
|
+
if (cmd.method === "site_check" || isUrlGated(cmd.method) || cmd.method === "tab_new") requireSiteChecksReady();
|
|
3460
|
+
if (cmd.method === "site_check") {
|
|
3461
|
+
const url = typeof cmd.params.url === "string" ? cmd.params.url : "";
|
|
3462
|
+
const verdict = evaluatePolicy(url, "get_text", policy);
|
|
3463
|
+
if (!verdict.ok) {
|
|
3464
|
+
if (hostOf(url)) this.deps.onBlocked?.(hostOf(url), "get_text");
|
|
3465
|
+
throw new CmdError("POLICY_DENIED", verdict.reason);
|
|
3466
|
+
}
|
|
3467
|
+
try {
|
|
3468
|
+
await requireSitePermission(url);
|
|
3469
|
+
} catch (err) {
|
|
3470
|
+
if (hostOf(url)) this.deps.onBlocked?.(hostOf(url), "get_text");
|
|
3471
|
+
throw err;
|
|
3472
|
+
}
|
|
3473
|
+
checkAccess();
|
|
3474
|
+
send({ type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data: { checked: true } });
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
2935
3477
|
if (cmd.method === "ping_probe") {
|
|
2936
3478
|
checkAccess();
|
|
2937
3479
|
send({ type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data: {} });
|
|
@@ -2977,7 +3519,7 @@
|
|
|
2977
3519
|
}
|
|
2978
3520
|
if (isUrlGated(cmd.method) && url !== "about:blank") {
|
|
2979
3521
|
try {
|
|
2980
|
-
await requireSitePermission(url);
|
|
3522
|
+
await requireSitePermission(url, tab !== null && url === targetUrl ? tab : void 0);
|
|
2981
3523
|
} catch (err) {
|
|
2982
3524
|
const host = hostOf(url);
|
|
2983
3525
|
if (host) this.deps.onBlocked?.(host, cmd.method);
|
|
@@ -3001,6 +3543,13 @@
|
|
|
3001
3543
|
if (Number.isSafeInteger(id) && id >= 0) entry.tabId = id;
|
|
3002
3544
|
if (opened?.url) entry.host = activityHost(opened.url) || entry.host;
|
|
3003
3545
|
}
|
|
3546
|
+
if (siteChecksEnabled() && ["tab_new", "navigate", "back", "forward", "reload"].includes(cmd.method)) {
|
|
3547
|
+
const landedTab = cmd.method === "tab_new" ? entry?.tabId : tab;
|
|
3548
|
+
if (landedTab !== void 0 && landedTab !== null) {
|
|
3549
|
+
const landedUrl = (await chrome.tabs.get(landedTab)).url ?? "";
|
|
3550
|
+
if (/^https?:\/\//i.test(landedUrl)) await requireSitePermission(landedUrl, landedTab);
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3004
3553
|
if (border2) void this.markBorder(border2, cmd, tab, data);
|
|
3005
3554
|
const frame = { type: "result", v: PROTOCOL_VERSION, id: cmd.id, ok: true, data };
|
|
3006
3555
|
const tabUrl = await observedTabUrl(cmd, tab);
|
|
@@ -3014,7 +3563,10 @@
|
|
|
3014
3563
|
send(frame);
|
|
3015
3564
|
} catch (err) {
|
|
3016
3565
|
const code = err instanceof CmdError ? err.code : "CDP_ERROR";
|
|
3017
|
-
|
|
3566
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
3567
|
+
if (ran && message.startsWith("Suspicious website ")) {
|
|
3568
|
+
message = `Website access paused after the command started: ${message} The page may already be open; use tabs_list after approval.`;
|
|
3569
|
+
}
|
|
3018
3570
|
if (entry) entry.outcome = ran && code === "POLICY_DENIED" ? "interrupted" : code === "POLICY_DENIED" ? "blocked" : "failed";
|
|
3019
3571
|
this.deps.log(`command "${cmd.method}" failed: ${message}`);
|
|
3020
3572
|
const frame = {
|
|
@@ -3044,6 +3596,21 @@
|
|
|
3044
3596
|
}
|
|
3045
3597
|
};
|
|
3046
3598
|
|
|
3599
|
+
// extension/src/sw/install-identity.ts
|
|
3600
|
+
var identity;
|
|
3601
|
+
function ensureInstallId() {
|
|
3602
|
+
return identity ??= (async () => {
|
|
3603
|
+
const { installId } = await chrome.storage.local.get("installId");
|
|
3604
|
+
if (typeof installId === "string" && installId.length > 0) return installId;
|
|
3605
|
+
const fresh = crypto.randomUUID();
|
|
3606
|
+
await chrome.storage.local.set({ installId: fresh });
|
|
3607
|
+
return fresh;
|
|
3608
|
+
})().catch((err) => {
|
|
3609
|
+
identity = void 0;
|
|
3610
|
+
throw err;
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3047
3614
|
// extension/src/sw/observers.ts
|
|
3048
3615
|
var SCRIPT_ID = "chrome-mcp-observers";
|
|
3049
3616
|
var updates = Promise.resolve();
|
|
@@ -3055,7 +3622,7 @@
|
|
|
3055
3622
|
try {
|
|
3056
3623
|
await initializeBrowserPermissions();
|
|
3057
3624
|
const existing = await chrome.scripting.getRegisteredContentScripts({ ids: [SCRIPT_ID] }).catch(() => []);
|
|
3058
|
-
const candidates = (policy ? observerMatches(policy) : []).flatMap((pattern) => pattern.startsWith("*://") ? [`http${pattern.slice(1)}`, `https${pattern.slice(1)}`] : [pattern]);
|
|
3625
|
+
const candidates = (policy && !policy.automaticSites ? observerMatches(policy) : []).flatMap((pattern) => pattern.startsWith("*://") ? [`http${pattern.slice(1)}`, `https${pattern.slice(1)}`] : [pattern]);
|
|
3059
3626
|
const checks = await Promise.all(candidates.map(async (pattern) => await chrome.permissions.contains({ origins: [pattern] }) ? pattern : null));
|
|
3060
3627
|
const wanted = checks.filter((pattern) => pattern !== null);
|
|
3061
3628
|
if (wanted.length === 0) {
|
|
@@ -3143,7 +3710,7 @@
|
|
|
3143
3710
|
return v && typeof v === "object" ? { ...v } : {};
|
|
3144
3711
|
}
|
|
3145
3712
|
var saveMarked = (m) => chrome.storage.session.set({ [KEY]: m }).catch(() => void 0);
|
|
3146
|
-
async function
|
|
3713
|
+
async function enabled2() {
|
|
3147
3714
|
const { tabBorder } = await chrome.storage.local.get("tabBorder");
|
|
3148
3715
|
return tabBorder !== false;
|
|
3149
3716
|
}
|
|
@@ -3219,7 +3786,7 @@
|
|
|
3219
3786
|
/** A command just ran in `tabId`: border it if it isn't already. */
|
|
3220
3787
|
async used(tabId) {
|
|
3221
3788
|
return this.locked(async () => {
|
|
3222
|
-
if (!await
|
|
3789
|
+
if (!await enabled2() || !await tabAllowed(tabId, this.getPolicy())) return;
|
|
3223
3790
|
const marked = await loadMarked();
|
|
3224
3791
|
if (marked[tabId]) return;
|
|
3225
3792
|
marked[tabId] = await paint(tabId);
|
|
@@ -3229,7 +3796,7 @@
|
|
|
3229
3796
|
/** A tab opened by tab_new: remember it, and paint once it loads. */
|
|
3230
3797
|
async opened(tabId) {
|
|
3231
3798
|
return this.locked(async () => {
|
|
3232
|
-
if (!await
|
|
3799
|
+
if (!await enabled2()) return;
|
|
3233
3800
|
const marked = await loadMarked();
|
|
3234
3801
|
marked[tabId] = marked[tabId] ?? "";
|
|
3235
3802
|
await saveMarked(marked);
|
|
@@ -3242,7 +3809,7 @@
|
|
|
3242
3809
|
return this.locked(async () => {
|
|
3243
3810
|
const marked = await loadMarked();
|
|
3244
3811
|
if (!(tabId in marked)) return;
|
|
3245
|
-
if (!await
|
|
3812
|
+
if (!await enabled2() || !await tabAllowed(tabId, this.getPolicy())) {
|
|
3246
3813
|
await unpaint(tabId, marked[tabId]);
|
|
3247
3814
|
delete marked[tabId];
|
|
3248
3815
|
} else {
|
|
@@ -3300,55 +3867,13 @@
|
|
|
3300
3867
|
return checks.filter((site) => site !== null);
|
|
3301
3868
|
}
|
|
3302
3869
|
|
|
3303
|
-
// extension/src/connect.ts
|
|
3304
|
-
var NATIVE_HOST = "com.mehmoodqureshi.mcp_browser_extension";
|
|
3305
|
-
async function fetchPairing() {
|
|
3306
|
-
if (typeof chrome.runtime.sendNativeMessage !== "function") {
|
|
3307
|
-
return { ok: false, error: "restart_needed", message: "Finishing setup. Click Connect again if this stays." };
|
|
3308
|
-
}
|
|
3309
|
-
try {
|
|
3310
|
-
const reply = await chrome.runtime.sendNativeMessage(NATIVE_HOST, { type: "pairing" });
|
|
3311
|
-
return reply ?? { ok: false, message: "The helper gave no answer." };
|
|
3312
|
-
} catch (err) {
|
|
3313
|
-
const why = err instanceof Error ? err.message : String(err);
|
|
3314
|
-
if (/not found|not registered|Specified native messaging host/i.test(why)) {
|
|
3315
|
-
return {
|
|
3316
|
-
ok: false,
|
|
3317
|
-
error: "no_helper",
|
|
3318
|
-
message: "Add the MCP Browser Extension server to your AI client (Claude Code, Cursor\u2026) and start it once; it sets this up. Then click Connect again."
|
|
3319
|
-
};
|
|
3320
|
-
}
|
|
3321
|
-
return { ok: false, message: why };
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
async function applyPairing(reply) {
|
|
3325
|
-
await chrome.storage.local.set({
|
|
3326
|
-
wsPort: reply.port,
|
|
3327
|
-
token: reply.token,
|
|
3328
|
-
// The server's own --profile, as a suggestion only: another browser may
|
|
3329
|
-
// already use that name, and taking it over would disconnect that one.
|
|
3330
|
-
suggestedProfile: reply.profile ?? "",
|
|
3331
|
-
pairingSource: "native"
|
|
3332
|
-
});
|
|
3333
|
-
}
|
|
3334
|
-
async function requestPairingFromClick() {
|
|
3335
|
-
let granted = false;
|
|
3336
|
-
try {
|
|
3337
|
-
granted = await chrome.permissions.request({ permissions: ["nativeMessaging"] });
|
|
3338
|
-
} catch (err) {
|
|
3339
|
-
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
3340
|
-
}
|
|
3341
|
-
if (!granted) return { ok: false, message: "Chrome needs that permission to fetch the pairing for you. You can also pair by hand in Settings." };
|
|
3342
|
-
return fetchPairing();
|
|
3343
|
-
}
|
|
3344
|
-
|
|
3345
3870
|
// extension/src/sw/background.ts
|
|
3346
3871
|
var PAIR_AFTER_RESTART_KEY = "pairAfterRestart";
|
|
3347
3872
|
var commandsRunning = 0;
|
|
3348
3873
|
var updateReady = false;
|
|
3349
3874
|
var updateCheckedFor = "";
|
|
3350
3875
|
function runningVersion() {
|
|
3351
|
-
return true ? "0.9.
|
|
3876
|
+
return true ? "0.9.38" : chrome.runtime.getManifest().version;
|
|
3352
3877
|
}
|
|
3353
3878
|
function isNewer(a, b) {
|
|
3354
3879
|
const parse = (v) => (/^(\d+)\.(\d+)\.(\d+)/.exec(v) ?? []).slice(1).map(Number);
|
|
@@ -3383,11 +3908,13 @@
|
|
|
3383
3908
|
var KEEPALIVE_ALARM = "chrome-mcp-keepalive";
|
|
3384
3909
|
var MAX_BLOCKED = 10;
|
|
3385
3910
|
var currentPolicy = null;
|
|
3911
|
+
var serverSiteChecks = false;
|
|
3912
|
+
var effectiveSitePolicy = () => serverSiteChecks ? localSitePolicy(currentPolicy) : currentPolicy;
|
|
3386
3913
|
var reportBlocked = (host, method) => {
|
|
3387
3914
|
void rememberBlocked(host, method).catch(() => void 0);
|
|
3388
3915
|
};
|
|
3389
|
-
var executor = new ChromeExecutor(
|
|
3390
|
-
var border = new TabBorder(
|
|
3916
|
+
var executor = new ChromeExecutor(effectiveSitePolicy, reportBlocked);
|
|
3917
|
+
var border = new TabBorder(effectiveSitePolicy);
|
|
3391
3918
|
var RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 1e4];
|
|
3392
3919
|
var reconnectAttempt = 0;
|
|
3393
3920
|
var reconnectTimer = null;
|
|
@@ -3406,6 +3933,11 @@
|
|
|
3406
3933
|
reconnectAttempt = 0;
|
|
3407
3934
|
}
|
|
3408
3935
|
var ws = new WsClient({
|
|
3936
|
+
onSiteChecks: (supported2) => {
|
|
3937
|
+
serverSiteChecks = supported2;
|
|
3938
|
+
setSiteChecksSupported(supported2);
|
|
3939
|
+
void chrome.storage.local.set({ siteChecksSupported: supported2 });
|
|
3940
|
+
},
|
|
3409
3941
|
onCommand: (cmd, reply) => {
|
|
3410
3942
|
commandsRunning++;
|
|
3411
3943
|
void router.dispatch(cmd, reply).finally(() => {
|
|
@@ -3428,9 +3960,9 @@
|
|
|
3428
3960
|
void chrome.storage.local.set({ allowedSites: [] });
|
|
3429
3961
|
return;
|
|
3430
3962
|
}
|
|
3431
|
-
policy = { ...policy, allowDomains: policy.allowDomains.map(normalizeDomainPattern).filter(Boolean) };
|
|
3963
|
+
policy = { ...policy, automaticSites: false, blockedDomains: [], allowDomains: policy.allowDomains.map(normalizeDomainPattern).filter(Boolean) };
|
|
3432
3964
|
currentPolicy = policy;
|
|
3433
|
-
void syncObserverScript(policy, (m) => console.debug("[mcp-browser-extension]", m));
|
|
3965
|
+
void syncObserverScript(localSitePolicy(policy), (m) => console.debug("[mcp-browser-extension]", m));
|
|
3434
3966
|
void onAllowlist(policy);
|
|
3435
3967
|
},
|
|
3436
3968
|
// Shown on the Options page, so you can tell which name this browser got.
|
|
@@ -3490,14 +4022,14 @@
|
|
|
3490
4022
|
authorize: (cmd, verified) => approvals.authorize(cmd, verified),
|
|
3491
4023
|
access: () => access.check(),
|
|
3492
4024
|
activity: () => {
|
|
3493
|
-
const
|
|
4025
|
+
const generation2 = activity.token();
|
|
3494
4026
|
return (entry) => {
|
|
3495
|
-
void activity.record(entry,
|
|
4027
|
+
void activity.record(entry, generation2).catch(historyFailed);
|
|
3496
4028
|
};
|
|
3497
4029
|
},
|
|
3498
4030
|
exec: executor,
|
|
3499
4031
|
send: (frame) => ws.send(frame),
|
|
3500
|
-
getPolicy:
|
|
4032
|
+
getPolicy: effectiveSitePolicy,
|
|
3501
4033
|
onBlocked: reportBlocked,
|
|
3502
4034
|
log: (m) => console.debug("[mcp-browser-extension]", m),
|
|
3503
4035
|
border
|
|
@@ -3528,7 +4060,7 @@
|
|
|
3528
4060
|
const res = await fetch(chrome.runtime.getURL("manifest.json"), { cache: "no-store" });
|
|
3529
4061
|
if (!res.ok) return;
|
|
3530
4062
|
const onDisk = await res.json();
|
|
3531
|
-
const loaded = true ? "0.9.
|
|
4063
|
+
const loaded = true ? "0.9.38" : chrome.runtime.getManifest().version;
|
|
3532
4064
|
if (typeof onDisk.version !== "string" || onDisk.version === loaded) return;
|
|
3533
4065
|
const { reloadedFor } = await chrome.storage.local.get("reloadedFor");
|
|
3534
4066
|
if (reloadedFor === onDisk.version) return;
|
|
@@ -3538,20 +4070,13 @@
|
|
|
3538
4070
|
} catch {
|
|
3539
4071
|
}
|
|
3540
4072
|
}
|
|
3541
|
-
async function ensureInstallId(installId) {
|
|
3542
|
-
if (typeof installId === "string" && installId.length > 0) return installId;
|
|
3543
|
-
const fresh = crypto.randomUUID();
|
|
3544
|
-
await chrome.storage.local.set({ installId: fresh });
|
|
3545
|
-
return fresh;
|
|
3546
|
-
}
|
|
3547
4073
|
async function getConfig() {
|
|
3548
|
-
const { wsPort, token, profile, suggestedProfile, pairingSource
|
|
4074
|
+
const { wsPort, token, profile, suggestedProfile, pairingSource } = await chrome.storage.local.get([
|
|
3549
4075
|
"wsPort",
|
|
3550
4076
|
"token",
|
|
3551
4077
|
"profile",
|
|
3552
4078
|
"suggestedProfile",
|
|
3553
|
-
"pairingSource"
|
|
3554
|
-
"installId"
|
|
4079
|
+
"pairingSource"
|
|
3555
4080
|
]);
|
|
3556
4081
|
if (typeof wsPort === "number" && wsPort > 0 && typeof token === "string" && token.length > 0) {
|
|
3557
4082
|
const label = typeof profile === "string" ? profile.trim() : "";
|
|
@@ -3562,23 +4087,30 @@
|
|
|
3562
4087
|
token,
|
|
3563
4088
|
profile: fromHelper ? "" : label,
|
|
3564
4089
|
suggestedProfile: suggested || (fromHelper ? label : ""),
|
|
3565
|
-
installId: await ensureInstallId(
|
|
4090
|
+
installId: await ensureInstallId()
|
|
3566
4091
|
};
|
|
3567
4092
|
}
|
|
3568
4093
|
return null;
|
|
3569
4094
|
}
|
|
3570
4095
|
async function onAllowlist(policy) {
|
|
3571
4096
|
await initializeBrowserPermissions();
|
|
3572
|
-
const effective = { ...policy, allowDomains: await permittedSites(policy.allowDomains) };
|
|
4097
|
+
const effective = { ...localSitePolicy(policy), allowDomains: await permittedSites(policy.allowDomains) };
|
|
3573
4098
|
const { blockedSites } = await chrome.storage.local.get("blockedSites");
|
|
3574
4099
|
const still = (Array.isArray(blockedSites) ? blockedSites : []).filter(
|
|
3575
|
-
(b) => !isDomainAllowed(`https://${b.host}/`, effective)
|
|
4100
|
+
(b) => !siteChecksEnabled() && !isDomainAllowed(`https://${b.host}/`, effective)
|
|
3576
4101
|
);
|
|
3577
4102
|
await chrome.storage.local.set({ allowedSites: policy.allowDomains, blockedSites: still });
|
|
3578
4103
|
pendingBlocked = still.length;
|
|
3579
4104
|
reflectBadge(ws.state);
|
|
3580
4105
|
}
|
|
3581
|
-
|
|
4106
|
+
var blockedQueue = Promise.resolve();
|
|
4107
|
+
function rememberBlocked(host, method) {
|
|
4108
|
+
const next = blockedQueue.then(() => recordBlocked(host, method));
|
|
4109
|
+
blockedQueue = next.catch(() => void 0);
|
|
4110
|
+
return next;
|
|
4111
|
+
}
|
|
4112
|
+
async function recordBlocked(host, method) {
|
|
4113
|
+
if (siteChecksConfigured()) return;
|
|
3582
4114
|
const { blockedSites, dismissedSites } = await chrome.storage.local.get(["blockedSites", "dismissedSites"]);
|
|
3583
4115
|
const listed = Array.isArray(blockedSites) ? blockedSites : [];
|
|
3584
4116
|
const prior = listed.filter((b) => b.host !== host);
|
|
@@ -3614,6 +4146,12 @@
|
|
|
3614
4146
|
})().catch(() => void 0);
|
|
3615
4147
|
});
|
|
3616
4148
|
var pendingBlocked = 0;
|
|
4149
|
+
var pendingSiteWarnings = 0;
|
|
4150
|
+
onSiteWarnings((count, fresh) => {
|
|
4151
|
+
pendingSiteWarnings = count;
|
|
4152
|
+
reflectBadge(ws.state);
|
|
4153
|
+
if (fresh) void showPopup();
|
|
4154
|
+
});
|
|
3617
4155
|
void chrome.storage.local.get("blockedSites").then(({ blockedSites }) => {
|
|
3618
4156
|
pendingBlocked = Array.isArray(blockedSites) ? blockedSites.length : 0;
|
|
3619
4157
|
});
|
|
@@ -3634,7 +4172,7 @@
|
|
|
3634
4172
|
}
|
|
3635
4173
|
};
|
|
3636
4174
|
function reflectBadge(state) {
|
|
3637
|
-
const b = access.stopped ? { text: "OFF", color: "#b91c1c", title: "MCP Browser Extension \u2014 access stopped" } : pendingClients > 0 ? { text: "?", color: "#b45309", title: "App connection awaiting approval. Open Settings to review." } : state === "connected" && pendingBlocked > 0 ? BLOCKED_BADGE : pendingFeatures > 0 ? { text: "?", color: "#b45309", title: "Permission needed. Open the extension popup \u2192 Extra permissions." } : BADGE[state] ?? BADGE.idle;
|
|
4175
|
+
const b = access.stopped ? { text: "OFF", color: "#b91c1c", title: "MCP Browser Extension \u2014 access stopped" } : pendingClients > 0 ? { text: "?", color: "#b45309", title: "App connection awaiting approval. Open Settings to review." } : state === "connected" && (pendingBlocked > 0 || pendingSiteWarnings > 0) ? BLOCKED_BADGE : pendingFeatures > 0 ? { text: "?", color: "#b45309", title: "Permission needed. Open the extension popup \u2192 Extra permissions." } : BADGE[state] ?? BADGE.idle;
|
|
3638
4176
|
try {
|
|
3639
4177
|
void chrome.action.setBadgeText({ text: b.text });
|
|
3640
4178
|
void chrome.action.setBadgeBackgroundColor({ color: b.color });
|
|
@@ -3698,21 +4236,22 @@
|
|
|
3698
4236
|
}
|
|
3699
4237
|
syncProxyAuthListener();
|
|
3700
4238
|
async function permissionsChanged() {
|
|
4239
|
+
await refreshSiteChecks();
|
|
3701
4240
|
syncProxyAuthListener();
|
|
3702
4241
|
await refreshFeatureBadge();
|
|
3703
4242
|
if (currentPolicy) {
|
|
3704
4243
|
await onAllowlist(currentPolicy);
|
|
3705
|
-
await syncObserverScript(currentPolicy, (m) => console.debug("[mcp-browser-extension]", m));
|
|
4244
|
+
await syncObserverScript(localSitePolicy(currentPolicy), (m) => console.debug("[mcp-browser-extension]", m));
|
|
3706
4245
|
}
|
|
3707
4246
|
await border.clearAll();
|
|
3708
4247
|
}
|
|
3709
4248
|
var featureBadgeGeneration = 0;
|
|
3710
4249
|
async function refreshFeatureBadge() {
|
|
3711
|
-
const
|
|
4250
|
+
const generation2 = ++featureBadgeGeneration;
|
|
3712
4251
|
const saved = (await chrome.storage.local.get(FEATURE_REQUESTS_KEY))[FEATURE_REQUESTS_KEY];
|
|
3713
4252
|
const requested = Array.isArray(saved) ? saved.filter((p) => typeof p === "string" && p in OPTIONAL_FEATURES) : [];
|
|
3714
4253
|
const missing = await Promise.all(requested.map(async (permission) => !await chrome.permissions.contains({ permissions: [permission] })));
|
|
3715
|
-
if (
|
|
4254
|
+
if (generation2 !== featureBadgeGeneration) return;
|
|
3716
4255
|
const stillMissing = requested.filter((_, i) => missing[i]);
|
|
3717
4256
|
pendingFeatures = stillMissing.length;
|
|
3718
4257
|
reflectBadge(ws.state);
|
|
@@ -3728,6 +4267,9 @@
|
|
|
3728
4267
|
void permissionsChanged().catch((err) => console.warn("[mcp-browser-extension] permission change failed", err));
|
|
3729
4268
|
});
|
|
3730
4269
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
4270
|
+
if (area === "local" && (changes[SITE_CHECKS_KEY] || changes[SITE_CHECKS_BLOCKED_KEY] || changes[SITE_CHECKS_PENDING_KEY])) {
|
|
4271
|
+
void permissionsChanged();
|
|
4272
|
+
}
|
|
3731
4273
|
if (area === "local" && changes[FEATURE_REQUESTS_KEY]) void refreshFeatureBadge();
|
|
3732
4274
|
if (area === "local" && changes.tabBorder?.newValue === false) void border.clearAll();
|
|
3733
4275
|
if (area === "local" && changes[PROXY_STORAGE_KEY]) void syncProxy();
|
|
@@ -3739,6 +4281,61 @@
|
|
|
3739
4281
|
}
|
|
3740
4282
|
});
|
|
3741
4283
|
chrome.runtime.onMessage.addListener((msg, sender, reply) => {
|
|
4284
|
+
if (["site_checks_mode", "site_warning_allow", "site_warning_revoke", "site_block"].includes(msg?.type)) {
|
|
4285
|
+
if (!isApprovalPage(sender) || access.stopped || !ws.isConnected()) {
|
|
4286
|
+
reply({ ok: false, error: "Use the connected extension popup or Settings to change website access." });
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
let operation;
|
|
4290
|
+
if (msg.type === "site_checks_mode" && typeof msg.allow === "boolean") {
|
|
4291
|
+
if (msg.allow && !serverSiteChecks) {
|
|
4292
|
+
reply({ ok: false, error: "Update and restart the connected MCP server first." });
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
operation = changeSiteChecks(msg.allow);
|
|
4296
|
+
} else if (msg.type === "site_warning_allow" && typeof msg.origin === "string" && Array.isArray(msg.reasons) && msg.reasons.length <= 32 && msg.reasons.every((id) => typeof id === "string")) {
|
|
4297
|
+
operation = allowSiteWarning(msg.origin, msg.reasons);
|
|
4298
|
+
} else if (msg.type === "site_warning_revoke" && typeof msg.origin === "string") {
|
|
4299
|
+
operation = revokeSiteWarning(msg.origin);
|
|
4300
|
+
} else if (msg.type === "site_block" && typeof msg.host === "string" && typeof msg.block === "boolean" && siteOrigins(msg.host).length) {
|
|
4301
|
+
operation = blockLocalSite(normalizeDomainPattern(msg.host), msg.block);
|
|
4302
|
+
} else {
|
|
4303
|
+
reply({ ok: false, error: "Invalid website access request." });
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
void operation.then(async () => {
|
|
4307
|
+
if (currentPolicy) await onAllowlist(currentPolicy);
|
|
4308
|
+
ws.send({ type: "approval_changed", v: PROTOCOL_VERSION });
|
|
4309
|
+
reply({ ok: true });
|
|
4310
|
+
}, (err) => reply({ ok: false, error: err instanceof Error ? err.message : "Could not save website access." })).finally(() => {
|
|
4311
|
+
if (msg.type === "site_checks_mode" && msg.allow) void reopenPopup(sender);
|
|
4312
|
+
});
|
|
4313
|
+
return true;
|
|
4314
|
+
}
|
|
4315
|
+
if (msg?.type === "phone_pairing") {
|
|
4316
|
+
if (!isApprovalPage(sender) || !PHONE_ACTIONS.includes(msg.action)) {
|
|
4317
|
+
reply({ ok: false, message: "Use Pair phone in the extension popup." });
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
const permission = msg.action === "create" ? chrome.permissions.request({ permissions: ["nativeMessaging"] }) : chrome.permissions.contains({ permissions: ["nativeMessaging"] });
|
|
4321
|
+
void permission.then(async (granted) => {
|
|
4322
|
+
if (!granted) {
|
|
4323
|
+
reply({ ok: false, message: "Click Generate QR code to allow phone pairing." });
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
if (typeof chrome.runtime.sendNativeMessage !== "function" && msg.action === "create") {
|
|
4327
|
+
reply({ ok: false, message: "Permission saved. Reopen the extension and click Pair phone." });
|
|
4328
|
+
setTimeout(() => chrome.runtime.reload(), 200);
|
|
4329
|
+
return;
|
|
4330
|
+
}
|
|
4331
|
+
reply(await phonePairingAction(msg.action, msg.id, {
|
|
4332
|
+
approvals,
|
|
4333
|
+
stopped: () => access.stopped,
|
|
4334
|
+
connected: () => ws.isConnected()
|
|
4335
|
+
}));
|
|
4336
|
+
}).catch(() => reply({ ok: false, message: "Could not open phone pairing. Try again." }));
|
|
4337
|
+
return true;
|
|
4338
|
+
}
|
|
3742
4339
|
if (msg?.type === "connect_with_click") {
|
|
3743
4340
|
if (!isApprovalPage(sender) || access.stopped) {
|
|
3744
4341
|
reply({ ok: false, message: "Use Resume connection in the extension popup or Settings first." });
|
|
@@ -3774,9 +4371,9 @@
|
|
|
3774
4371
|
return;
|
|
3775
4372
|
}
|
|
3776
4373
|
const operation = msg.allow ? chrome.permissions.request({ permissions: [msg.feature] }) : chrome.permissions.remove({ permissions: [msg.feature] });
|
|
3777
|
-
void operation.then((
|
|
3778
|
-
ok:
|
|
3779
|
-
...!
|
|
4374
|
+
void operation.then((changed2) => reply({
|
|
4375
|
+
ok: changed2 || !msg.allow,
|
|
4376
|
+
...!changed2 && msg.allow ? { error: "No access was added. You can allow it whenever you are ready." } : {}
|
|
3780
4377
|
}), () => reply({ ok: false, error: "Could not change this permission. Please try again." })).finally(() => {
|
|
3781
4378
|
if (msg.allow) void reopenPopup(sender);
|
|
3782
4379
|
});
|
|
@@ -3848,7 +4445,8 @@
|
|
|
3848
4445
|
reply({ ok: false, error: "Please click Allow for this website and accept Chrome\u2019s permission request." });
|
|
3849
4446
|
return;
|
|
3850
4447
|
}
|
|
3851
|
-
if (
|
|
4448
|
+
if (siteChecksEnabled()) await blockLocalSite(normalizeDomainPattern(msg.host), !msg.allow);
|
|
4449
|
+
else if (!msg.allow) await chrome.permissions.remove({ origins });
|
|
3852
4450
|
if (!ws.isConnected() || msg.allow && access.stopped) {
|
|
3853
4451
|
reply({ ok: false, error: "Access is stopped or disconnected. Reconnect before allowing this website." });
|
|
3854
4452
|
return;
|