feat: Ed25519 challenge-response auth + YubiKey/SSH agent support (v0.9.0)
Testing / test (push) Successful in 26s
Package Extension / package-extension (push) Successful in 22s
Build & Publish Package / publish (push) Successful in 27s

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>
This commit is contained in:
2026-05-02 16:20:39 +02:00
parent 9f03e29807
commit 4b2abbbfc5
14 changed files with 735 additions and 121 deletions
+2
View File
@@ -94,6 +94,8 @@ export async function sessionDiff({ nameA, nameB }) {
export async function sessionAutoSave({ enabled }) {
await chrome.storage.local.set({ autoSave: enabled });
chrome.tabs.onUpdated.removeListener(autoSaveHandler);
chrome.tabs.onRemoved.removeListener(autoSaveHandler);
if (enabled) {
chrome.tabs.onUpdated.addListener(autoSaveHandler);
chrome.tabs.onRemoved.addListener(autoSaveHandler);
+3 -3
View File
@@ -23,6 +23,7 @@ export async function tabsClose({ tabId, inactive, 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);
}
@@ -133,8 +134,8 @@ export async function tabsSort({ by }) {
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 || "about:blank").hostname;
const db = new URL(b.url || "about:blank").hostname;
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++) {
@@ -146,7 +147,6 @@ export async function tabsSort({ by }) {
}
export async function tabsMergeWindows() {
const [focused] = await chrome.windows.getAll({ populate: false });
const current = await chrome.windows.getCurrent();
const all = await chrome.windows.getAll({ populate: true });
let moved = 0;