4b2abbbfc5
Security: - serve.py: server now sends nonce challenge before accepting any command; clients sign nonce + SHA256(canonical_payload) with Ed25519 key - New --authorized-keys FILE option for serve; token auth still works as fallback - Connection limit: BoundedSemaphore(64) in serve.py - Secure file creation with os.open(..., 0o600) for token/key files - New auth.py module: keygen, file key load/save, SSH agent protocol (pure Python), sign/verify helpers compatible with both file keys and agent-held keys (YubiKey, TPM, gpg-agent) Features: - YubiKey support via SSH agent protocol — no new runtime deps, just $SSH_AUTH_SOCK - New `browser-cli auth` command group: keygen, trust, show, keys - Global --key PATH flag (or BROWSER_CLI_KEY env) selects signing key; pass "agent" or "agent:<selector>" to use SSH agent key - BrowserCLI Python API gains key= parameter Bug fixes (11 issues across two review passes): - client.py: check response is not None before json.loads - native_host.py: _read_exact_stream loop handles EINTR short reads; fix Windows Listener leak on accept error - __init__.py: open_wait / tabs_watch_url raise RuntimeError instead of silent None - extension/tabs.ts: dedupe skips tabs without URL; tabsSort uses pendingUrl fallback - extension/session.ts: removeListener before addListener prevents duplicate handlers Breaking: TCP serve protocol now sends a challenge frame first (v0.9.0) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
// @ts-nocheck
|
|
import { executeScript, getActiveTab, getAliases, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core';
|
|
export async function tabsList() {
|
|
const windows = await chrome.windows.getAll({ populate: true });
|
|
const aliases = await getAliases();
|
|
const tabs = [];
|
|
for (const w of windows) {
|
|
for (const t of w.tabs) {
|
|
tabs.push({
|
|
...tabInfo(t),
|
|
windowAlias: aliases[t.windowId] || null,
|
|
pinned: t.pinned,
|
|
favIconUrl: t.favIconUrl,
|
|
});
|
|
}
|
|
}
|
|
return tabs;
|
|
}
|
|
|
|
export async function tabsClose({ tabId, inactive, duplicates }) {
|
|
let toClose = [];
|
|
if (duplicates) {
|
|
const all = await chrome.tabs.query({});
|
|
const seen = new Set();
|
|
for (const t of all) {
|
|
if (!t.url) continue;
|
|
if (seen.has(t.url)) toClose.push(t.id);
|
|
else seen.add(t.url);
|
|
}
|
|
} else if (inactive) {
|
|
const all = await chrome.tabs.query({});
|
|
toClose = all.filter(t => !t.active).map(t => t.id);
|
|
} else if (tabId) {
|
|
toClose = [tabId];
|
|
}
|
|
if (toClose.length) await chrome.tabs.remove(toClose);
|
|
return { closed: toClose.length };
|
|
}
|
|
|
|
export async function tabsMove({ tabId, groupId, windowId, index, forward, backward }) {
|
|
const moveProps = {};
|
|
if (windowId != null) moveProps.windowId = windowId;
|
|
|
|
if (forward || backward) {
|
|
const tab = await chrome.tabs.get(tabId);
|
|
if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal
|
|
else moveProps.index = Math.max(0, tab.index - 1);
|
|
} else if (index != null) {
|
|
moveProps.index = index;
|
|
} else {
|
|
moveProps.index = -1;
|
|
}
|
|
|
|
await chrome.tabs.move(tabId, moveProps);
|
|
if (groupId != null) {
|
|
await chrome.tabs.group({ tabIds: [tabId], groupId });
|
|
}
|
|
return { tabId };
|
|
}
|
|
|
|
export async function tabsActive({ tabId }) {
|
|
const tab = await chrome.tabs.get(tabId);
|
|
await chrome.windows.update(tab.windowId, { focused: true });
|
|
await chrome.tabs.update(tabId, { active: true });
|
|
return { tabId };
|
|
}
|
|
|
|
export async function tabsActiveInWindow({ windowId }) {
|
|
const activeTabs = await chrome.tabs.query({ windowId, active: true });
|
|
const tab = activeTabs[0];
|
|
if (!tab) {
|
|
throw new Error(`No active tab found for window ${windowId}`);
|
|
}
|
|
return tabInfo(tab);
|
|
}
|
|
|
|
export async function tabsStatus({ tabId }) {
|
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
|
return tabInfo(tab);
|
|
}
|
|
|
|
export async function tabsFilter({ pattern }) {
|
|
const all = await chrome.tabs.query({});
|
|
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
|
}
|
|
|
|
export async function tabsCount({ pattern }) {
|
|
const all = await chrome.tabs.query({});
|
|
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
|
|
return all.length;
|
|
}
|
|
|
|
export async function tabsQuery({ search }) {
|
|
const q = search.toLowerCase();
|
|
const all = await chrome.tabs.query({});
|
|
return all.filter(t =>
|
|
(t.url && t.url.toLowerCase().includes(q)) ||
|
|
(t.title && t.title.toLowerCase().includes(q))
|
|
).map(tabInfo);
|
|
}
|
|
|
|
export async function tabsHtml({ tabId }) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
|
if (!isScriptableUrl(tab.url || tab.pendingUrl || "")) {
|
|
throw new Error(`Cannot get HTML of ${tab.url || tab.pendingUrl} — navigate to a regular web page first`);
|
|
}
|
|
try {
|
|
const results = await executeScript({
|
|
target: { tabId: tab.id },
|
|
func: () => document.documentElement.outerHTML,
|
|
});
|
|
return results[0]?.result || "";
|
|
} catch (e) {
|
|
const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page");
|
|
if (i < 2 && transient) {
|
|
await new Promise(r => setTimeout(r, 300));
|
|
continue;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function tabsDedupe() {
|
|
return tabsClose({ duplicates: true });
|
|
}
|
|
|
|
export async function tabsSort({ by }) {
|
|
const windows = await chrome.windows.getAll({ populate: true });
|
|
let moved = 0;
|
|
for (const w of windows) {
|
|
const sorted = [...w.tabs].sort((a, b) => {
|
|
if (by === "title") return (a.title || "").localeCompare(b.title || "");
|
|
if (by === "time") return a.id - b.id; // lower id = opened earlier
|
|
// domain (default)
|
|
const da = new URL(a.url || a.pendingUrl || "about:blank").hostname;
|
|
const db = new URL(b.url || b.pendingUrl || "about:blank").hostname;
|
|
return da.localeCompare(db);
|
|
});
|
|
for (let i = 0; i < sorted.length; i++) {
|
|
await chrome.tabs.move(sorted[i].id, { index: i });
|
|
moved++;
|
|
}
|
|
}
|
|
return { moved };
|
|
}
|
|
|
|
export async function tabsMergeWindows() {
|
|
const current = await chrome.windows.getCurrent();
|
|
const all = await chrome.windows.getAll({ populate: true });
|
|
let moved = 0;
|
|
for (const w of all) {
|
|
if (w.id === current.id) continue;
|
|
const ids = w.tabs.map(t => t.id);
|
|
await chrome.tabs.move(ids, { windowId: current.id, index: -1 });
|
|
moved += ids.length;
|
|
}
|
|
return { moved };
|
|
}
|
|
|
|
export async function tabsPin({ tabId }) {
|
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
|
await chrome.tabs.update(tab.id, { pinned: true });
|
|
return { tabId: tab.id, pinned: true };
|
|
}
|
|
|
|
export async function tabsUnpin({ tabId }) {
|
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
|
await chrome.tabs.update(tab.id, { pinned: false });
|
|
return { tabId: tab.id, pinned: false };
|
|
}
|
|
|
|
export async function tabsScreenshot({ tabId, format = "png", quality } = {}) {
|
|
let windowId;
|
|
if (tabId) {
|
|
const tab = await chrome.tabs.get(tabId);
|
|
await chrome.tabs.update(tabId, { active: true });
|
|
windowId = tab.windowId;
|
|
} else {
|
|
const tab = await getActiveTab();
|
|
windowId = tab.windowId;
|
|
}
|
|
const opts = { format };
|
|
if (format === "jpeg" && quality != null) opts.quality = quality;
|
|
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, opts);
|
|
return { dataUrl, format };
|
|
}
|
|
|
|
export async function tabsWatchUrl({ pattern, timeout = 30000, tabId } = {}) {
|
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
|
const deadline = Date.now() + timeout;
|
|
const regex = new RegExp(pattern);
|
|
while (Date.now() < deadline) {
|
|
const t = await chrome.tabs.get(tab.id);
|
|
const url = t.url || t.pendingUrl || "";
|
|
if (regex.test(url)) return tabInfo(t);
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
throw new Error(`Tab ${tab.id} URL did not match '${pattern}' within ${timeout}ms`);
|
|
}
|
|
|
|
export async function tabsMute({ tabId }) {
|
|
const tab = await resolveTabForDirectAction(tabId, "mute");
|
|
await chrome.tabs.update(tab.id, { muted: true });
|
|
return { tabId: tab.id, muted: true };
|
|
}
|
|
|
|
export async function tabsUnmute({ tabId }) {
|
|
const tab = await resolveTabForDirectAction(tabId, "unmute");
|
|
await chrome.tabs.update(tab.id, { muted: false });
|
|
return { tabId: tab.id, muted: false };
|
|
}
|
|
|