feat: add remote trust and server identity pinning
Testing / remote-protocol-compat (0.16.0) (push) Successful in 1m1s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 1m3s
Testing / test (push) Failing after 1m15s
Build & Publish Package / publish (push) Successful in 51s
Package Extension / package-extension (push) Successful in 1m6s

- Add SSH-style server identity keys and known-host verification for remote serve endpoints.
- Add remote add/list/remove commands for explicit endpoint persistence.
- Fix remote clients listing to fan out through target discovery instead of ambiguous auto-routing.
- Add URL glob matching for tabs filter and count with extension tests.
- Add n8n credential pinning for server public keys or SHA256 fingerprints.
- Remove obsolete compat shim behavior while keeping empty compat seams for future protocol changes.
- Bump browser-cli to 0.16.4 and n8n node to 0.3.1.
- Cover known-hosts, remote registry, compat seams, n8n protocol verification, and URL matching with tests.
This commit is contained in:
2026-06-26 08:53:21 +02:00
parent 1ae9c33f00
commit 6270d8c956
28 changed files with 981 additions and 297 deletions
+13 -33
View File
@@ -3,48 +3,28 @@ Auth-field normalizers — applied to the raw incoming message *before* the
auth check runs. Protocol fields (pubkey, sig, …) are still present here.
Add one entry per breaking auth-field change:
("X.Y.Z", transformer_fn)
("X.Y.Z", transformer_fn)
Entries must stay in ascending version order.
The registry is intentionally empty: the first public release was 0.14.1, so no
legacy-client shim has ever been needed. This module is the seam — add a tuple
here (and a unit test for it) the day a breaking auth-field change ships.
"""
from __future__ import annotations
from typing import Callable
from browser_cli.version_manager import parse_version
# ── v0.9.3 ────────────────────────────────────────────────────────────────────
def _auth_0_9_3(msg: dict) -> dict:
"""pubkey validation tightened to lowercase hex; normalize for older clients."""
changed: dict = {}
pk = msg.get("pubkey")
if isinstance(pk, str) and pk:
changed["pubkey"] = pk.lower()
if msg.get("command") in {"browser-cli.auth.trust", "browser-cli.auth.policy"}:
args = msg.get("args") or {}
trust_pk = args.get("pubkey")
identifier = args.get("identifier")
patched = dict(args)
if isinstance(trust_pk, str) and trust_pk:
patched["pubkey"] = trust_pk.lower()
if isinstance(identifier, str) and identifier and len(identifier) == 64:
patched["identifier"] = identifier.lower()
if patched != args:
changed["args"] = patched
return {**msg, **changed} if changed else msg
# ── registry ──────────────────────────────────────────────────────────────────
_AUTH_COMPAT: list[tuple[str, Callable[[dict], dict]]] = [
("0.9.3", _auth_0_9_3),
]
_AUTH_COMPAT: list[tuple[str, Callable[[dict], dict]]] = []
def adapt_auth(msg: dict, client_version: str) -> dict:
"""Apply all auth normalizers needed to bring msg up to the current format."""
cv = parse_version(client_version)
for version, fn in _AUTH_COMPAT:
if cv < parse_version(version):
msg = fn(msg)
"""Apply all auth normalizers needed to bring msg up to the current format."""
if not _AUTH_COMPAT:
return msg
cv = parse_version(client_version)
for version, fn in _AUTH_COMPAT:
if cv < parse_version(version):
msg = fn(msg)
return msg
+19 -16
View File
@@ -3,7 +3,7 @@ Command-format shims — applied to clean_msg (protocol fields already stripped)
before forwarding to the native host, and to responses before sending back.
Add one entry per breaking command-format change:
("X.Y.Z", request_fn, response_fn)
("X.Y.Z", request_fn, response_fn)
- request_fn(msg: dict) -> dict or None
- response_fn(resp: bytes, command: str) -> bytes or None
@@ -11,33 +11,36 @@ Add one entry per breaking command-format change:
Entries must stay in ascending version order.
adapt_request walks forward (oldest first); adapt_response walks backward.
Current baseline: 0.9.3 — no command-format shims needed yet.
The registry is intentionally empty: no command-format shim has been needed
since the first public release (0.14.1). This module is the seam — add a tuple
here (and a unit test for it) the day a breaking command-format change ships.
"""
from __future__ import annotations
from typing import Callable
from browser_cli.version_manager import parse_version
# ── registry ──────────────────────────────────────────────────────────────────
_COMPAT: list[tuple[str, Callable[[dict], dict] | None, Callable[[bytes, str], bytes] | None]] = [
# ("1.0.0", _req_1_0_0, _resp_1_0_0),
# ("1.0.0", _req_1_0_0, _resp_1_0_0),
]
def adapt_request(msg: dict, client_version: str) -> dict:
"""Upgrade a client message to the current browser command format."""
cv = parse_version(client_version)
for version, req_fn, _ in _COMPAT:
if cv < parse_version(version) and req_fn is not None:
msg = req_fn(msg)
"""Upgrade a client message to the current browser command format."""
if not _COMPAT:
return msg
cv = parse_version(client_version)
for version, req_fn, _ in _COMPAT:
if cv < parse_version(version) and req_fn is not None:
msg = req_fn(msg)
return msg
def adapt_response(resp: bytes, command: str, client_version: str) -> bytes:
"""Downgrade a native-host response to the format the client expects."""
cv = parse_version(client_version)
for version, _, resp_fn in reversed(_COMPAT):
if cv < parse_version(version) and resp_fn is not None:
resp = resp_fn(resp, command)
"""Downgrade a native-host response to the format the client expects."""
if not _COMPAT:
return resp
cv = parse_version(client_version)
for version, _, resp_fn in reversed(_COMPAT):
if cv < parse_version(version) and resp_fn is not None:
resp = resp_fn(resp, command)
return resp