mcp-browser-extension 0.9.30 → 0.9.32
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 +77 -14
- package/SETUP.md +40 -7
- package/dist/shared/activity.d.ts +25 -0
- package/dist/shared/activity.js +72 -0
- package/dist/shared/browser-permissions.d.ts +20 -0
- package/dist/shared/browser-permissions.js +40 -0
- package/dist/shared/client-auth.d.ts +28 -0
- package/dist/shared/client-auth.js +38 -0
- package/dist/shared/observers.d.ts +1 -1
- package/dist/shared/observers.js +1 -3
- package/dist/shared/policy.d.ts +5 -7
- package/dist/shared/policy.js +11 -26
- package/dist/shared/protocol.d.ts +4 -0
- package/dist/shared/protocol.js +3 -3
- package/dist/shared/screenshot.d.ts +4 -2
- package/dist/shared/screenshot.js +13 -8
- package/dist/src/access-browser.d.ts +15 -0
- package/dist/src/access-browser.js +202 -0
- package/dist/src/access.d.ts +2 -0
- package/dist/src/access.js +92 -18
- package/dist/src/bridge/client-identity.d.ts +10 -0
- package/dist/src/bridge/client-identity.js +28 -0
- package/dist/src/bridge/connection.d.ts +5 -0
- package/dist/src/bridge/connection.js +4 -12
- package/dist/src/bridge/peer.d.ts +4 -19
- package/dist/src/bridge/peer.js +17 -8
- package/dist/src/bridge/server.d.ts +7 -16
- package/dist/src/bridge/server.js +64 -17
- package/dist/src/bridge/site-grants.d.ts +1 -2
- package/dist/src/bridge/site-grants.js +4 -3
- package/dist/src/cli.js +1 -3
- package/dist/src/config.js +16 -5
- package/dist/src/executor/cdp-executor.d.ts +3 -0
- package/dist/src/executor/cdp-executor.js +69 -25
- package/dist/src/mcp/server.js +5 -3
- package/dist/src/mcp/tools.d.ts +0 -12
- package/dist/src/mcp/tools.js +48 -22
- package/dist/src/remote/key.js +12 -2
- package/dist/src/security/policy.d.ts +4 -0
- package/dist/src/security/policy.js +3 -1
- package/docs/BLUEPRINT.md +6 -1
- package/docs/CLIENT-APPROVAL.md +150 -0
- package/extension-dist/activity-dashboard.css +43 -0
- package/extension-dist/background.js +1036 -73
- package/extension-dist/manifest.json +10 -9
- package/extension-dist/options.html +101 -32
- package/extension-dist/options.js +558 -70
- package/extension-dist/popup.html +164 -90
- package/extension-dist/popup.js +576 -79
- package/package.json +3 -2
|
@@ -1,48 +1,493 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// shared/client-auth.ts
|
|
4
|
+
var CLIENT_GRANT_MS = 8 * 60 * 60 * 1e3;
|
|
5
|
+
var CLIENT_CLOCK_SKEW_MS = 2 * 60 * 1e3;
|
|
6
|
+
var CLIENT_REQUEST_MS = 10 * 60 * 1e3;
|
|
7
|
+
var CLIENT_STORAGE_KEY = "clientConnections";
|
|
8
|
+
|
|
3
9
|
// shared/protocol.ts
|
|
4
10
|
var DEFAULT_WS_PORT = 38017;
|
|
5
11
|
|
|
6
|
-
//
|
|
7
|
-
var
|
|
8
|
-
|
|
12
|
+
// shared/activity.ts
|
|
13
|
+
var ACCESS_STOPPED_KEY = "accessStopped";
|
|
14
|
+
var ACTIVITY_KEY = "recentActivity";
|
|
15
|
+
function activityRows(entries) {
|
|
16
|
+
const rows = [];
|
|
17
|
+
const opens = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const key = entry.tabId !== void 0 && entry.key ? `${entry.key}:${entry.tabId}` : "";
|
|
20
|
+
if (key && entry.method === "tab_new" && entry.outcome === "done") opens.set(key, entry);
|
|
21
|
+
const opened = key && entry.method === "tab_close" && entry.outcome === "done" ? opens.get(key) : void 0;
|
|
22
|
+
if (opened) {
|
|
23
|
+
const index = rows.findIndex((row) => row.id === opened.id);
|
|
24
|
+
if (index !== -1) rows.splice(index, 1);
|
|
25
|
+
rows.push({ ...entry, host: entry.host || opened.host, opened });
|
|
26
|
+
opens.delete(key);
|
|
27
|
+
} else rows.push(entry);
|
|
28
|
+
}
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
var LABELS = {
|
|
32
|
+
tabs_list: "List tabs",
|
|
33
|
+
tab_select: "Select tab",
|
|
34
|
+
tab_new: "Open tab",
|
|
35
|
+
tab_close: "Close tab",
|
|
36
|
+
navigate: "Navigate",
|
|
37
|
+
back: "Go back",
|
|
38
|
+
forward: "Go forward",
|
|
39
|
+
reload: "Reload page",
|
|
40
|
+
click: "Click",
|
|
41
|
+
draw: "Draw",
|
|
42
|
+
type: "Type text",
|
|
43
|
+
press: "Press key",
|
|
44
|
+
hover: "Hover",
|
|
45
|
+
scroll: "Scroll",
|
|
46
|
+
screenshot: "Take screenshot",
|
|
47
|
+
get_text: "Read text",
|
|
48
|
+
get_html: "Read HTML",
|
|
49
|
+
eval: "Run page script",
|
|
50
|
+
snapshot: "Read page structure",
|
|
51
|
+
fill_form: "Fill form",
|
|
52
|
+
get_cookies: "Read cookies",
|
|
53
|
+
storage: "Access site storage",
|
|
54
|
+
download_file: "Download file",
|
|
55
|
+
upload_file: "Upload file",
|
|
56
|
+
print_pdf: "Print PDF",
|
|
57
|
+
select_option: "Choose option",
|
|
58
|
+
observers: "Read page observations",
|
|
59
|
+
proxy_status: "Check proxy",
|
|
60
|
+
frames: "List frames"
|
|
61
|
+
};
|
|
62
|
+
var activityLabel = (method) => LABELS[method] ?? method.replaceAll("_", " ");
|
|
63
|
+
|
|
64
|
+
// extension/src/ui-controls.ts
|
|
65
|
+
var lists = /* @__PURE__ */ new WeakMap();
|
|
66
|
+
var pending = /* @__PURE__ */ new Map();
|
|
67
|
+
var pressing = false;
|
|
68
|
+
var pressGeneration = 0;
|
|
69
|
+
document.addEventListener("pointerdown", () => {
|
|
70
|
+
pressing = true;
|
|
71
|
+
pressGeneration++;
|
|
72
|
+
}, true);
|
|
73
|
+
function release() {
|
|
74
|
+
const generation = pressGeneration;
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
if (generation !== pressGeneration) return;
|
|
77
|
+
pressing = false;
|
|
78
|
+
const updates = [...pending.values()];
|
|
79
|
+
pending.clear();
|
|
80
|
+
for (const update of updates) update();
|
|
81
|
+
}, 0);
|
|
82
|
+
}
|
|
83
|
+
window.addEventListener("pointerup", release, true);
|
|
84
|
+
window.addEventListener("pointercancel", release, true);
|
|
85
|
+
window.addEventListener("blur", release);
|
|
86
|
+
function renderList(list4, items, key, signature, create) {
|
|
87
|
+
if (pressing) {
|
|
88
|
+
pending.set(list4, () => renderList(list4, items, key, signature, create));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
pending.delete(list4);
|
|
92
|
+
const previous = lists.get(list4) ?? /* @__PURE__ */ new Map();
|
|
93
|
+
const next = /* @__PURE__ */ new Map();
|
|
94
|
+
const scrollTop = list4.scrollTop;
|
|
95
|
+
let cursor = list4.firstElementChild;
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
const id = key(item), version = signature(item), old = previous.get(id);
|
|
98
|
+
const element = old?.signature === version ? old.element : create(item);
|
|
99
|
+
if (old && element !== old.element) {
|
|
100
|
+
const details = element.querySelector("details");
|
|
101
|
+
if (details) details.open = old.element.querySelector("details")?.open ?? false;
|
|
102
|
+
if (old.element.parentElement === list4) {
|
|
103
|
+
if (cursor === old.element) cursor = element;
|
|
104
|
+
old.element.replaceWith(element);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (element === cursor) cursor = cursor.nextElementSibling;
|
|
108
|
+
else list4.insertBefore(element, cursor);
|
|
109
|
+
next.set(id, { signature: version, element });
|
|
110
|
+
}
|
|
111
|
+
const keep = new Set([...next.values()].map((row) => row.element));
|
|
112
|
+
for (const child of Array.from(list4.children)) if (!keep.has(child)) child.remove();
|
|
113
|
+
list4.scrollTop = scrollTop;
|
|
114
|
+
lists.set(list4, next);
|
|
115
|
+
}
|
|
116
|
+
async function busyButton(button2, operation) {
|
|
117
|
+
if (button2.disabled) return;
|
|
118
|
+
button2.disabled = true;
|
|
119
|
+
button2.setAttribute("aria-busy", "true");
|
|
9
120
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
121
|
+
await operation();
|
|
122
|
+
} finally {
|
|
123
|
+
button2.disabled = false;
|
|
124
|
+
button2.removeAttribute("aria-busy");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// extension/src/client-connections.ts
|
|
129
|
+
var list = document.getElementById("client-connections");
|
|
130
|
+
var note = document.getElementById("client-note");
|
|
131
|
+
var count = document.getElementById("client-count");
|
|
132
|
+
function appName(reported) {
|
|
133
|
+
const name = reported.replace(/\s+v?\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/, "");
|
|
134
|
+
return name === "codex-mcp-client" ? "Codex" : name;
|
|
135
|
+
}
|
|
136
|
+
function accessStatus(client, now) {
|
|
137
|
+
if (client.state === "revoked") return "Access removed";
|
|
138
|
+
if (client.expiresAt <= now) return "Access expired";
|
|
139
|
+
if (client.state === "pending") return "Waiting for your approval";
|
|
140
|
+
const minutes = Math.ceil((client.expiresAt - now) / 6e4);
|
|
141
|
+
const hours = Math.floor(minutes / 60);
|
|
142
|
+
const remaining = hours > 0 ? `${hours} ${hours === 1 ? "hour" : "hours"}` : `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
143
|
+
return `Access allowed \xB7 ${remaining} left`;
|
|
144
|
+
}
|
|
145
|
+
function action(label, id, allow) {
|
|
146
|
+
const button2 = document.createElement("button");
|
|
147
|
+
button2.textContent = label;
|
|
148
|
+
button2.addEventListener("click", async (event) => {
|
|
149
|
+
if (!event.isTrusted) return;
|
|
150
|
+
button2.disabled = true;
|
|
151
|
+
try {
|
|
152
|
+
const result = await chrome.runtime.sendMessage({ type: "client_decision", id, allow });
|
|
153
|
+
note.textContent = result?.ok ? "" : result?.error ?? "Could not save the decision.";
|
|
154
|
+
note.dataset.error = String(!result?.ok);
|
|
155
|
+
} catch {
|
|
156
|
+
note.textContent = "Could not reach the extension. Reopen this page and try again.";
|
|
157
|
+
note.dataset.error = "true";
|
|
158
|
+
} finally {
|
|
159
|
+
button2.disabled = false;
|
|
20
160
|
}
|
|
21
|
-
|
|
161
|
+
await render();
|
|
162
|
+
});
|
|
163
|
+
return button2;
|
|
164
|
+
}
|
|
165
|
+
var renderGeneration = 0;
|
|
166
|
+
async function render() {
|
|
167
|
+
const generation = ++renderGeneration;
|
|
168
|
+
const state = await chrome.storage.local.get([CLIENT_STORAGE_KEY, ACCESS_STOPPED_KEY]);
|
|
169
|
+
if (generation !== renderGeneration) return;
|
|
170
|
+
const saved = state[CLIENT_STORAGE_KEY];
|
|
171
|
+
const stopped = state[ACCESS_STOPPED_KEY] !== void 0 && state[ACCESS_STOPPED_KEY] !== false;
|
|
172
|
+
const clients = Array.isArray(saved) ? saved : [];
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const pending2 = (client) => client.state === "pending" && client.expiresAt > now;
|
|
175
|
+
const waiting = clients.filter(pending2).length;
|
|
176
|
+
for (const id of ["client-help", "client-limits"]) {
|
|
177
|
+
const hint = document.getElementById(id);
|
|
178
|
+
if (hint) hint.hidden = clients.length === 0 || id === "client-help" && document.body.dataset.view === "popup" && waiting === 0;
|
|
179
|
+
}
|
|
180
|
+
if (count) {
|
|
181
|
+
count.textContent = waiting ? `${waiting} waiting` : `${clients.length} ${clients.length === 1 ? "connection" : "connections"}`;
|
|
182
|
+
count.dataset.pending = String(waiting > 0);
|
|
22
183
|
}
|
|
184
|
+
const sorted = clients.slice().sort(
|
|
185
|
+
(a, b) => Number(pending2(b)) - Number(pending2(a)) || b.requestedAt - a.requestedAt
|
|
186
|
+
);
|
|
187
|
+
renderList(
|
|
188
|
+
list,
|
|
189
|
+
sorted,
|
|
190
|
+
(client) => client.id,
|
|
191
|
+
(client) => JSON.stringify([client.name, client.state, client.expiresAt, client.expiresAt <= now, stopped]),
|
|
192
|
+
(client) => {
|
|
193
|
+
const li = document.createElement("li");
|
|
194
|
+
li.dataset.clientId = client.id;
|
|
195
|
+
const name = document.createElement("strong");
|
|
196
|
+
name.textContent = appName(client.name);
|
|
197
|
+
const key = document.createElement("code");
|
|
198
|
+
key.textContent = `Key ${client.id.slice(0, 12)}`;
|
|
199
|
+
key.title = client.id;
|
|
200
|
+
const status = document.createElement("span");
|
|
201
|
+
status.className = "client-status";
|
|
202
|
+
const expired = client.expiresAt <= now;
|
|
203
|
+
li.dataset.state = client.state === "revoked" ? "revoked" : expired ? "expired" : client.state;
|
|
204
|
+
status.textContent = accessStatus(client, Date.now());
|
|
205
|
+
const info = document.createElement("div");
|
|
206
|
+
info.className = "client-info";
|
|
207
|
+
info.append(name, status);
|
|
208
|
+
const needsApproval = client.state === "revoked" || client.state === "pending" && !expired;
|
|
209
|
+
if (needsApproval) info.append(key);
|
|
210
|
+
const details = document.createElement("details");
|
|
211
|
+
details.className = "client-details";
|
|
212
|
+
const summary = document.createElement("summary");
|
|
213
|
+
summary.textContent = "Details";
|
|
214
|
+
details.append(summary);
|
|
215
|
+
for (const [field, text] of [
|
|
216
|
+
["app", `Reported app: ${client.name}`],
|
|
217
|
+
["requested", `Requested: ${new Date(client.requestedAt).toLocaleString()}`],
|
|
218
|
+
...client.state === "approved" ? [["expires", `Access ends: ${new Date(client.expiresAt).toLocaleString()}`]] : []
|
|
219
|
+
]) {
|
|
220
|
+
const line = document.createElement("p");
|
|
221
|
+
line.className = `client-${field}`;
|
|
222
|
+
line.textContent = text;
|
|
223
|
+
details.append(line);
|
|
224
|
+
}
|
|
225
|
+
if (!needsApproval) details.append(key);
|
|
226
|
+
const controls = document.createElement("div");
|
|
227
|
+
controls.className = "client-actions";
|
|
228
|
+
if (client.state === "revoked") controls.append(action("Approve again for 8 hours", client.id, true));
|
|
229
|
+
else if (client.state === "pending" && !expired) {
|
|
230
|
+
const approve = action("Approve for 8 hours", client.id, true);
|
|
231
|
+
approve.className = "primary";
|
|
232
|
+
controls.append(approve, action("Deny", client.id, false));
|
|
233
|
+
} else if (client.state === "approved" && !expired) controls.append(action("Remove access", client.id, false));
|
|
234
|
+
if (stopped) controls.querySelectorAll("button").forEach((button2) => {
|
|
235
|
+
button2.disabled = true;
|
|
236
|
+
});
|
|
237
|
+
li.append(info, controls, details);
|
|
238
|
+
return li;
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
const byId = new Map(clients.map((client) => [client.id, client]));
|
|
242
|
+
for (const row of Array.from(list.querySelectorAll("[data-client-id]"))) {
|
|
243
|
+
const client = byId.get(row.dataset.clientId);
|
|
244
|
+
const status = row.querySelector(".client-status");
|
|
245
|
+
if (client && status) {
|
|
246
|
+
const label = accessStatus(client, now);
|
|
247
|
+
if (status.textContent !== label) status.textContent = label;
|
|
248
|
+
const requested = row.querySelector(".client-requested");
|
|
249
|
+
const requestTime = `Requested: ${new Date(client.requestedAt).toLocaleString()}`;
|
|
250
|
+
if (requested && requested.textContent !== requestTime) requested.textContent = requestTime;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (clients.length && note.textContent?.startsWith("No requests yet.")) note.textContent = "";
|
|
254
|
+
if (!clients.length) note.textContent = "No requests yet. Ask your app to use Chrome. Its request will appear here.";
|
|
255
|
+
if (stopped) note.textContent = "Resume connection above to approve apps.";
|
|
256
|
+
else if (note.textContent === "Resume connection above to approve apps.") note.textContent = "";
|
|
257
|
+
}
|
|
258
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
259
|
+
if (area === "local" && (changes[CLIENT_STORAGE_KEY] || changes[ACCESS_STOPPED_KEY])) void render();
|
|
260
|
+
});
|
|
261
|
+
void render();
|
|
262
|
+
setInterval(() => void render(), 3e4);
|
|
263
|
+
|
|
264
|
+
// extension/src/activity-dashboard.ts
|
|
265
|
+
var stop = document.getElementById("access-stop");
|
|
266
|
+
var resume = document.getElementById("access-resume");
|
|
267
|
+
var accessNote = document.getElementById("access-note");
|
|
268
|
+
var historyNote = document.getElementById("activity-note");
|
|
269
|
+
var list2 = document.getElementById("activity-list");
|
|
270
|
+
var filter = document.getElementById("activity-filter");
|
|
271
|
+
var popup = document.body.dataset.view === "popup";
|
|
272
|
+
var accessGeneration = 0;
|
|
273
|
+
var historyGeneration = 0;
|
|
274
|
+
async function renderAccess() {
|
|
275
|
+
const generation = ++accessGeneration;
|
|
276
|
+
try {
|
|
277
|
+
const saved = await chrome.storage.local.get(ACCESS_STOPPED_KEY);
|
|
278
|
+
if (generation !== accessGeneration) return;
|
|
279
|
+
const stopped = saved[ACCESS_STOPPED_KEY] !== void 0 && saved[ACCESS_STOPPED_KEY] !== false;
|
|
280
|
+
stop.hidden = stopped && accessNote.dataset.error !== "true";
|
|
281
|
+
resume.hidden = !stopped;
|
|
282
|
+
document.getElementById("connect").hidden = stopped;
|
|
283
|
+
if (accessNote.dataset.error !== "true") accessNote.textContent = stopped ? "All apps are disconnected. Resume, then approve your app again." : "Stop disconnects all apps. An action already running may finish.";
|
|
284
|
+
} catch {
|
|
285
|
+
accessNote.textContent = "Could not read access settings. Reopen this page to try again.";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
var OUTCOMES = {
|
|
289
|
+
running: "Running",
|
|
290
|
+
done: "Done",
|
|
291
|
+
blocked: "Blocked",
|
|
292
|
+
failed: "Failed",
|
|
293
|
+
interrupted: "Interrupted"
|
|
294
|
+
};
|
|
295
|
+
async function renderHistory() {
|
|
296
|
+
const generation = ++historyGeneration;
|
|
297
|
+
try {
|
|
298
|
+
const saved = await chrome.storage.session.get([ACTIVITY_KEY, "activityError"]);
|
|
299
|
+
if (generation !== historyGeneration) return;
|
|
300
|
+
const entries = Array.isArray(saved[ACTIVITY_KEY]) ? saved[ACTIVITY_KEY] : [];
|
|
301
|
+
const selected = filter?.value ?? "all";
|
|
302
|
+
const visible = activityRows(entries).filter((e) => selected === "all" || e.outcome === selected).reverse().slice(0, popup ? 5 : 100);
|
|
303
|
+
renderList(list2, visible, (entry) => entry.opened?.id ?? entry.id, (entry) => JSON.stringify(entry), (entry) => {
|
|
304
|
+
const item = document.createElement("li");
|
|
305
|
+
item.className = "activity-item";
|
|
306
|
+
item.dataset.outcome = entry.outcome;
|
|
307
|
+
const details = document.createElement("details");
|
|
308
|
+
const heading = document.createElement("summary");
|
|
309
|
+
heading.className = "activity-row";
|
|
310
|
+
const host = document.createElement("span");
|
|
311
|
+
host.className = "activity-host";
|
|
312
|
+
host.textContent = entry.host || (entry.method === "tabs_list" ? "All tabs" : entry.method === "proxy_status" ? "Proxy" : "No website");
|
|
313
|
+
host.title = host.textContent;
|
|
314
|
+
const label = document.createElement("span");
|
|
315
|
+
label.className = "activity-label";
|
|
316
|
+
const completedTab = entry.outcome === "done" && (entry.method === "tab_new" || entry.method === "tab_close");
|
|
317
|
+
label.textContent = entry.opened ? "Opened \u2192 Closed" : completedTab ? entry.method === "tab_new" ? "Opened" : "Closed" : activityLabel(entry.method);
|
|
318
|
+
const outcome = document.createElement("span");
|
|
319
|
+
outcome.className = "activity-outcome";
|
|
320
|
+
outcome.textContent = OUTCOMES[entry.outcome] ?? "Unknown";
|
|
321
|
+
outcome.hidden = completedTab;
|
|
322
|
+
heading.append(host, label, outcome);
|
|
323
|
+
const meta = document.createElement("div");
|
|
324
|
+
meta.className = "activity-meta";
|
|
325
|
+
const app = document.createElement("span");
|
|
326
|
+
app.textContent = entry.key ? `${entry.app} \xB7 ${entry.key.slice(0, 12)}` : "Unverified app";
|
|
327
|
+
const time = document.createElement("time");
|
|
328
|
+
time.dateTime = new Date(entry.at).toISOString();
|
|
329
|
+
time.textContent = new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
330
|
+
time.title = new Date(entry.at).toLocaleString();
|
|
331
|
+
meta.append(app, time);
|
|
332
|
+
if (entry.opened) {
|
|
333
|
+
const opened = document.createElement("span");
|
|
334
|
+
opened.className = "activity-opened";
|
|
335
|
+
opened.textContent = `Opened ${new Date(entry.opened.at).toLocaleString()} \xB7 Closed ${new Date(entry.at).toLocaleString()}`;
|
|
336
|
+
meta.append(opened);
|
|
337
|
+
}
|
|
338
|
+
details.append(heading, meta);
|
|
339
|
+
item.append(details);
|
|
340
|
+
return item;
|
|
341
|
+
});
|
|
342
|
+
historyNote.textContent = saved.activityError ? "Some activity could not be saved. This history may be incomplete." : visible.length ? popup ? `Latest ${visible.length} ${visible.length === 1 ? "activity" : "activities"}.` : `${visible.length} activities shown.` : selected === "all" ? "No activity yet." : "No matching requests.";
|
|
343
|
+
document.getElementById("activity-clear").disabled = entries.length === 0 && !saved.activityError;
|
|
344
|
+
} catch {
|
|
345
|
+
historyNote.textContent = "Activity is unavailable. Reopen this page to try again.";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function control(id, type, note3) {
|
|
349
|
+
const button2 = document.getElementById(id);
|
|
350
|
+
button2.addEventListener("click", async (event) => {
|
|
351
|
+
if (!event.isTrusted) return;
|
|
352
|
+
button2.disabled = true;
|
|
353
|
+
try {
|
|
354
|
+
const result = await chrome.runtime.sendMessage({ type });
|
|
355
|
+
note3.dataset.error = String(!result?.ok);
|
|
356
|
+
note3.textContent = result?.ok ? "" : result?.error ?? "Could not save this change. Please try again.";
|
|
357
|
+
} catch {
|
|
358
|
+
note3.dataset.error = "true";
|
|
359
|
+
note3.textContent = "Could not reach the extension. Reopen this page and try again.";
|
|
360
|
+
} finally {
|
|
361
|
+
button2.disabled = false;
|
|
362
|
+
}
|
|
363
|
+
if (type !== "activity_clear") await renderAccess();
|
|
364
|
+
else if (note3.dataset.error !== "true") await renderHistory();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
control("access-stop", "access_stop", accessNote);
|
|
368
|
+
control("access-resume", "access_resume", accessNote);
|
|
369
|
+
control("activity-clear", "activity_clear", historyNote);
|
|
370
|
+
document.getElementById("activity-all")?.addEventListener("click", () => {
|
|
371
|
+
void chrome.tabs.create({ url: `${chrome.runtime.getURL("options.html")}#activity-section` });
|
|
372
|
+
});
|
|
373
|
+
filter?.addEventListener("change", () => void renderHistory());
|
|
374
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
375
|
+
if (area === "local" && changes[ACCESS_STOPPED_KEY]) void renderAccess();
|
|
376
|
+
if (area === "session" && (changes[ACTIVITY_KEY] || changes.activityError)) void renderHistory();
|
|
377
|
+
});
|
|
378
|
+
void renderAccess();
|
|
379
|
+
void renderHistory();
|
|
380
|
+
|
|
381
|
+
// shared/policy.ts
|
|
382
|
+
function normalizeDomainPattern(pattern) {
|
|
383
|
+
let p = pattern.trim().toLowerCase();
|
|
384
|
+
if (p === "*" || p === "*://*/*" || p === "<all_urls>") return "";
|
|
385
|
+
p = p.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
386
|
+
p = p.replace(/[/?#].*$/, "");
|
|
387
|
+
p = p.replace(/^[^@]*@/, "").replace(/:\d+$/, "");
|
|
388
|
+
return p === "*" || p === "*." ? "" : p;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// shared/browser-permissions.ts
|
|
392
|
+
function siteOrigins(input) {
|
|
393
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(input) && !/^https?:\/\//i.test(input)) return [];
|
|
394
|
+
const pattern = normalizeDomainPattern(input);
|
|
395
|
+
const host = pattern.startsWith("*.") ? pattern.slice(2) : pattern;
|
|
396
|
+
if (!host || host.length > 253 || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/.test(host)) return [];
|
|
397
|
+
return [`http://${pattern}/*`, `https://${pattern}/*`];
|
|
23
398
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
399
|
+
var OPTIONAL_FEATURES = {
|
|
400
|
+
cookies: { label: "Cookies", explanation: "Read cookies on websites you have allowed. Cookie values still follow your server settings." },
|
|
401
|
+
downloads: { label: "Downloads", explanation: "Save files from websites you have allowed." }
|
|
402
|
+
};
|
|
403
|
+
var FEATURE_REQUESTS_KEY = "browserPermissionRequests";
|
|
404
|
+
var PROXY_AUTH_PERMISSIONS = ["webRequest", "webRequestAuthProvider"];
|
|
405
|
+
var IP_LOOKUP_ORIGINS = ["https://ipinfo.io/*", "https://api.ipify.org/*"];
|
|
406
|
+
|
|
407
|
+
// extension/src/extra-permissions.ts
|
|
408
|
+
var section = document.getElementById("extra-permissions");
|
|
409
|
+
var list3 = document.getElementById("extra-permissions-list");
|
|
410
|
+
var note2 = document.getElementById("extra-permissions-note");
|
|
411
|
+
var rendering = 0;
|
|
412
|
+
async function render2() {
|
|
413
|
+
if (!section || !list3 || !note2) return;
|
|
414
|
+
const generation = ++rendering;
|
|
415
|
+
const saved = (await chrome.storage.local.get(FEATURE_REQUESTS_KEY))[FEATURE_REQUESTS_KEY];
|
|
416
|
+
const pending2 = Array.isArray(saved) ? saved : [];
|
|
417
|
+
const features = [];
|
|
418
|
+
for (const feature of Object.keys(OPTIONAL_FEATURES)) {
|
|
419
|
+
const granted = await chrome.permissions.contains({ permissions: [feature] }).catch(() => false);
|
|
420
|
+
if (!granted && !pending2.includes(feature)) continue;
|
|
421
|
+
features.push({ feature, granted });
|
|
422
|
+
}
|
|
423
|
+
if (generation !== rendering) return;
|
|
424
|
+
renderList(list3, features, ({ feature }) => feature, ({ granted }) => String(granted), ({ feature, granted }) => {
|
|
425
|
+
const { label, explanation } = OPTIONAL_FEATURES[feature];
|
|
426
|
+
const row = document.createElement("li");
|
|
427
|
+
row.className = "client-row";
|
|
428
|
+
row.dataset.feature = feature;
|
|
429
|
+
const title = document.createElement("strong");
|
|
430
|
+
title.textContent = label;
|
|
431
|
+
const description = document.createElement("p");
|
|
432
|
+
description.className = "note hint";
|
|
433
|
+
description.textContent = explanation;
|
|
434
|
+
const button2 = document.createElement("button");
|
|
435
|
+
button2.className = granted ? "ghost" : "primary";
|
|
436
|
+
button2.textContent = granted ? `Remove ${label.toLowerCase()}` : `Allow ${label.toLowerCase()}`;
|
|
437
|
+
button2.addEventListener("click", async (event) => {
|
|
438
|
+
if (!event.isTrusted) return;
|
|
439
|
+
try {
|
|
440
|
+
await busyButton(button2, async () => {
|
|
441
|
+
const result = await chrome.runtime.sendMessage({ type: "feature_access", feature, allow: !granted });
|
|
442
|
+
note2.textContent = result?.ok ? "" : result?.error ?? "Could not change this permission. Please try again.";
|
|
443
|
+
});
|
|
444
|
+
} catch {
|
|
445
|
+
note2.textContent = "Could not change this permission. Please try again.";
|
|
446
|
+
}
|
|
447
|
+
await render2();
|
|
448
|
+
});
|
|
449
|
+
row.append(title, description, button2);
|
|
450
|
+
return row;
|
|
31
451
|
});
|
|
452
|
+
section.hidden = features.length === 0;
|
|
32
453
|
}
|
|
33
|
-
|
|
34
|
-
|
|
454
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
455
|
+
if (area === "local" && changes[FEATURE_REQUESTS_KEY]) void render2();
|
|
456
|
+
});
|
|
457
|
+
chrome.permissions.onAdded.addListener(() => void render2());
|
|
458
|
+
chrome.permissions.onRemoved.addListener(() => void render2());
|
|
459
|
+
void render2();
|
|
460
|
+
|
|
461
|
+
// extension/src/site-access.ts
|
|
462
|
+
async function permittedSites(sites) {
|
|
463
|
+
const checks = await Promise.all(sites.map(async (site) => {
|
|
464
|
+
const origins = siteOrigins(site);
|
|
465
|
+
return origins.length && await chrome.permissions.contains({ origins }).catch(() => false) ? site : null;
|
|
466
|
+
}));
|
|
467
|
+
return checks.filter((site) => site !== null);
|
|
468
|
+
}
|
|
469
|
+
async function changeSiteAccess(input, allow) {
|
|
470
|
+
const origins = siteOrigins(input);
|
|
471
|
+
if (!origins.length) return { ok: false, error: "Please enter one website, such as example.com. All-site access is not available." };
|
|
35
472
|
try {
|
|
36
|
-
|
|
473
|
+
const result = await chrome.runtime.sendMessage({
|
|
474
|
+
type: "site_access",
|
|
475
|
+
host: normalizeDomainPattern(input),
|
|
476
|
+
allow
|
|
477
|
+
});
|
|
478
|
+
return result ?? { ok: false, error: "Please start your connected app and try again." };
|
|
37
479
|
} catch (err) {
|
|
38
|
-
return { ok: false,
|
|
480
|
+
return { ok: false, error: err instanceof Error ? err.message : "Could not change this website permission. Please try again." };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// extension/src/connect.ts
|
|
485
|
+
async function connectWithOneClick() {
|
|
486
|
+
try {
|
|
487
|
+
return await chrome.runtime.sendMessage({ type: "connect_with_click" }) ?? { ok: false, message: "Could not reach the extension." };
|
|
488
|
+
} catch {
|
|
489
|
+
return { ok: false, message: "Could not reach the extension. Reopen the popup and try again." };
|
|
39
490
|
}
|
|
40
|
-
if (!granted) return { ok: false, message: "Chrome needs that permission to fetch the pairing for you. You can also pair by hand in Settings." };
|
|
41
|
-
const reply = await fetchPairing();
|
|
42
|
-
if (!reply.ok) return { ok: false, message: reply.message };
|
|
43
|
-
await applyPairing(reply);
|
|
44
|
-
await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
|
|
45
|
-
return { ok: true };
|
|
46
491
|
}
|
|
47
492
|
|
|
48
493
|
// shared/proxy.ts
|
|
@@ -107,12 +552,13 @@
|
|
|
107
552
|
sourceEl.textContent = pairingSource === "auto" ? "Paired automatically from the pairing.json the server wrote into this extension folder. Saving here overrides it." : pairingSource === "manual" ? "Paired by hand. Saved values take precedence over the bundled pairing.json." : typeof wsPort === "number" && wsPort > 0 && connState !== "idle" ? "Paired with values saved by an earlier version. Saving here keeps them manual." : "Not paired yet. If you loaded this extension from the MCP Browser Extension package folder, start the server once and it pairs itself; otherwise paste the values below.";
|
|
108
553
|
portEl.value = typeof wsPort === "number" && wsPort > 0 ? String(wsPort) : String(DEFAULT_WS_PORT);
|
|
109
554
|
profileEl.value = typeof profile === "string" ? profile : "";
|
|
110
|
-
|
|
555
|
+
render3(typeof connState === "string" ? connState : "idle");
|
|
111
556
|
}
|
|
112
557
|
var quickEl = document.getElementById("quick");
|
|
113
558
|
var connectEl = document.getElementById("connect");
|
|
114
559
|
var connectMsgEl = document.getElementById("connect-msg");
|
|
115
|
-
connectEl.addEventListener("click", async () => {
|
|
560
|
+
connectEl.addEventListener("click", async (event) => {
|
|
561
|
+
if (!event.isTrusted) return;
|
|
116
562
|
connectEl.disabled = true;
|
|
117
563
|
connectEl.textContent = "Connecting\u2026";
|
|
118
564
|
const res = await connectWithOneClick();
|
|
@@ -121,19 +567,23 @@
|
|
|
121
567
|
connectMsgEl.className = res.ok ? "hint" : "hint error";
|
|
122
568
|
connectMsgEl.textContent = res.ok ? "Connecting\u2026" : res.message ?? "Could not connect.";
|
|
123
569
|
});
|
|
124
|
-
function
|
|
570
|
+
function render3(state) {
|
|
125
571
|
quickEl.hidden = false;
|
|
126
572
|
if (!connectEl.disabled) connectEl.textContent = state === "connected" ? "Connect again" : "Connect";
|
|
127
573
|
const labels = {
|
|
128
|
-
connected: "
|
|
574
|
+
connected: "connected",
|
|
129
575
|
connecting: "\u2026 connecting",
|
|
130
|
-
unauthorized: "
|
|
131
|
-
superseded: "
|
|
576
|
+
unauthorized: "rejected (bad/stale token \u2014 re-paste)",
|
|
577
|
+
superseded: "another browser connected under the same profile name. Type a different Profile name above and Save & connect.",
|
|
132
578
|
idle: "\u25CB not connected"
|
|
133
579
|
};
|
|
134
580
|
statusEl.textContent = `Status: ${labels[state] ?? state}`;
|
|
581
|
+
void chrome.storage.local.get(ACCESS_STOPPED_KEY).then((saved) => {
|
|
582
|
+
if (saved[ACCESS_STOPPED_KEY]) statusEl.textContent = "Status: access stopped";
|
|
583
|
+
});
|
|
135
584
|
}
|
|
136
|
-
saveEl.addEventListener("click", async () => {
|
|
585
|
+
saveEl.addEventListener("click", async (event) => {
|
|
586
|
+
if (!event.isTrusted) return;
|
|
137
587
|
const wsPort = Number(portEl.value);
|
|
138
588
|
const stored = await chrome.storage.local.get("token");
|
|
139
589
|
const token = tokenEl.value.trim() || (typeof stored.token === "string" ? stored.token : "");
|
|
@@ -143,8 +593,8 @@
|
|
|
143
593
|
return;
|
|
144
594
|
}
|
|
145
595
|
await chrome.storage.local.set({ wsPort, token, profile, pairingSource: "manual" });
|
|
146
|
-
await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
|
|
147
|
-
statusEl.textContent = "Status: \u2026 connecting";
|
|
596
|
+
const result = await chrome.runtime.sendMessage({ type: "reconnect" }).catch(() => void 0);
|
|
597
|
+
statusEl.textContent = result?.ok === false ? `Status: ${result.error}` : "Status: \u2026 connecting";
|
|
148
598
|
});
|
|
149
599
|
var blockedWrapEl = document.getElementById("blocked-wrap");
|
|
150
600
|
var blockedEl = document.getElementById("blocked");
|
|
@@ -153,13 +603,18 @@
|
|
|
153
603
|
var newSiteEl = document.getElementById("new-site");
|
|
154
604
|
var addSiteEl = document.getElementById("add-site");
|
|
155
605
|
async function grant(host, allow) {
|
|
156
|
-
const res = await
|
|
157
|
-
|
|
606
|
+
const res = await changeSiteAccess(host, allow);
|
|
607
|
+
await renderSites();
|
|
608
|
+
sitesNoteEl.textContent = res.ok ? "" : res.error ?? "Could not change website access. Please try again.";
|
|
158
609
|
}
|
|
159
610
|
function button(label, onClick) {
|
|
160
611
|
const b = document.createElement("button");
|
|
161
612
|
b.textContent = label;
|
|
162
|
-
b.addEventListener("click",
|
|
613
|
+
b.addEventListener("click", (event) => {
|
|
614
|
+
if (event.isTrusted) void busyButton(b, onClick).catch(() => {
|
|
615
|
+
sitesNoteEl.textContent = "Could not finish that action. Please try again.";
|
|
616
|
+
});
|
|
617
|
+
});
|
|
163
618
|
return b;
|
|
164
619
|
}
|
|
165
620
|
function siteRow(host, extra) {
|
|
@@ -175,36 +630,46 @@
|
|
|
175
630
|
t.textContent = text;
|
|
176
631
|
return t;
|
|
177
632
|
}
|
|
633
|
+
var sitesGeneration = 0;
|
|
178
634
|
async function renderSites() {
|
|
179
|
-
const
|
|
635
|
+
const generation = ++sitesGeneration;
|
|
636
|
+
const { allowedSites, grantedSites, blockedSites, dismissedSites, connState } = await chrome.storage.local.get([
|
|
180
637
|
"allowedSites",
|
|
181
638
|
"grantedSites",
|
|
182
639
|
"blockedSites",
|
|
640
|
+
"dismissedSites",
|
|
183
641
|
"connState"
|
|
184
642
|
]);
|
|
185
|
-
const allowed = Array.isArray(allowedSites) ? allowedSites : [];
|
|
643
|
+
const allowed = Array.isArray(allowedSites) ? allowedSites.map(normalizeDomainPattern).filter(Boolean) : [];
|
|
186
644
|
const granted = Array.isArray(grantedSites) ? grantedSites : null;
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
645
|
+
const permitted = await permittedSites(allowed);
|
|
646
|
+
if (generation !== sitesGeneration) return;
|
|
647
|
+
const pending2 = Array.isArray(blockedSites) ? blockedSites : [];
|
|
648
|
+
const blocked = [.../* @__PURE__ */ new Set([...pending2.map((b) => b.host), ...allowed.filter((site) => !permitted.includes(site))])].filter((host) => !permitted.includes(host) && (!Array.isArray(dismissedSites) || !dismissedSites.includes(host)));
|
|
649
|
+
renderList(
|
|
650
|
+
blockedEl,
|
|
651
|
+
blocked,
|
|
652
|
+
(host) => host,
|
|
653
|
+
(host) => host,
|
|
654
|
+
(host) => siteRow(host, [
|
|
655
|
+
tag("needs permission"),
|
|
656
|
+
button("Allow", () => grant(host, true)),
|
|
657
|
+
button("Dismiss", () => chrome.runtime.sendMessage({ type: "dismiss_blocked", host }))
|
|
658
|
+
])
|
|
196
659
|
);
|
|
197
|
-
blockedWrapEl.hidden = blocked.length === 0 ||
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
660
|
+
blockedWrapEl.hidden = blocked.length === 0 || connState !== "connected";
|
|
661
|
+
renderList(
|
|
662
|
+
allowedEl,
|
|
663
|
+
permitted,
|
|
664
|
+
(host) => host,
|
|
665
|
+
(host) => JSON.stringify([host, granted?.includes(host)]),
|
|
666
|
+
(host) => siteRow(host, [...granted?.includes(host) ? [] : [tag("set by server flags")], button("Remove", () => grant(host, false))])
|
|
202
667
|
);
|
|
203
668
|
if (connState !== "connected") {
|
|
204
669
|
sitesNoteEl.textContent = "Connect to the MCP Browser Extension server to see and change the allowed sites.";
|
|
205
670
|
} else if (granted === null) {
|
|
206
671
|
sitesNoteEl.textContent = "This MCP Browser Extension server does not take sites from here (it is older, or runs with --no-site-grants). Use --allow-domain instead.";
|
|
207
|
-
} else if (
|
|
672
|
+
} else if (permitted.length === 0) {
|
|
208
673
|
sitesNoteEl.textContent = "No sites are allowed yet.";
|
|
209
674
|
} else {
|
|
210
675
|
sitesNoteEl.textContent = "";
|
|
@@ -213,23 +678,33 @@
|
|
|
213
678
|
addSiteEl.disabled = !canGrant;
|
|
214
679
|
newSiteEl.disabled = !canGrant;
|
|
215
680
|
}
|
|
216
|
-
|
|
681
|
+
function addSite(event) {
|
|
682
|
+
if (!event.isTrusted) return;
|
|
217
683
|
const host = newSiteEl.value.trim();
|
|
218
684
|
if (!host) return;
|
|
219
685
|
newSiteEl.value = "";
|
|
220
686
|
void grant(host, true);
|
|
221
|
-
}
|
|
687
|
+
}
|
|
688
|
+
addSiteEl.addEventListener("click", addSite);
|
|
222
689
|
newSiteEl.addEventListener("keydown", (e) => {
|
|
223
|
-
if (e.key === "Enter")
|
|
690
|
+
if (e.key === "Enter") {
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
addSite(e);
|
|
693
|
+
}
|
|
224
694
|
});
|
|
225
695
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
226
|
-
if (area === "local" && changes
|
|
696
|
+
if (area === "local" && changes[ACCESS_STOPPED_KEY]) {
|
|
697
|
+
void chrome.storage.local.get("connState").then((saved) => render3(String(saved.connState ?? "idle")));
|
|
698
|
+
}
|
|
699
|
+
if (area === "local" && changes.connState) render3(String(changes.connState.newValue));
|
|
227
700
|
if (area === "local" && changes.pairedProfile) renderPairedAs(changes.pairedProfile.newValue);
|
|
228
|
-
if (area === "local" && (changes.allowedSites || changes.grantedSites || changes.blockedSites || changes.connState)) {
|
|
701
|
+
if (area === "local" && (changes.allowedSites || changes.grantedSites || changes.blockedSites || changes.dismissedSites || changes.connState)) {
|
|
229
702
|
void renderSites();
|
|
230
703
|
}
|
|
231
704
|
});
|
|
232
705
|
void renderSites();
|
|
706
|
+
chrome.permissions.onAdded.addListener(() => void renderSites());
|
|
707
|
+
chrome.permissions.onRemoved.addListener(() => void renderSites());
|
|
233
708
|
void loadExisting();
|
|
234
709
|
var proxyEnabledEl = document.getElementById("proxy-enabled");
|
|
235
710
|
var proxySchemeEl = document.getElementById("proxy-scheme");
|
|
@@ -269,7 +744,8 @@
|
|
|
269
744
|
}
|
|
270
745
|
proxyHostEl.addEventListener("change", splitPastedProxy);
|
|
271
746
|
proxyHostEl.addEventListener("paste", () => setTimeout(splitPastedProxy, 0));
|
|
272
|
-
proxyWebrtcEl.addEventListener("change", async () => {
|
|
747
|
+
proxyWebrtcEl.addEventListener("change", async (event) => {
|
|
748
|
+
if (!event.isTrusted) return;
|
|
273
749
|
if (proxyWebrtcEl.checked) {
|
|
274
750
|
proxyWebrtcEl.checked = await chrome.permissions.request({ permissions: ["privacy"] }).catch(() => false);
|
|
275
751
|
} else {
|
|
@@ -278,9 +754,9 @@
|
|
|
278
754
|
}
|
|
279
755
|
await chrome.runtime.sendMessage({ type: "proxy_apply" }).catch(() => void 0);
|
|
280
756
|
});
|
|
281
|
-
proxySaveEl.addEventListener("click", async () => {
|
|
757
|
+
proxySaveEl.addEventListener("click", async (event) => {
|
|
758
|
+
if (!event.isTrusted) return;
|
|
282
759
|
splitPastedProxy();
|
|
283
|
-
const stored = parseProxyConfig((await chrome.storage.local.get(PROXY_STORAGE_KEY))[PROXY_STORAGE_KEY]);
|
|
284
760
|
const host = proxyHostEl.value.trim();
|
|
285
761
|
const port = Number(proxyPortEl.value);
|
|
286
762
|
const enabled = proxyEnabledEl.checked;
|
|
@@ -288,16 +764,28 @@
|
|
|
288
764
|
return proxyNote("Proxy: enter a host and a port (1-65535) first", "err");
|
|
289
765
|
}
|
|
290
766
|
const username = proxyUserEl.value.trim();
|
|
291
|
-
const password = proxyPassEl.value || (stored && stored.username === username ? stored.password : "");
|
|
292
767
|
const scheme = proxySchemeEl.value;
|
|
293
768
|
if (enabled && username && scheme.startsWith("socks")) {
|
|
294
769
|
return proxyNote("Proxy: Chrome cannot log in to a SOCKS proxy. Pick HTTP or HTTPS, or use a proxy without a login.", "err");
|
|
295
770
|
}
|
|
771
|
+
if (enabled && username && !await chrome.permissions.request({ permissions: PROXY_AUTH_PERMISSIONS }).catch(() => false)) {
|
|
772
|
+
return proxyNote("Proxy access was not added. Click Save proxy whenever you are ready.", "err");
|
|
773
|
+
}
|
|
774
|
+
const stored = parseProxyConfig((await chrome.storage.local.get(PROXY_STORAGE_KEY))[PROXY_STORAGE_KEY]);
|
|
775
|
+
const password = proxyPassEl.value || (stored && stored.username === username ? stored.password : "");
|
|
296
776
|
await chrome.storage.local.set({ [PROXY_STORAGE_KEY]: { enabled, scheme, host, port, username, password } });
|
|
777
|
+
if (!enabled) {
|
|
778
|
+
if (await chrome.permissions.contains({ permissions: ["proxy"] })) await chrome.proxy.settings.clear({ scope: "regular" });
|
|
779
|
+
await chrome.permissions.remove({ permissions: PROXY_AUTH_PERMISSIONS });
|
|
780
|
+
}
|
|
297
781
|
proxyPassEl.value = "";
|
|
298
782
|
proxyNote(enabled ? `Proxy: on (${scheme}://${host}:${port}). Click Check IP to confirm.` : "Proxy: off", enabled ? "on" : "");
|
|
299
783
|
});
|
|
300
|
-
proxyCheckEl.addEventListener("click", async () => {
|
|
784
|
+
proxyCheckEl.addEventListener("click", async (event) => {
|
|
785
|
+
if (!event.isTrusted) return;
|
|
786
|
+
if (!await chrome.permissions.request({ origins: IP_LOOKUP_ORIGINS }).catch(() => false)) {
|
|
787
|
+
return proxyNote("IP lookup permission was not added. You can try again whenever you are ready.", "err");
|
|
788
|
+
}
|
|
301
789
|
proxyCheckEl.disabled = true;
|
|
302
790
|
proxyNote("Proxy: checking\u2026");
|
|
303
791
|
const res = await chrome.runtime.sendMessage({ type: "proxy_status" }).catch(() => null);
|