@btraut/browser-bridge 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +6 -0
- package/dist/api.js +12 -2
- package/dist/api.js.map +2 -2
- package/dist/index.js +146 -106
- package/dist/index.js.map +3 -3
- package/extension/assets/ui.css +447 -0
- package/extension/dist/background.js +676 -30
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/options-ui.js +374 -0
- package/extension/dist/options-ui.js.map +7 -0
- package/extension/dist/permission-prompt-ui.js +62 -0
- package/extension/dist/permission-prompt-ui.js.map +7 -0
- package/extension/dist/popup-ui.js +51 -0
- package/extension/dist/popup-ui.js.map +7 -0
- package/extension/manifest.json +8 -3
- package/package.json +1 -1
- package/skills/browser-bridge/SKILL.md +1 -0
- package/skills/browser-bridge/skill.json +1 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// packages/extension/src/site-permissions.ts
|
|
4
|
+
var SITE_ALLOWLIST_KEY = "siteAllowlist";
|
|
5
|
+
var SITE_PERMISSIONS_MODE_KEY = "sitePermissionsMode";
|
|
6
|
+
var DEFAULT_SITE_PERMISSIONS_MODE = "granular";
|
|
7
|
+
var isAllowlistEntry = (value) => {
|
|
8
|
+
if (!value || typeof value !== "object") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const v = value;
|
|
12
|
+
return typeof v.createdAt === "string" && typeof v.lastUsedAt === "string";
|
|
13
|
+
};
|
|
14
|
+
var normalizeSiteKey = (siteKey) => siteKey.toLowerCase();
|
|
15
|
+
var readAllowlistRaw = async () => {
|
|
16
|
+
return await new Promise((resolve) => {
|
|
17
|
+
chrome.storage.local.get(
|
|
18
|
+
[SITE_ALLOWLIST_KEY],
|
|
19
|
+
(result) => {
|
|
20
|
+
const raw = result?.[SITE_ALLOWLIST_KEY];
|
|
21
|
+
if (!raw || typeof raw !== "object") {
|
|
22
|
+
resolve({});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
27
|
+
if (typeof k !== "string") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!isAllowlistEntry(v)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
out[normalizeSiteKey(k)] = v;
|
|
34
|
+
}
|
|
35
|
+
resolve(out);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
var writeAllowlistRaw = async (allowlist) => {
|
|
41
|
+
return await new Promise((resolve) => {
|
|
42
|
+
chrome.storage.local.set(
|
|
43
|
+
{ [SITE_ALLOWLIST_KEY]: allowlist },
|
|
44
|
+
() => resolve()
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
var readSitePermissionsMode = async () => {
|
|
49
|
+
return await new Promise((resolve) => {
|
|
50
|
+
chrome.storage.local.get(
|
|
51
|
+
[SITE_PERMISSIONS_MODE_KEY],
|
|
52
|
+
(result) => {
|
|
53
|
+
const raw = result?.[SITE_PERMISSIONS_MODE_KEY];
|
|
54
|
+
if (raw === "granular" || raw === "bypass") {
|
|
55
|
+
resolve(raw);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
resolve(DEFAULT_SITE_PERMISSIONS_MODE);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
var writeSitePermissionsMode = async (mode) => {
|
|
64
|
+
return await new Promise((resolve) => {
|
|
65
|
+
chrome.storage.local.set(
|
|
66
|
+
{ [SITE_PERMISSIONS_MODE_KEY]: mode },
|
|
67
|
+
() => resolve()
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
var getAllowlistedSites = async () => {
|
|
72
|
+
return await readAllowlistRaw();
|
|
73
|
+
};
|
|
74
|
+
var allowSiteAlways = async (siteKey, now = /* @__PURE__ */ new Date()) => {
|
|
75
|
+
const key = normalizeSiteKey(siteKey);
|
|
76
|
+
const allowlist = await readAllowlistRaw();
|
|
77
|
+
const nowIso = now.toISOString();
|
|
78
|
+
const existing = allowlist[key];
|
|
79
|
+
allowlist[key] = {
|
|
80
|
+
createdAt: existing?.createdAt ?? nowIso,
|
|
81
|
+
lastUsedAt: nowIso
|
|
82
|
+
};
|
|
83
|
+
await writeAllowlistRaw(allowlist);
|
|
84
|
+
};
|
|
85
|
+
var upsertAllowlistedSites = async (entries) => {
|
|
86
|
+
const allowlist = await readAllowlistRaw();
|
|
87
|
+
let changed = false;
|
|
88
|
+
for (const [k, v] of Object.entries(entries ?? {})) {
|
|
89
|
+
if (typeof k !== "string") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!isAllowlistEntry(v)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
allowlist[normalizeSiteKey(k)] = v;
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
if (!changed) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await writeAllowlistRaw(allowlist);
|
|
102
|
+
};
|
|
103
|
+
var revokeSite = async (siteKey) => {
|
|
104
|
+
const key = normalizeSiteKey(siteKey);
|
|
105
|
+
const allowlist = await readAllowlistRaw();
|
|
106
|
+
if (!allowlist[key]) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
delete allowlist[key];
|
|
110
|
+
await writeAllowlistRaw(allowlist);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// packages/extension/src/options-ui.ts
|
|
114
|
+
var byId = (id) => {
|
|
115
|
+
const el = document.getElementById(id);
|
|
116
|
+
if (!el) {
|
|
117
|
+
throw new Error(`Missing element: ${id}`);
|
|
118
|
+
}
|
|
119
|
+
return el;
|
|
120
|
+
};
|
|
121
|
+
var elFromHtml = (html) => {
|
|
122
|
+
const tpl = document.createElement("template");
|
|
123
|
+
tpl.innerHTML = html.trim();
|
|
124
|
+
const node = tpl.content.firstElementChild;
|
|
125
|
+
if (!node) {
|
|
126
|
+
throw new Error("Expected element from template.");
|
|
127
|
+
}
|
|
128
|
+
return node;
|
|
129
|
+
};
|
|
130
|
+
var formatTime = (iso) => {
|
|
131
|
+
const d = new Date(iso);
|
|
132
|
+
if (!Number.isFinite(d.getTime())) {
|
|
133
|
+
return iso;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
137
|
+
year: "numeric",
|
|
138
|
+
month: "numeric",
|
|
139
|
+
day: "numeric",
|
|
140
|
+
hour: "numeric",
|
|
141
|
+
minute: "2-digit"
|
|
142
|
+
}).format(d);
|
|
143
|
+
} catch {
|
|
144
|
+
return iso;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var createToast = () => {
|
|
148
|
+
const wrap = document.createElement("div");
|
|
149
|
+
wrap.className = "bb-toast-wrap";
|
|
150
|
+
document.body.appendChild(wrap);
|
|
151
|
+
let activeTimer = null;
|
|
152
|
+
const clear = () => {
|
|
153
|
+
if (activeTimer !== null) {
|
|
154
|
+
globalThis.clearTimeout(activeTimer);
|
|
155
|
+
activeTimer = null;
|
|
156
|
+
}
|
|
157
|
+
wrap.innerHTML = "";
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
showUndo: ({ message, onUndo }) => {
|
|
161
|
+
clear();
|
|
162
|
+
const toast2 = elFromHtml(`
|
|
163
|
+
<div class="bb-toast" role="status" aria-live="polite">
|
|
164
|
+
<div class="bb-toast-msg"></div>
|
|
165
|
+
<button class="bb-link-button" type="button">Undo</button>
|
|
166
|
+
</div>
|
|
167
|
+
`);
|
|
168
|
+
const msgEl = toast2.querySelector(".bb-toast-msg");
|
|
169
|
+
const undoBtn = toast2.querySelector("button");
|
|
170
|
+
if (!msgEl || !undoBtn) {
|
|
171
|
+
throw new Error("Toast missing required elements.");
|
|
172
|
+
}
|
|
173
|
+
msgEl.textContent = message;
|
|
174
|
+
undoBtn.addEventListener("click", () => {
|
|
175
|
+
undoBtn.disabled = true;
|
|
176
|
+
void (async () => {
|
|
177
|
+
try {
|
|
178
|
+
await onUndo();
|
|
179
|
+
} finally {
|
|
180
|
+
clear();
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
});
|
|
184
|
+
wrap.appendChild(toast2);
|
|
185
|
+
activeTimer = globalThis.setTimeout(() => clear(), 6e3);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
var toast = createToast();
|
|
190
|
+
var getModeEls = () => {
|
|
191
|
+
const granular = byId("bb-mode-granular");
|
|
192
|
+
const bypass = byId("bb-mode-bypass");
|
|
193
|
+
const warning = byId("bb-bypass-warning");
|
|
194
|
+
const sitesDetails = byId("bb-sites-details");
|
|
195
|
+
const sitesSummary = byId("bb-sites-summary");
|
|
196
|
+
const sitesIgnored = byId("bb-sites-ignored");
|
|
197
|
+
if (granular.type !== "radio" || bypass.type !== "radio") {
|
|
198
|
+
throw new Error("Expected radio inputs for permissions mode.");
|
|
199
|
+
}
|
|
200
|
+
if (sitesDetails.tagName.toLowerCase() !== "details") {
|
|
201
|
+
throw new Error("Expected a <details> for the sites disclosure.");
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
granular,
|
|
205
|
+
bypass,
|
|
206
|
+
warning,
|
|
207
|
+
sitesDetails,
|
|
208
|
+
sitesSummary,
|
|
209
|
+
sitesIgnored
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
var lastMode = null;
|
|
213
|
+
var modeWriteInProgress = false;
|
|
214
|
+
var applyMode = (mode) => {
|
|
215
|
+
const els = getModeEls();
|
|
216
|
+
els.granular.checked = mode === "granular";
|
|
217
|
+
els.bypass.checked = mode === "bypass";
|
|
218
|
+
els.warning.hidden = mode !== "bypass";
|
|
219
|
+
els.sitesIgnored.hidden = mode !== "bypass";
|
|
220
|
+
if (mode === "bypass") {
|
|
221
|
+
els.sitesSummary.textContent = "Approved sites (ignored in bypass mode)";
|
|
222
|
+
els.sitesDetails.classList.remove("bb-sites-details--no-summary");
|
|
223
|
+
if (lastMode !== "bypass") {
|
|
224
|
+
els.sitesDetails.open = false;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
els.sitesSummary.textContent = "Approved sites";
|
|
228
|
+
els.sitesDetails.classList.add("bb-sites-details--no-summary");
|
|
229
|
+
if (lastMode !== "granular") {
|
|
230
|
+
els.sitesDetails.open = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
lastMode = mode;
|
|
234
|
+
};
|
|
235
|
+
var refreshMode = async () => {
|
|
236
|
+
applyMode(await readSitePermissionsMode());
|
|
237
|
+
};
|
|
238
|
+
var focusSiteRow = (site) => {
|
|
239
|
+
const container = byId("bb-sites");
|
|
240
|
+
const rows = Array.from(container.querySelectorAll(".bb-site-row"));
|
|
241
|
+
for (const rowEl of rows) {
|
|
242
|
+
const el = rowEl;
|
|
243
|
+
if (el.dataset.site !== site) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
el.scrollIntoView({ block: "nearest" });
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
const btn = el.querySelector("button");
|
|
251
|
+
btn?.focus();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var render = (rows) => {
|
|
256
|
+
const container = byId("bb-sites");
|
|
257
|
+
container.innerHTML = "";
|
|
258
|
+
if (rows.length === 0) {
|
|
259
|
+
const empty = document.createElement("div");
|
|
260
|
+
empty.className = "bb-site-empty";
|
|
261
|
+
empty.textContent = "No approved sites yet.";
|
|
262
|
+
container.appendChild(empty);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (const row of rows) {
|
|
266
|
+
const item = elFromHtml(`
|
|
267
|
+
<div class="bb-site-row" role="listitem">
|
|
268
|
+
<div class="bb-site-main">
|
|
269
|
+
<div class="bb-site-key"></div>
|
|
270
|
+
<div class="bb-site-meta"></div>
|
|
271
|
+
</div>
|
|
272
|
+
<button class="bb-link-button bb-link-button-danger" type="button">
|
|
273
|
+
Revoke
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
`);
|
|
277
|
+
item.dataset.site = row.site;
|
|
278
|
+
const key = item.querySelector(".bb-site-key");
|
|
279
|
+
const meta = item.querySelector(".bb-site-meta");
|
|
280
|
+
const revokeBtn = item.querySelector("button");
|
|
281
|
+
if (!key || !meta || !revokeBtn) {
|
|
282
|
+
throw new Error("List row missing required elements.");
|
|
283
|
+
}
|
|
284
|
+
key.textContent = row.site;
|
|
285
|
+
meta.textContent = `Last used: ${formatTime(row.lastUsedAt)}`;
|
|
286
|
+
meta.title = `Approved: ${formatTime(row.createdAt)}
|
|
287
|
+
Last used: ${formatTime(
|
|
288
|
+
row.lastUsedAt
|
|
289
|
+
)}`;
|
|
290
|
+
revokeBtn.addEventListener("click", () => {
|
|
291
|
+
revokeBtn.disabled = true;
|
|
292
|
+
void (async () => {
|
|
293
|
+
const before = await getAllowlistedSites();
|
|
294
|
+
const entry = before[row.site] ?? before[row.site.toLowerCase()];
|
|
295
|
+
try {
|
|
296
|
+
await revokeSite(row.site);
|
|
297
|
+
} finally {
|
|
298
|
+
await refresh();
|
|
299
|
+
revokeBtn.disabled = false;
|
|
300
|
+
}
|
|
301
|
+
if (entry) {
|
|
302
|
+
toast.showUndo({
|
|
303
|
+
message: `Revoked ${row.site}.`,
|
|
304
|
+
onUndo: async () => {
|
|
305
|
+
try {
|
|
306
|
+
await upsertAllowlistedSites({ [row.site]: entry });
|
|
307
|
+
const after = await getAllowlistedSites();
|
|
308
|
+
if (!after[row.site] && !after[row.site.toLowerCase()]) {
|
|
309
|
+
await allowSiteAlways(row.site);
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.warn(
|
|
313
|
+
"Undo revoke failed; falling back to allowSiteAlways.",
|
|
314
|
+
err
|
|
315
|
+
);
|
|
316
|
+
await allowSiteAlways(row.site);
|
|
317
|
+
}
|
|
318
|
+
await refresh();
|
|
319
|
+
focusSiteRow(row.site);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
324
|
+
});
|
|
325
|
+
container.appendChild(item);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var refresh = async () => {
|
|
329
|
+
const allowlist = await getAllowlistedSites();
|
|
330
|
+
const rows = Object.entries(allowlist).map(([site, entry]) => ({
|
|
331
|
+
site,
|
|
332
|
+
createdAt: entry.createdAt,
|
|
333
|
+
lastUsedAt: entry.lastUsedAt
|
|
334
|
+
}));
|
|
335
|
+
rows.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
336
|
+
render(rows);
|
|
337
|
+
};
|
|
338
|
+
var setMode = async (mode) => {
|
|
339
|
+
if (modeWriteInProgress) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
modeWriteInProgress = true;
|
|
343
|
+
try {
|
|
344
|
+
await writeSitePermissionsMode(mode);
|
|
345
|
+
applyMode(mode);
|
|
346
|
+
} finally {
|
|
347
|
+
modeWriteInProgress = false;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
var refreshAll = async () => {
|
|
351
|
+
await Promise.all([refresh(), refreshMode()]);
|
|
352
|
+
};
|
|
353
|
+
var main = () => {
|
|
354
|
+
void refreshAll();
|
|
355
|
+
const { granular, bypass } = getModeEls();
|
|
356
|
+
granular.addEventListener("change", () => {
|
|
357
|
+
if (!granular.checked) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
void setMode("granular");
|
|
361
|
+
});
|
|
362
|
+
bypass.addEventListener("change", () => {
|
|
363
|
+
if (!bypass.checked) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
void setMode("bypass");
|
|
367
|
+
});
|
|
368
|
+
chrome.storage?.onChanged?.addListener?.(() => {
|
|
369
|
+
void refreshAll();
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
main();
|
|
373
|
+
})();
|
|
374
|
+
//# sourceMappingURL=options-ui.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/site-permissions.ts", "../src/options-ui.ts"],
|
|
4
|
+
"sourcesContent": ["export const SITE_ALLOWLIST_KEY = 'siteAllowlist';\nexport const PERMISSION_PROMPT_WAIT_MS_KEY = 'permissionPromptWaitMs';\nexport const DEFAULT_PERMISSION_PROMPT_WAIT_MS = 30_000;\nexport const SITE_PERMISSIONS_MODE_KEY = 'sitePermissionsMode';\n\nexport type SitePermissionsMode = 'granular' | 'bypass';\nexport const DEFAULT_SITE_PERMISSIONS_MODE: SitePermissionsMode = 'granular';\n\nexport type SiteAllowlistEntry = {\n createdAt: string; // ISO\n lastUsedAt: string; // ISO\n};\n\nexport type SiteAllowlist = Record<string, SiteAllowlistEntry>;\n\nexport const siteKeyFromUrl = (rawUrl: string): string | null => {\n if (!rawUrl || typeof rawUrl !== 'string') {\n return null;\n }\n\n try {\n const parsed = new URL(rawUrl);\n // Only gate \"real web\" pages for now.\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return null;\n }\n\n // URL.hostname is lowercased by the platform.\n if (!parsed.hostname) {\n return null;\n }\n\n return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;\n } catch {\n return null;\n }\n};\n\nconst isAllowlistEntry = (value: unknown): value is SiteAllowlistEntry => {\n if (!value || typeof value !== 'object') {\n return false;\n }\n const v = value as Record<string, unknown>;\n return typeof v.createdAt === 'string' && typeof v.lastUsedAt === 'string';\n};\n\nconst normalizeSiteKey = (siteKey: string): string => siteKey.toLowerCase();\n\nconst readAllowlistRaw = async (): Promise<SiteAllowlist> => {\n return await new Promise<SiteAllowlist>((resolve) => {\n chrome.storage.local.get(\n [SITE_ALLOWLIST_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[SITE_ALLOWLIST_KEY];\n if (!raw || typeof raw !== 'object') {\n resolve({});\n return;\n }\n\n const out: SiteAllowlist = {};\n for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof k !== 'string') {\n continue;\n }\n if (!isAllowlistEntry(v)) {\n continue;\n }\n out[normalizeSiteKey(k)] = v;\n }\n\n resolve(out);\n }\n );\n });\n};\n\nconst writeAllowlistRaw = async (allowlist: SiteAllowlist): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set({ [SITE_ALLOWLIST_KEY]: allowlist }, () =>\n resolve()\n );\n });\n};\n\nexport const readSitePermissionsMode =\n async (): Promise<SitePermissionsMode> => {\n return await new Promise<SitePermissionsMode>((resolve) => {\n chrome.storage.local.get(\n [SITE_PERMISSIONS_MODE_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[SITE_PERMISSIONS_MODE_KEY];\n if (raw === 'granular' || raw === 'bypass') {\n resolve(raw);\n return;\n }\n resolve(DEFAULT_SITE_PERMISSIONS_MODE);\n }\n );\n });\n };\n\nexport const writeSitePermissionsMode = async (\n mode: SitePermissionsMode\n): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set({ [SITE_PERMISSIONS_MODE_KEY]: mode }, () =>\n resolve()\n );\n });\n};\n\nexport const readPermissionPromptWaitMs = async (): Promise<number> => {\n return await new Promise<number>((resolve) => {\n chrome.storage.local.get(\n [PERMISSION_PROMPT_WAIT_MS_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[PERMISSION_PROMPT_WAIT_MS_KEY];\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n resolve(raw);\n return;\n }\n if (typeof raw === 'string') {\n const parsed = Number(raw);\n if (Number.isFinite(parsed) && parsed > 0) {\n resolve(parsed);\n return;\n }\n }\n\n resolve(DEFAULT_PERMISSION_PROMPT_WAIT_MS);\n }\n );\n });\n};\n\nexport const getAllowlistedSites = async (): Promise<SiteAllowlist> => {\n return await readAllowlistRaw();\n};\n\nexport const isSiteAllowed = async (siteKey: string): Promise<boolean> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n return Boolean(allowlist[key]);\n};\n\nexport const allowSiteAlways = async (\n siteKey: string,\n now: Date = new Date()\n): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n const nowIso = now.toISOString();\n\n const existing = allowlist[key];\n allowlist[key] = {\n createdAt: existing?.createdAt ?? nowIso,\n lastUsedAt: nowIso,\n };\n\n await writeAllowlistRaw(allowlist);\n};\n\nexport const upsertAllowlistedSites = async (\n entries: SiteAllowlist\n): Promise<void> => {\n const allowlist = await readAllowlistRaw();\n let changed = false;\n\n for (const [k, v] of Object.entries(entries ?? {})) {\n if (typeof k !== 'string') {\n continue;\n }\n if (!isAllowlistEntry(v)) {\n continue;\n }\n allowlist[normalizeSiteKey(k)] = v;\n changed = true;\n }\n\n if (!changed) {\n return;\n }\n\n await writeAllowlistRaw(allowlist);\n};\n\nexport const touchSiteLastUsed = async (\n siteKey: string,\n now: Date = new Date()\n): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n const existing = allowlist[key];\n if (!existing) {\n return;\n }\n\n allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };\n await writeAllowlistRaw(allowlist);\n};\n\nexport const revokeSite = async (siteKey: string): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n if (!allowlist[key]) {\n return;\n }\n delete allowlist[key];\n await writeAllowlistRaw(allowlist);\n};\n", "import {\n allowSiteAlways,\n getAllowlistedSites,\n readSitePermissionsMode,\n revokeSite,\n type SitePermissionsMode,\n upsertAllowlistedSites,\n writeSitePermissionsMode,\n} from './site-permissions.js';\n\ntype Row = {\n site: string;\n createdAt: string;\n lastUsedAt: string;\n};\n\ntype ModeEls = {\n granular: HTMLInputElement;\n bypass: HTMLInputElement;\n warning: HTMLElement;\n sitesDetails: HTMLDetailsElement;\n sitesSummary: HTMLElement;\n sitesIgnored: HTMLElement;\n};\n\nconst byId = (id: string): HTMLElement => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n return el;\n};\n\nconst elFromHtml = (html: string): HTMLElement => {\n const tpl = document.createElement('template');\n tpl.innerHTML = html.trim();\n const node = tpl.content.firstElementChild;\n if (!node) {\n throw new Error('Expected element from template.');\n }\n return node as HTMLElement;\n};\n\nconst formatTime = (iso: string): string => {\n const d = new Date(iso);\n if (!Number.isFinite(d.getTime())) {\n return iso;\n }\n try {\n return new Intl.DateTimeFormat(undefined, {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n }).format(d);\n } catch {\n return iso;\n }\n};\n\nconst createToast = (): {\n showUndo: (opts: { message: string; onUndo: () => Promise<void> }) => void;\n} => {\n const wrap = document.createElement('div');\n wrap.className = 'bb-toast-wrap';\n document.body.appendChild(wrap);\n\n let activeTimer: ReturnType<typeof globalThis.setTimeout> | null = null;\n\n const clear = (): void => {\n if (activeTimer !== null) {\n globalThis.clearTimeout(activeTimer);\n activeTimer = null;\n }\n wrap.innerHTML = '';\n };\n\n return {\n showUndo: ({ message, onUndo }): void => {\n clear();\n\n const toast = elFromHtml(`\n <div class=\"bb-toast\" role=\"status\" aria-live=\"polite\">\n <div class=\"bb-toast-msg\"></div>\n <button class=\"bb-link-button\" type=\"button\">Undo</button>\n </div>\n `);\n const msgEl = toast.querySelector('.bb-toast-msg') as HTMLElement | null;\n const undoBtn = toast.querySelector('button') as HTMLButtonElement | null;\n if (!msgEl || !undoBtn) {\n throw new Error('Toast missing required elements.');\n }\n\n msgEl.textContent = message;\n undoBtn.addEventListener('click', () => {\n undoBtn.disabled = true;\n void (async () => {\n try {\n await onUndo();\n } finally {\n clear();\n }\n })();\n });\n\n wrap.appendChild(toast);\n activeTimer = globalThis.setTimeout(() => clear(), 6000);\n },\n };\n};\n\nconst toast = createToast();\n\nconst getModeEls = (): ModeEls => {\n const granular = byId('bb-mode-granular') as HTMLInputElement;\n const bypass = byId('bb-mode-bypass') as HTMLInputElement;\n const warning = byId('bb-bypass-warning');\n const sitesDetails = byId('bb-sites-details') as HTMLDetailsElement;\n const sitesSummary = byId('bb-sites-summary');\n const sitesIgnored = byId('bb-sites-ignored');\n\n if (granular.type !== 'radio' || bypass.type !== 'radio') {\n throw new Error('Expected radio inputs for permissions mode.');\n }\n if (sitesDetails.tagName.toLowerCase() !== 'details') {\n throw new Error('Expected a <details> for the sites disclosure.');\n }\n\n return {\n granular,\n bypass,\n warning,\n sitesDetails,\n sitesSummary,\n sitesIgnored,\n };\n};\n\nlet lastMode: SitePermissionsMode | null = null;\nlet modeWriteInProgress = false;\n\nconst applyMode = (mode: SitePermissionsMode): void => {\n const els = getModeEls();\n els.granular.checked = mode === 'granular';\n els.bypass.checked = mode === 'bypass';\n\n els.warning.hidden = mode !== 'bypass';\n els.sitesIgnored.hidden = mode !== 'bypass';\n\n if (mode === 'bypass') {\n els.sitesSummary.textContent = 'Approved sites (ignored in bypass mode)';\n els.sitesDetails.classList.remove('bb-sites-details--no-summary');\n if (lastMode !== 'bypass') {\n els.sitesDetails.open = false;\n }\n } else {\n els.sitesSummary.textContent = 'Approved sites';\n els.sitesDetails.classList.add('bb-sites-details--no-summary');\n if (lastMode !== 'granular') {\n els.sitesDetails.open = true;\n }\n }\n\n lastMode = mode;\n};\n\nconst refreshMode = async (): Promise<void> => {\n applyMode(await readSitePermissionsMode());\n};\n\nconst focusSiteRow = (site: string): void => {\n const container = byId('bb-sites');\n const rows = Array.from(container.querySelectorAll('.bb-site-row'));\n for (const rowEl of rows) {\n const el = rowEl as HTMLElement;\n if (el.dataset.site !== site) {\n continue;\n }\n\n try {\n el.scrollIntoView({ block: 'nearest' });\n } catch {\n // ignore\n }\n\n const btn = el.querySelector('button') as HTMLButtonElement | null;\n btn?.focus();\n return;\n }\n};\n\nconst render = (rows: Row[]): void => {\n const container = byId('bb-sites');\n container.innerHTML = '';\n\n if (rows.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'bb-site-empty';\n empty.textContent = 'No approved sites yet.';\n container.appendChild(empty);\n return;\n }\n\n for (const row of rows) {\n const item = elFromHtml(`\n <div class=\"bb-site-row\" role=\"listitem\">\n <div class=\"bb-site-main\">\n <div class=\"bb-site-key\"></div>\n <div class=\"bb-site-meta\"></div>\n </div>\n <button class=\"bb-link-button bb-link-button-danger\" type=\"button\">\n Revoke\n </button>\n </div>\n `);\n (item as HTMLElement).dataset.site = row.site;\n\n const key = item.querySelector('.bb-site-key') as HTMLElement | null;\n const meta = item.querySelector('.bb-site-meta') as HTMLElement | null;\n const revokeBtn = item.querySelector('button') as HTMLButtonElement | null;\n if (!key || !meta || !revokeBtn) {\n throw new Error('List row missing required elements.');\n }\n\n key.textContent = row.site;\n meta.textContent = `Last used: ${formatTime(row.lastUsedAt)}`;\n meta.title = `Approved: ${formatTime(row.createdAt)}\\nLast used: ${formatTime(\n row.lastUsedAt\n )}`;\n\n revokeBtn.addEventListener('click', () => {\n revokeBtn.disabled = true;\n void (async () => {\n const before = await getAllowlistedSites();\n const entry = before[row.site] ?? before[row.site.toLowerCase()];\n\n try {\n await revokeSite(row.site);\n } finally {\n await refresh();\n revokeBtn.disabled = false;\n }\n\n if (entry) {\n toast.showUndo({\n message: `Revoked ${row.site}.`,\n onUndo: async () => {\n try {\n await upsertAllowlistedSites({ [row.site]: entry });\n const after = await getAllowlistedSites();\n if (!after[row.site] && !after[row.site.toLowerCase()]) {\n await allowSiteAlways(row.site);\n }\n } catch (err) {\n // If restore fails for any reason, fall back to re-adding the site.\n // This preserves the intended user outcome even if timestamps change.\n console.warn(\n 'Undo revoke failed; falling back to allowSiteAlways.',\n err\n );\n await allowSiteAlways(row.site);\n }\n await refresh();\n focusSiteRow(row.site);\n },\n });\n }\n })();\n });\n\n container.appendChild(item);\n }\n};\n\nconst refresh = async (): Promise<void> => {\n const allowlist = await getAllowlistedSites();\n const rows: Row[] = Object.entries(allowlist).map(([site, entry]) => ({\n site,\n createdAt: entry.createdAt,\n lastUsedAt: entry.lastUsedAt,\n }));\n\n rows.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));\n render(rows);\n};\n\nconst setMode = async (mode: SitePermissionsMode): Promise<void> => {\n if (modeWriteInProgress) {\n return;\n }\n\n modeWriteInProgress = true;\n try {\n await writeSitePermissionsMode(mode);\n applyMode(mode);\n } finally {\n modeWriteInProgress = false;\n }\n};\n\nconst refreshAll = async (): Promise<void> => {\n await Promise.all([refresh(), refreshMode()]);\n};\n\nconst main = (): void => {\n void refreshAll();\n\n const { granular, bypass } = getModeEls();\n granular.addEventListener('change', () => {\n if (!granular.checked) {\n return;\n }\n void setMode('granular');\n });\n bypass.addEventListener('change', () => {\n if (!bypass.checked) {\n return;\n }\n void setMode('bypass');\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (chrome as any).storage?.onChanged?.addListener?.(() => {\n void refreshAll();\n });\n};\n\nmain();\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["toast"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// packages/extension/src/permission-prompt-ui.ts
|
|
4
|
+
var PORT_NAME = "permission_prompt";
|
|
5
|
+
var byId = (id) => {
|
|
6
|
+
const el = document.getElementById(id);
|
|
7
|
+
if (!el) {
|
|
8
|
+
throw new Error(`Missing element: ${id}`);
|
|
9
|
+
}
|
|
10
|
+
return el;
|
|
11
|
+
};
|
|
12
|
+
var setDisabled = (disabled) => {
|
|
13
|
+
for (const id of ["bb-allow-once", "bb-allow-always", "bb-deny"]) {
|
|
14
|
+
byId(id).disabled = disabled;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var main = () => {
|
|
18
|
+
const qs = new URLSearchParams(window.location.search);
|
|
19
|
+
const requestId = qs.get("requestId") ?? "";
|
|
20
|
+
const site = qs.get("site") ?? "";
|
|
21
|
+
const action = qs.get("action") ?? "";
|
|
22
|
+
const summary = byId("bb-summary");
|
|
23
|
+
const siteEl = byId("bb-site");
|
|
24
|
+
if (!requestId || !site) {
|
|
25
|
+
summary.textContent = "Invalid prompt state. Close this window and retry.";
|
|
26
|
+
siteEl.textContent = "";
|
|
27
|
+
setDisabled(true);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
summary.innerHTML = action ? `Browser Bridge wants to run <span class="bb-inline-code">${escapeHtml(
|
|
31
|
+
action
|
|
32
|
+
)}</span> on this site:` : "Browser Bridge wants to act on this site:";
|
|
33
|
+
siteEl.textContent = site;
|
|
34
|
+
const port = chrome.runtime.connect({ name: PORT_NAME });
|
|
35
|
+
const sendDecision = (decision) => {
|
|
36
|
+
setDisabled(true);
|
|
37
|
+
try {
|
|
38
|
+
port.postMessage({
|
|
39
|
+
type: "decision",
|
|
40
|
+
requestId,
|
|
41
|
+
decision
|
|
42
|
+
});
|
|
43
|
+
} finally {
|
|
44
|
+
window.setTimeout(() => window.close(), 50);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
byId("bb-allow-once").addEventListener(
|
|
48
|
+
"click",
|
|
49
|
+
() => sendDecision("allow_once")
|
|
50
|
+
);
|
|
51
|
+
byId("bb-allow-always").addEventListener(
|
|
52
|
+
"click",
|
|
53
|
+
() => sendDecision("allow_always")
|
|
54
|
+
);
|
|
55
|
+
byId("bb-deny").addEventListener("click", () => sendDecision("deny"));
|
|
56
|
+
};
|
|
57
|
+
var escapeHtml = (raw) => {
|
|
58
|
+
return raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
59
|
+
};
|
|
60
|
+
main();
|
|
61
|
+
})();
|
|
62
|
+
//# sourceMappingURL=permission-prompt-ui.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/permission-prompt-ui.ts"],
|
|
4
|
+
"sourcesContent": ["type Decision = 'allow_once' | 'allow_always' | 'deny';\n\nconst PORT_NAME = 'permission_prompt';\n\nconst byId = (id: string): HTMLElement => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n return el;\n};\n\nconst setDisabled = (disabled: boolean): void => {\n for (const id of ['bb-allow-once', 'bb-allow-always', 'bb-deny']) {\n (byId(id) as HTMLButtonElement).disabled = disabled;\n }\n};\n\nconst main = (): void => {\n const qs = new URLSearchParams(window.location.search);\n const requestId = qs.get('requestId') ?? '';\n const site = qs.get('site') ?? '';\n const action = qs.get('action') ?? '';\n\n const summary = byId('bb-summary');\n const siteEl = byId('bb-site');\n\n if (!requestId || !site) {\n summary.textContent = 'Invalid prompt state. Close this window and retry.';\n siteEl.textContent = '';\n setDisabled(true);\n return;\n }\n\n summary.innerHTML = action\n ? `Browser Bridge wants to run <span class=\"bb-inline-code\">${escapeHtml(\n action\n )}</span> on this site:`\n : 'Browser Bridge wants to act on this site:';\n siteEl.textContent = site;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const port = (chrome as any).runtime.connect({ name: PORT_NAME });\n\n const sendDecision = (decision: Decision): void => {\n setDisabled(true);\n try {\n port.postMessage({\n type: 'decision',\n requestId,\n decision,\n });\n } finally {\n // Allow the postMessage to flush, but don't leave the window hanging.\n window.setTimeout(() => window.close(), 50);\n }\n };\n\n byId('bb-allow-once').addEventListener('click', () =>\n sendDecision('allow_once')\n );\n byId('bb-allow-always').addEventListener('click', () =>\n sendDecision('allow_always')\n );\n byId('bb-deny').addEventListener('click', () => sendDecision('deny'));\n};\n\nconst escapeHtml = (raw: string): string => {\n // Keep it tiny; this is only used for displaying the action string.\n return raw.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n};\n\nmain();\n\n// Make this file a module (avoid global name collisions across UI entrypoints).\nexport {};\n"],
|
|
5
|
+
"mappings": ";;;AAEA,MAAM,YAAY;AAElB,MAAM,OAAO,CAAC,OAA4B;AACxC,UAAM,KAAK,SAAS,eAAe,EAAE;AACrC,QAAI,CAAC,IAAI;AACP,YAAM,IAAI,MAAM,oBAAoB,EAAE,EAAE;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAEA,MAAM,cAAc,CAAC,aAA4B;AAC/C,eAAW,MAAM,CAAC,iBAAiB,mBAAmB,SAAS,GAAG;AAChE,MAAC,KAAK,EAAE,EAAwB,WAAW;AAAA,IAC7C;AAAA,EACF;AAEA,MAAM,OAAO,MAAY;AACvB,UAAM,KAAK,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACrD,UAAM,YAAY,GAAG,IAAI,WAAW,KAAK;AACzC,UAAM,OAAO,GAAG,IAAI,MAAM,KAAK;AAC/B,UAAM,SAAS,GAAG,IAAI,QAAQ,KAAK;AAEnC,UAAM,UAAU,KAAK,YAAY;AACjC,UAAM,SAAS,KAAK,SAAS;AAE7B,QAAI,CAAC,aAAa,CAAC,MAAM;AACvB,cAAQ,cAAc;AACtB,aAAO,cAAc;AACrB,kBAAY,IAAI;AAChB;AAAA,IACF;AAEA,YAAQ,YAAY,SAChB,4DAA4D;AAAA,MAC1D;AAAA,IACF,CAAC,0BACD;AACJ,WAAO,cAAc;AAGrB,UAAM,OAAQ,OAAe,QAAQ,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEhE,UAAM,eAAe,CAAC,aAA6B;AACjD,kBAAY,IAAI;AAChB,UAAI;AACF,aAAK,YAAY;AAAA,UACf,MAAM;AAAA,UACN;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH,UAAE;AAEA,eAAO,WAAW,MAAM,OAAO,MAAM,GAAG,EAAE;AAAA,MAC5C;AAAA,IACF;AAEA,SAAK,eAAe,EAAE;AAAA,MAAiB;AAAA,MAAS,MAC9C,aAAa,YAAY;AAAA,IAC3B;AACA,SAAK,iBAAiB,EAAE;AAAA,MAAiB;AAAA,MAAS,MAChD,aAAa,cAAc;AAAA,IAC7B;AACA,SAAK,SAAS,EAAE,iBAAiB,SAAS,MAAM,aAAa,MAAM,CAAC;AAAA,EACtE;AAEA,MAAM,aAAa,CAAC,QAAwB;AAE1C,WAAO,IAAI,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAAA,EAC9E;AAEA,OAAK;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// packages/extension/src/popup-ui.ts
|
|
4
|
+
var byId = (id) => {
|
|
5
|
+
const el = document.getElementById(id);
|
|
6
|
+
if (!el) {
|
|
7
|
+
throw new Error(`Missing element: ${id}`);
|
|
8
|
+
}
|
|
9
|
+
if (!(el instanceof HTMLAnchorElement)) {
|
|
10
|
+
throw new Error(`Expected <a> element: ${id}`);
|
|
11
|
+
}
|
|
12
|
+
return el;
|
|
13
|
+
};
|
|
14
|
+
var openOptionsPopupWindow = async () => {
|
|
15
|
+
const chromeAny = chrome;
|
|
16
|
+
const url = chromeAny.runtime.getURL("options.html");
|
|
17
|
+
await new Promise((resolve) => {
|
|
18
|
+
chromeAny.windows.create(
|
|
19
|
+
{
|
|
20
|
+
type: "popup",
|
|
21
|
+
url,
|
|
22
|
+
focused: true,
|
|
23
|
+
width: 900,
|
|
24
|
+
height: 720
|
|
25
|
+
},
|
|
26
|
+
() => resolve()
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
var openGithub = async () => {
|
|
31
|
+
const chromeAny = chrome;
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
chromeAny.tabs.create(
|
|
34
|
+
{ url: "https://github.com/btraut/browser-bridge" },
|
|
35
|
+
() => resolve()
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
var main = () => {
|
|
40
|
+
byId("bb-settings").addEventListener("click", (e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
void openOptionsPopupWindow().finally(() => window.close());
|
|
43
|
+
});
|
|
44
|
+
byId("bb-about").addEventListener("click", (e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
void openGithub().finally(() => window.close());
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
main();
|
|
50
|
+
})();
|
|
51
|
+
//# sourceMappingURL=popup-ui.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/popup-ui.ts"],
|
|
4
|
+
"sourcesContent": ["const byId = (id: string): HTMLAnchorElement => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n if (!(el instanceof HTMLAnchorElement)) {\n throw new Error(`Expected <a> element: ${id}`);\n }\n return el;\n};\n\nconst openOptionsPopupWindow = async (): Promise<void> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const chromeAny = chrome as any;\n const url = chromeAny.runtime.getURL('options.html');\n await new Promise<void>((resolve) => {\n chromeAny.windows.create(\n {\n type: 'popup',\n url,\n focused: true,\n width: 900,\n height: 720,\n },\n () => resolve()\n );\n });\n};\n\nconst openGithub = async (): Promise<void> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const chromeAny = chrome as any;\n await new Promise<void>((resolve) => {\n chromeAny.tabs.create(\n { url: 'https://github.com/btraut/browser-bridge' },\n () => resolve()\n );\n });\n};\n\nconst main = (): void => {\n byId('bb-settings').addEventListener('click', (e) => {\n e.preventDefault();\n void openOptionsPopupWindow().finally(() => window.close());\n });\n byId('bb-about').addEventListener('click', (e) => {\n e.preventDefault();\n void openGithub().finally(() => window.close());\n });\n};\n\nmain();\n\n// Make this file a module (avoid global name collisions across UI entrypoints).\nexport {};\n"],
|
|
5
|
+
"mappings": ";;;AAAA,MAAM,OAAO,CAAC,OAAkC;AAC9C,UAAM,KAAK,SAAS,eAAe,EAAE;AACrC,QAAI,CAAC,IAAI;AACP,YAAM,IAAI,MAAM,oBAAoB,EAAE,EAAE;AAAA,IAC1C;AACA,QAAI,EAAE,cAAc,oBAAoB;AACtC,YAAM,IAAI,MAAM,yBAAyB,EAAE,EAAE;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AAEA,MAAM,yBAAyB,YAA2B;AAExD,UAAM,YAAY;AAClB,UAAM,MAAM,UAAU,QAAQ,OAAO,cAAc;AACnD,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,gBAAU,QAAQ;AAAA,QAChB;AAAA,UACE,MAAM;AAAA,UACN;AAAA,UACA,SAAS;AAAA,UACT,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA,MAAM,QAAQ;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAM,aAAa,YAA2B;AAE5C,UAAM,YAAY;AAClB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,gBAAU,KAAK;AAAA,QACb,EAAE,KAAK,2CAA2C;AAAA,QAClD,MAAM,QAAQ;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAM,OAAO,MAAY;AACvB,SAAK,aAAa,EAAE,iBAAiB,SAAS,CAAC,MAAM;AACnD,QAAE,eAAe;AACjB,WAAK,uBAAuB,EAAE,QAAQ,MAAM,OAAO,MAAM,CAAC;AAAA,IAC5D,CAAC;AACD,SAAK,UAAU,EAAE,iBAAiB,SAAS,CAAC,MAAM;AAChD,QAAE,eAAe;AACjB,WAAK,WAAW,EAAE,QAAQ,MAAM,OAAO,MAAM,CAAC;AAAA,IAChD,CAAC;AAAA,EACH;AAEA,OAAK;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Browser Bridge",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Control Chrome for Browser Bridge: drive tabs and inspect pages for coding agents.",
|
|
6
6
|
"icons": {
|
|
7
7
|
"16": "assets/icons/icon-16.png",
|
|
@@ -14,13 +14,18 @@
|
|
|
14
14
|
"16": "assets/icons/icon-16.png",
|
|
15
15
|
"32": "assets/icons/icon-32.png",
|
|
16
16
|
"48": "assets/icons/icon-48.png"
|
|
17
|
-
}
|
|
17
|
+
},
|
|
18
|
+
"default_popup": "popup.html"
|
|
18
19
|
},
|
|
19
20
|
"background": {
|
|
20
21
|
"service_worker": "dist/background.js",
|
|
21
22
|
"type": "module"
|
|
22
23
|
},
|
|
23
|
-
"
|
|
24
|
+
"options_ui": {
|
|
25
|
+
"page": "options.html",
|
|
26
|
+
"open_in_tab": true
|
|
27
|
+
},
|
|
28
|
+
"permissions": ["tabs", "storage", "webNavigation", "debugger", "tabGroups"],
|
|
24
29
|
"host_permissions": ["<all_urls>"],
|
|
25
30
|
"content_scripts": [
|
|
26
31
|
{
|
package/package.json
CHANGED
|
@@ -59,6 +59,7 @@ browser-bridge session close --session-id <id>
|
|
|
59
59
|
Notes:
|
|
60
60
|
|
|
61
61
|
- `--max-nodes` only applies to `--format ax` snapshots. For `--format html`, the snapshot succeeds and the flag is ignored with a warning.
|
|
62
|
+
- When `tab_id` is omitted, drive commands target a dedicated agent window/tab that Browser Bridge creates and reuses automatically.
|
|
62
63
|
|
|
63
64
|
Element targeting:
|
|
64
65
|
|