feat: add n8n serve node and harden remote access

- Add the n8n community node package with credentials, command mapping, direct serve TCP client, and browser-cli protocol crypto helpers.

- Cover Ed25519 signing, canonical JSON, PQ transport encryption, request mapping, and security behavior with unit tests.

- Harden serve-http with per-address rate limiting, an 8 MB request body cap, and clear warnings when binding plain HTTP beyond loopback.

- Stop one-shot --key overrides from being persisted automatically; document explicit remote trust and keep key-management behind the keys policy tier.

- Make HTML-to-Markdown conversion safer by bounding tree depth and dropping unsafe link/image URL schemes.

- Bump package and extension release metadata to 0.16.3.
This commit is contained in:
2026-06-19 10:00:23 +02:00
parent 7fe0e27fec
commit cea8a7e994
28 changed files with 3687 additions and 164 deletions
+42
View File
@@ -462,6 +462,48 @@ class TestPerKeyPolicy:
client.close()
t.join(timeout=2)
def _trust_and_query_keys(self, tmp_path, monkeypatch, server_policy):
"""Authenticate, then send browser-cli.auth.keys; return the response dict."""
from browser_cli.serve.security import ServeSecurity
path = tmp_path / "authorized_keys"
pem, pub = generate_keypair()
path.write_text(pub + " mykey\n")
key_path = tmp_path / "client.key.pem"
key_path.write_bytes(pem)
priv = load_private_key(key_path)
client, server = _pair()
t = _spawn(server, path, ServeSecurity(policy=server_policy))
challenge = _recv_framed(client)
nonce = bytes.fromhex(challenge["nonce"])
msg = {"id": "x", "command": "browser-cli.auth.keys", "args": {}, "user_agent": FAKE_UA, "pubkey": pub}
msg["sig"] = sign(priv, nonce, msg).hex()
_send_framed(client, json.dumps(msg).encode())
resp = _recv_framed(client)
client.close()
t.join(timeout=2)
return resp
def test_key_management_blocked_without_keys_grant(self, tmp_path, monkeypatch):
"""Even full control+dangerous can't list keys — the control command is gated."""
from browser_cli.command_security import CommandPolicy
resp = self._trust_and_query_keys(
tmp_path, monkeypatch,
CommandPolicy(allow_read_page=True, allow_control=True, allow_dangerous=True),
)
assert resp["success"] is False
assert "blocked" in resp["error"].lower() and "keys" in resp["error"].lower()
def test_key_management_allowed_with_keys_grant(self, tmp_path, monkeypatch):
"""With allow_keys, the control command runs and returns the trusted-key list."""
from browser_cli.command_security import CommandPolicy
resp = self._trust_and_query_keys(tmp_path, monkeypatch, CommandPolicy(allow_keys=True))
assert resp["success"] is True
assert resp["data"][0]["name"] == "mykey"
class TestRateLimit:
def test_shared_rate_limiter_blocks_second_command(self, monkeypatch):
"""A burst-1 limiter shared across connections allows the first command, denies the next."""