Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4b2abbbfc5
|
|||
|
9f03e29807
|
|||
|
e1ff67e259
|
|||
|
a8421e97f5
|
+31
-21
@@ -18,6 +18,7 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
||||||
from browser_cli.models import Group, Tab
|
from browser_cli.models import Group, Tab
|
||||||
@@ -33,7 +34,7 @@ class BrowserCounts:
|
|||||||
|
|
||||||
|
|
||||||
class BrowserCLI:
|
class BrowserCLI:
|
||||||
def __init__(self, browser: str | None = None, remote: str | None = None, token: str | None = None):
|
def __init__(self, browser: str | None = None, remote: str | None = None, token: str | None = None, key: str | None = None):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
browser: Profile alias to target. Required when multiple browser
|
browser: Profile alias to target. Required when multiple browser
|
||||||
@@ -42,14 +43,18 @@ class BrowserCLI:
|
|||||||
Format: ``"host:port"`` (e.g. ``"192.168.1.10:8765"``).
|
Format: ``"host:port"`` (e.g. ``"192.168.1.10:8765"``).
|
||||||
Can be combined with ``browser`` to route to a specific
|
Can be combined with ``browser`` to route to a specific
|
||||||
remote profile.
|
remote profile.
|
||||||
token: Auth token for the remote serve instance.
|
token: Auth token for the remote serve instance (legacy token auth).
|
||||||
|
key: Path to Ed25519 private key PEM for pubkey auth. When set,
|
||||||
|
overrides token auth. Defaults to ``~/.config/browser-cli/client.key.pem``
|
||||||
|
if that file exists.
|
||||||
"""
|
"""
|
||||||
self._browser = browser
|
self._browser = browser
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self._token = token
|
self._token = token
|
||||||
|
self._key = Path(key) if key else None
|
||||||
|
|
||||||
def _cmd(self, command: str, args: dict | None = None):
|
def _cmd(self, command: str, args: dict | None = None):
|
||||||
return send_command(command, args, profile=self._browser, remote=self._remote, token=self._token)
|
return send_command(command, args, profile=self._browser, remote=self._remote, token=self._token, key=self._key)
|
||||||
|
|
||||||
def _multi_browser_targets(self):
|
def _multi_browser_targets(self):
|
||||||
if self._browser is not None:
|
if self._browser is not None:
|
||||||
@@ -64,10 +69,11 @@ class BrowserCLI:
|
|||||||
|
|
||||||
def _collect_multi_browser(self, command: str, args: dict | None = None):
|
def _collect_multi_browser(self, command: str, args: dict | None = None):
|
||||||
results = []
|
results = []
|
||||||
for target in self._multi_browser_targets():
|
targets = self._multi_browser_targets()
|
||||||
|
for target in targets:
|
||||||
try:
|
try:
|
||||||
if target.remote:
|
if target.remote:
|
||||||
data = send_command(command, args, profile=target.profile, remote=target.remote, token=target.token)
|
data = send_command(command, args, profile=target.profile, remote=target.remote, token=target.token, key=self._key)
|
||||||
else:
|
else:
|
||||||
data = send_command(command, args, profile=target.profile)
|
data = send_command(command, args, profile=target.profile)
|
||||||
except (BrowserNotConnected, RuntimeError):
|
except (BrowserNotConnected, RuntimeError):
|
||||||
@@ -75,7 +81,7 @@ class BrowserCLI:
|
|||||||
results.append((target, data))
|
results.append((target, data))
|
||||||
if results:
|
if results:
|
||||||
return results
|
return results
|
||||||
if self._multi_browser_targets():
|
if targets:
|
||||||
raise BrowserNotConnected(
|
raise BrowserNotConnected(
|
||||||
"Cannot resolve a browser socket automatically.\n"
|
"Cannot resolve a browser socket automatically.\n"
|
||||||
"Make sure the browser is running with the browser-cli extension enabled,\n"
|
"Make sure the browser is running with the browser-cli extension enabled,\n"
|
||||||
@@ -173,7 +179,9 @@ class BrowserCLI:
|
|||||||
"url": url, "timeout": int(timeout * 1000),
|
"url": url, "timeout": int(timeout * 1000),
|
||||||
"background": background, "window": window, "group": group,
|
"background": background, "window": window, "group": group,
|
||||||
})
|
})
|
||||||
return self._make_tab(data) if isinstance(data, dict) and "id" in data else data
|
if not isinstance(data, dict) or "id" not in data:
|
||||||
|
raise RuntimeError("navigate.open_wait returned unexpected data")
|
||||||
|
return self._make_tab(data)
|
||||||
|
|
||||||
def wait_for_load(
|
def wait_for_load(
|
||||||
self,
|
self,
|
||||||
@@ -291,7 +299,9 @@ class BrowserCLI:
|
|||||||
) -> "Tab":
|
) -> "Tab":
|
||||||
"""Block until the tab URL matches regex pattern. Returns the Tab."""
|
"""Block until the tab URL matches regex pattern. Returns the Tab."""
|
||||||
data = self._cmd("tabs.watch_url", {"pattern": pattern, "tabId": tab_id, "timeout": int(timeout * 1000)})
|
data = self._cmd("tabs.watch_url", {"pattern": pattern, "tabId": tab_id, "timeout": int(timeout * 1000)})
|
||||||
return self._make_tab(data) if isinstance(data, dict) and "id" in data else data
|
if not isinstance(data, dict) or "id" not in data:
|
||||||
|
raise RuntimeError("tabs.watch_url returned unexpected data")
|
||||||
|
return self._make_tab(data)
|
||||||
|
|
||||||
def tabs_screenshot(
|
def tabs_screenshot(
|
||||||
self,
|
self,
|
||||||
@@ -440,7 +450,7 @@ class BrowserCLI:
|
|||||||
for target, windows in multi_results
|
for target, windows in multi_results
|
||||||
for window in (windows or [])
|
for window in (windows or [])
|
||||||
]
|
]
|
||||||
return self._cmd("windows.list", {})
|
return self._cmd("windows.list", {}) or []
|
||||||
|
|
||||||
def windows_rename(self, window_id: int, name: str) -> None:
|
def windows_rename(self, window_id: int, name: str) -> None:
|
||||||
self._cmd("windows.rename", {"windowId": window_id, "name": name})
|
self._cmd("windows.rename", {"windowId": window_id, "name": name})
|
||||||
@@ -450,12 +460,12 @@ class BrowserCLI:
|
|||||||
|
|
||||||
def windows_open(self, url: str | None = None) -> dict:
|
def windows_open(self, url: str | None = None) -> dict:
|
||||||
"""Open a new browser window, optionally on a URL."""
|
"""Open a new browser window, optionally on a URL."""
|
||||||
return self._cmd("windows.open", {"url": url})
|
return self._cmd("windows.open", {"url": url}) or {}
|
||||||
|
|
||||||
# ── DOM ───────────────────────────────────────────────────────────────
|
# ── DOM ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def dom_query(self, selector: str) -> list[dict]:
|
def dom_query(self, selector: str) -> list[dict]:
|
||||||
return self._cmd("dom.query", {"selector": selector})
|
return self._cmd("dom.query", {"selector": selector}) or []
|
||||||
|
|
||||||
def dom_click(self, selector: str) -> None:
|
def dom_click(self, selector: str) -> None:
|
||||||
self._cmd("dom.click", {"selector": selector})
|
self._cmd("dom.click", {"selector": selector})
|
||||||
@@ -464,13 +474,13 @@ class BrowserCLI:
|
|||||||
self._cmd("dom.type", {"selector": selector, "text": text})
|
self._cmd("dom.type", {"selector": selector, "text": text})
|
||||||
|
|
||||||
def dom_attr(self, selector: str, attr: str) -> list[str]:
|
def dom_attr(self, selector: str, attr: str) -> list[str]:
|
||||||
return self._cmd("dom.attr", {"selector": selector, "attr": attr})
|
return self._cmd("dom.attr", {"selector": selector, "attr": attr}) or []
|
||||||
|
|
||||||
def dom_text(self, selector: str) -> list[str]:
|
def dom_text(self, selector: str) -> list[str]:
|
||||||
return self._cmd("dom.text", {"selector": selector})
|
return self._cmd("dom.text", {"selector": selector}) or []
|
||||||
|
|
||||||
def dom_exists(self, selector: str) -> bool:
|
def dom_exists(self, selector: str) -> bool:
|
||||||
return self._cmd("dom.exists", {"selector": selector})
|
return self._cmd("dom.exists", {"selector": selector}) or False
|
||||||
|
|
||||||
def dom_scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
|
def dom_scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
|
||||||
"""Scroll to a CSS selector or to pixel coordinates."""
|
"""Scroll to a CSS selector or to pixel coordinates."""
|
||||||
@@ -630,13 +640,13 @@ class BrowserCLI:
|
|||||||
# ── Extract ───────────────────────────────────────────────────────────
|
# ── Extract ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def extract_links(self) -> list[dict]:
|
def extract_links(self) -> list[dict]:
|
||||||
return self._cmd("extract.links", {})
|
return self._cmd("extract.links", {}) or []
|
||||||
|
|
||||||
def extract_images(self) -> list[dict]:
|
def extract_images(self) -> list[dict]:
|
||||||
return self._cmd("extract.images", {})
|
return self._cmd("extract.images", {}) or []
|
||||||
|
|
||||||
def extract_text(self) -> str:
|
def extract_text(self) -> str:
|
||||||
return self._cmd("extract.text", {})
|
return self._cmd("extract.text", {}) or ""
|
||||||
|
|
||||||
def extract_json(self, selector: str):
|
def extract_json(self, selector: str):
|
||||||
return self._cmd("extract.json", {"selector": selector})
|
return self._cmd("extract.json", {"selector": selector})
|
||||||
@@ -653,7 +663,7 @@ class BrowserCLI:
|
|||||||
self._cmd("session.load", {"name": name})
|
self._cmd("session.load", {"name": name})
|
||||||
|
|
||||||
def session_diff(self, name_a: str, name_b: str) -> dict:
|
def session_diff(self, name_a: str, name_b: str) -> dict:
|
||||||
return self._cmd("session.diff", {"nameA": name_a, "nameB": name_b})
|
return self._cmd("session.diff", {"nameA": name_a, "nameB": name_b}) or {}
|
||||||
|
|
||||||
def session_list(self) -> list[dict]:
|
def session_list(self) -> list[dict]:
|
||||||
"""Return saved sessions.
|
"""Return saved sessions.
|
||||||
@@ -667,7 +677,7 @@ class BrowserCLI:
|
|||||||
for target, sessions in multi_results
|
for target, sessions in multi_results
|
||||||
for session in (sessions or [])
|
for session in (sessions or [])
|
||||||
]
|
]
|
||||||
return self._cmd("session.list", {})
|
return self._cmd("session.list", {}) or []
|
||||||
|
|
||||||
def session_remove(self, name: str) -> None:
|
def session_remove(self, name: str) -> None:
|
||||||
self._cmd("session.remove", {"name": name})
|
self._cmd("session.remove", {"name": name})
|
||||||
@@ -685,8 +695,8 @@ class BrowserCLI:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
transformed = filter_fn(tabs)
|
transformed = filter_fn(tabs)
|
||||||
except Exception:
|
except (AttributeError, TypeError):
|
||||||
transformed = None
|
return [tab for tab in tabs if filter_fn(tab)]
|
||||||
|
|
||||||
if isinstance(transformed, list):
|
if isinstance(transformed, list):
|
||||||
return transformed
|
return transformed
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
"""Ed25519 keypair management and challenge-response auth helpers."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||||
|
from cryptography.hazmat.primitives.serialization import (
|
||||||
|
Encoding,
|
||||||
|
NoEncryption,
|
||||||
|
PrivateFormat,
|
||||||
|
PublicFormat,
|
||||||
|
load_pem_private_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
_CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "browser-cli"
|
||||||
|
DEFAULT_KEY_PATH = _CONFIG_DIR / "client.key.pem"
|
||||||
|
DEFAULT_AUTHORIZED_KEYS_PATH = _CONFIG_DIR / "authorized_keys"
|
||||||
|
|
||||||
|
# ── SSH agent protocol constants ───────────────────────────────────────────────
|
||||||
|
_SSH_AGENTC_REQUEST_IDENTITIES = 11
|
||||||
|
_SSH_AGENT_IDENTITIES_ANSWER = 12
|
||||||
|
_SSH_AGENTC_SIGN_REQUEST = 13
|
||||||
|
_SSH_AGENT_SIGN_RESPONSE = 14
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_str(s: bytes) -> bytes:
|
||||||
|
return struct.pack(">I", len(s)) + s
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_str(data: bytes, off: int) -> tuple[bytes, int]:
|
||||||
|
n = struct.unpack_from(">I", data, off)[0]
|
||||||
|
return data[off + 4 : off + 4 + n], off + 4 + n
|
||||||
|
|
||||||
|
|
||||||
|
def _agent_roundtrip(msg: bytes) -> bytes:
|
||||||
|
sock_path = os.environ.get("SSH_AUTH_SOCK")
|
||||||
|
if not sock_path:
|
||||||
|
raise RuntimeError("SSH_AUTH_SOCK not set — is gpg-agent / ssh-agent running?")
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.settimeout(10)
|
||||||
|
sock.connect(sock_path)
|
||||||
|
sock.sendall(struct.pack(">I", len(msg)) + msg)
|
||||||
|
raw_len = b""
|
||||||
|
while len(raw_len) < 4:
|
||||||
|
chunk = sock.recv(4 - len(raw_len))
|
||||||
|
if not chunk:
|
||||||
|
raise RuntimeError("SSH agent closed connection")
|
||||||
|
raw_len += chunk
|
||||||
|
n = struct.unpack(">I", raw_len)[0]
|
||||||
|
resp = b""
|
||||||
|
while len(resp) < n:
|
||||||
|
chunk = sock.recv(n - len(resp))
|
||||||
|
if not chunk:
|
||||||
|
raise RuntimeError("SSH agent closed connection mid-response")
|
||||||
|
resp += chunk
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
# ── AgentKey ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentKey:
|
||||||
|
"""Ed25519 key backed by an SSH agent (YubiKey, TPM, ssh-agent, gpg-agent …)."""
|
||||||
|
blob: bytes
|
||||||
|
comment: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pubkey_bytes(self) -> bytes:
|
||||||
|
_algo, off = _unpack_str(self.blob, 0)
|
||||||
|
key_bytes, _ = _unpack_str(self.blob, off)
|
||||||
|
return key_bytes
|
||||||
|
|
||||||
|
|
||||||
|
# ── Agent helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def agent_list_keys() -> list[AgentKey]:
|
||||||
|
"""Return all Ed25519 keys currently held by the SSH agent."""
|
||||||
|
resp = _agent_roundtrip(bytes([_SSH_AGENTC_REQUEST_IDENTITIES]))
|
||||||
|
if resp[0] != _SSH_AGENT_IDENTITIES_ANSWER:
|
||||||
|
raise RuntimeError(f"Unexpected agent response: {resp[0]}")
|
||||||
|
n_keys = struct.unpack_from(">I", resp, 1)[0]
|
||||||
|
keys: list[AgentKey] = []
|
||||||
|
off = 5
|
||||||
|
for _ in range(n_keys):
|
||||||
|
blob, off = _unpack_str(resp, off)
|
||||||
|
comment, off = _unpack_str(resp, off)
|
||||||
|
algo, _ = _unpack_str(blob, 0)
|
||||||
|
if algo == b"ssh-ed25519":
|
||||||
|
keys.append(AgentKey(blob=blob, comment=comment.decode("utf-8", errors="replace")))
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def agent_find_key(selector: str | None = None) -> AgentKey | None:
|
||||||
|
"""Return the first agent Ed25519 key whose comment contains selector (or any if None)."""
|
||||||
|
try:
|
||||||
|
keys = agent_list_keys()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
for key in keys:
|
||||||
|
if selector is None or selector in key.comment:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def agent_sign_raw(key: AgentKey, data: bytes) -> bytes:
|
||||||
|
"""Ask the SSH agent to sign data and return the raw 64-byte Ed25519 signature."""
|
||||||
|
msg = (
|
||||||
|
bytes([_SSH_AGENTC_SIGN_REQUEST])
|
||||||
|
+ _pack_str(key.blob)
|
||||||
|
+ _pack_str(data)
|
||||||
|
+ struct.pack(">I", 0)
|
||||||
|
)
|
||||||
|
resp = _agent_roundtrip(msg)
|
||||||
|
if resp[0] != _SSH_AGENT_SIGN_RESPONSE:
|
||||||
|
raise RuntimeError(f"SSH agent refused to sign (response code {resp[0]})")
|
||||||
|
sig_blob, _ = _unpack_str(resp, 1)
|
||||||
|
_algo, soff = _unpack_str(sig_blob, 0)
|
||||||
|
raw_sig, _ = _unpack_str(sig_blob, soff)
|
||||||
|
if len(raw_sig) != 64:
|
||||||
|
raise RuntimeError(f"Unexpected signature length {len(raw_sig)}")
|
||||||
|
return raw_sig
|
||||||
|
|
||||||
|
|
||||||
|
# ── File-based key helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_keypair() -> tuple[bytes, str]:
|
||||||
|
"""Return (private_key_pem_bytes, public_key_hex)."""
|
||||||
|
priv = Ed25519PrivateKey.generate()
|
||||||
|
pem = priv.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
||||||
|
pub_hex = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex()
|
||||||
|
return pem, pub_hex
|
||||||
|
|
||||||
|
|
||||||
|
def load_private_key(path: Path) -> Ed25519PrivateKey:
|
||||||
|
return load_pem_private_key(path.read_bytes(), password=None)
|
||||||
|
|
||||||
|
|
||||||
|
def public_key_hex(key: Ed25519PrivateKey | AgentKey) -> str:
|
||||||
|
if isinstance(key, AgentKey):
|
||||||
|
return key.pubkey_bytes.hex()
|
||||||
|
return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Canonical payload + sign/verify ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def canonical_payload(msg: dict) -> bytes:
|
||||||
|
"""Deterministic JSON encoding of msg without auth fields."""
|
||||||
|
return json.dumps(
|
||||||
|
{k: v for k, v in msg.items() if k not in {"pubkey", "sig"}},
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def sign(key: Ed25519PrivateKey | AgentKey, nonce: bytes, msg: dict) -> bytes:
|
||||||
|
"""Sign nonce + SHA256(canonical_payload(msg)) — works for both file keys and agent keys."""
|
||||||
|
data = nonce + hashlib.sha256(canonical_payload(msg)).digest()
|
||||||
|
if isinstance(key, AgentKey):
|
||||||
|
return agent_sign_raw(key, data)
|
||||||
|
return key.sign(data)
|
||||||
|
|
||||||
|
|
||||||
|
def verify(pub_hex: str, nonce: bytes, msg: dict, sig_hex: str) -> bool:
|
||||||
|
"""Return True if sig_hex is a valid Ed25519 signature over the canonical payload."""
|
||||||
|
try:
|
||||||
|
pub_bytes = bytes.fromhex(pub_hex)
|
||||||
|
pub_key = Ed25519PublicKey.from_public_bytes(pub_bytes)
|
||||||
|
message = nonce + hashlib.sha256(canonical_payload(msg)).digest()
|
||||||
|
pub_key.verify(bytes.fromhex(sig_hex), message)
|
||||||
|
return True
|
||||||
|
except (InvalidSignature, Exception):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def new_nonce() -> str:
|
||||||
|
return secrets.token_hex(32)
|
||||||
|
|
||||||
|
|
||||||
|
def load_authorized_keys(path: Path) -> list[str]:
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
line.strip()
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines()
|
||||||
|
if line.strip() and not line.startswith("#")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def add_authorized_key(path: Path, pub_hex: str) -> bool:
|
||||||
|
"""Append pub_hex to authorized_keys. Returns False if already present."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
existing = set(load_authorized_keys(path))
|
||||||
|
if pub_hex in existing:
|
||||||
|
return False
|
||||||
|
with open(path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(pub_hex + "\n")
|
||||||
|
return True
|
||||||
+144
-11
@@ -101,12 +101,7 @@ def _rename_target_profile(target_browser: str | None) -> str | None:
|
|||||||
def _ensure_unique_browser_alias(alias: str, target_browser: str | None) -> None:
|
def _ensure_unique_browser_alias(alias: str, target_browser: str | None) -> None:
|
||||||
target_profile = _rename_target_profile(target_browser)
|
target_profile = _rename_target_profile(target_browser)
|
||||||
|
|
||||||
profiles: dict[str, str] = {}
|
profiles: dict[str, str] = load_registry(REGISTRY_PATH)
|
||||||
if REGISTRY_PATH.exists():
|
|
||||||
try:
|
|
||||||
profiles = json.loads(REGISTRY_PATH.read_text())
|
|
||||||
except Exception:
|
|
||||||
profiles = {}
|
|
||||||
|
|
||||||
if alias in profiles and alias != target_profile:
|
if alias in profiles and alias != target_profile:
|
||||||
raise click.ClickException(f"Browser alias '{alias}' already exists")
|
raise click.ClickException(f"Browser alias '{alias}' already exists")
|
||||||
@@ -200,24 +195,161 @@ def _print_version(ctx, param, value):
|
|||||||
"--token", default=None, metavar="TOKEN",
|
"--token", default=None, metavar="TOKEN",
|
||||||
help="Auth token for the remote browser-cli serve instance.",
|
help="Auth token for the remote browser-cli serve instance.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--key", default=None, metavar="PATH",
|
||||||
|
help="Ed25519 private key PEM for pubkey auth with a remote serve instance.",
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def main(ctx, browser, remote, token):
|
def main(ctx, browser, remote, token, key):
|
||||||
"""Control your running browser from the terminal via a Chrome extension."""
|
"""Control your running browser from the terminal via a Chrome extension."""
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj["browser"] = browser
|
ctx.obj["browser"] = browser
|
||||||
ctx.obj["browser_explicit"] = browser is not None
|
ctx.obj["browser_explicit"] = browser is not None
|
||||||
if browser:
|
if browser:
|
||||||
os.environ["BROWSER_CLI_PROFILE"] = browser
|
os.environ["BROWSER_CLI_PROFILE"] = browser
|
||||||
|
ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_PROFILE", None))
|
||||||
ctx.obj["remote"] = remote
|
ctx.obj["remote"] = remote
|
||||||
ctx.obj["token"] = token
|
ctx.obj["token"] = token
|
||||||
|
ctx.obj["key"] = key
|
||||||
if remote:
|
if remote:
|
||||||
os.environ["BROWSER_CLI_REMOTE"] = remote
|
os.environ["BROWSER_CLI_REMOTE"] = remote
|
||||||
|
ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_REMOTE", None))
|
||||||
if token:
|
if token:
|
||||||
save_remote_token(remote, token)
|
save_remote_token(remote, token)
|
||||||
if token:
|
if key:
|
||||||
os.environ["BROWSER_CLI_TOKEN"] = token
|
os.environ["BROWSER_CLI_KEY"] = key
|
||||||
|
ctx.call_on_close(lambda: os.environ.pop("BROWSER_CLI_KEY", None))
|
||||||
|
|
||||||
|
|
||||||
|
# ── auth ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@click.group("auth")
|
||||||
|
def auth_group():
|
||||||
|
"""Manage Ed25519 keys for public-key authentication with browser-cli serve."""
|
||||||
|
|
||||||
|
|
||||||
|
@auth_group.command("keygen")
|
||||||
|
@click.option("--output", "-o", default=None, metavar="PATH", help="Output path for the private key PEM.")
|
||||||
|
@click.option("--force", is_flag=True, help="Overwrite existing key.")
|
||||||
|
def cmd_auth_keygen(output, force):
|
||||||
|
"""Generate an Ed25519 keypair for pubkey auth."""
|
||||||
|
from browser_cli.auth import DEFAULT_KEY_PATH, generate_keypair
|
||||||
|
|
||||||
|
key_path = Path(output) if output else DEFAULT_KEY_PATH
|
||||||
|
if key_path.exists() and not force:
|
||||||
|
console.print(f"[red]Key already exists:[/red] {key_path} (use --force to overwrite)")
|
||||||
|
sys.exit(1)
|
||||||
|
pem, pub_hex = generate_keypair()
|
||||||
|
key_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd = os.open(str(key_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
with os.fdopen(fd, "wb") as f:
|
||||||
|
f.write(pem)
|
||||||
|
console.print(f"[green]✓[/green] Private key: {key_path}")
|
||||||
|
console.print(f"\nPublic key:\n [bold cyan]{pub_hex}[/bold cyan]")
|
||||||
|
console.print(f"\nOn the serve host, trust this key:")
|
||||||
|
console.print(f" [dim]browser-cli auth trust {pub_hex}[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_group.command("trust")
|
||||||
|
@click.argument("pubkey")
|
||||||
|
@click.option("--file", "keys_file", default=None, metavar="PATH", help="Authorized keys file (default: ~/.config/browser-cli/authorized_keys).")
|
||||||
|
def cmd_auth_trust(pubkey, keys_file):
|
||||||
|
"""Add a public key to the authorized keys file on the serve host."""
|
||||||
|
from browser_cli.auth import DEFAULT_AUTHORIZED_KEYS_PATH, add_authorized_key
|
||||||
|
|
||||||
|
if len(pubkey) != 64:
|
||||||
|
console.print("[red]Invalid public key:[/red] expected 64 hex characters (Ed25519 raw public key)")
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
bytes.fromhex(pubkey)
|
||||||
|
except ValueError:
|
||||||
|
console.print("[red]Invalid public key:[/red] not valid hex")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
path = Path(keys_file) if keys_file else DEFAULT_AUTHORIZED_KEYS_PATH
|
||||||
|
added = add_authorized_key(path, pubkey)
|
||||||
|
if added:
|
||||||
|
console.print(f"[green]✓[/green] Trusted: [cyan]{pubkey}[/cyan]")
|
||||||
|
console.print(f" File: {path}")
|
||||||
|
console.print(f"\nStart the server with:")
|
||||||
|
console.print(f" [dim]browser-cli serve --authorized-keys {path}[/dim]")
|
||||||
|
else:
|
||||||
|
console.print(f"[yellow]Already trusted:[/yellow] {pubkey}")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_group.command("show")
|
||||||
|
@click.option("--key", "key_src", default=None, metavar="PATH|agent[:<selector>]",
|
||||||
|
help="Key source: path to PEM file, 'agent', or 'agent:<comment-filter>'.")
|
||||||
|
def cmd_auth_show(key_src):
|
||||||
|
"""Print the Ed25519 public key that browser-cli will use for auth."""
|
||||||
|
from browser_cli.auth import DEFAULT_KEY_PATH, agent_find_key, load_private_key, public_key_hex
|
||||||
|
|
||||||
|
src = key_src or os.environ.get("BROWSER_CLI_KEY", str(DEFAULT_KEY_PATH))
|
||||||
|
|
||||||
|
if src == "agent" or src.startswith("agent:"):
|
||||||
|
selector = src[6:] or None
|
||||||
|
key = agent_find_key(selector)
|
||||||
|
if key is None:
|
||||||
|
console.print("[red]No Ed25519 key found in SSH agent.[/red]")
|
||||||
|
console.print(" Make sure gpg-agent / ssh-agent is running and the key is loaded.")
|
||||||
|
sys.exit(1)
|
||||||
|
console.print(f"[dim]source:[/dim] agent ({key.comment})")
|
||||||
|
console.print(public_key_hex(key))
|
||||||
|
return
|
||||||
|
|
||||||
|
path = Path(src)
|
||||||
|
if not path.exists():
|
||||||
|
console.print(f"[red]No key found at {path}[/red]")
|
||||||
|
console.print(" Run: [dim]browser-cli auth keygen[/dim]")
|
||||||
|
console.print(" Or use: [dim]browser-cli auth show --key agent[/dim]")
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
priv = load_private_key(path)
|
||||||
|
console.print(f"[dim]source:[/dim] {path}")
|
||||||
|
console.print(public_key_hex(priv))
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Failed to load key:[/red] {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_group.command("keys")
|
||||||
|
def cmd_auth_keys():
|
||||||
|
"""List all Ed25519 keys available for pubkey auth (file + SSH agent)."""
|
||||||
|
from browser_cli.auth import DEFAULT_KEY_PATH, agent_list_keys, load_private_key, public_key_hex
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
table = Table(show_header=True, header_style="bold cyan")
|
||||||
|
table.add_column("Source")
|
||||||
|
table.add_column("Comment / Path")
|
||||||
|
table.add_column("Public Key")
|
||||||
|
|
||||||
|
# File key
|
||||||
|
if DEFAULT_KEY_PATH.exists():
|
||||||
|
try:
|
||||||
|
priv = load_private_key(DEFAULT_KEY_PATH)
|
||||||
|
hex_key = public_key_hex(priv)
|
||||||
|
table.add_row("[green]file[/green]", str(DEFAULT_KEY_PATH), hex_key)
|
||||||
|
except Exception as e:
|
||||||
|
table.add_row("[red]file[/red]", str(DEFAULT_KEY_PATH), f"[red]{e}[/red]")
|
||||||
|
|
||||||
|
# Agent keys
|
||||||
|
try:
|
||||||
|
agent_keys = agent_list_keys()
|
||||||
|
for k in agent_keys:
|
||||||
|
table.add_row("[cyan]agent[/cyan]", k.comment, public_key_hex(k))
|
||||||
|
except Exception as e:
|
||||||
|
table.add_row("[dim]agent[/dim]", f"[dim]{e}[/dim]", "")
|
||||||
|
|
||||||
|
if table.row_count == 0:
|
||||||
|
console.print("[yellow]No keys found.[/yellow] Run: [dim]browser-cli auth keygen[/dim]")
|
||||||
|
return
|
||||||
|
console.print(table)
|
||||||
|
console.print("\nTo trust a key on the serve host:")
|
||||||
|
console.print(" [dim]browser-cli auth trust <public-key>[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
main.add_command(auth_group)
|
||||||
|
|
||||||
# ── Sub-command groups ─────────────────────────────────────────────────────────
|
# ── Sub-command groups ─────────────────────────────────────────────────────────
|
||||||
main.add_command(nav_group)
|
main.add_command(nav_group)
|
||||||
main.add_command(tabs_group)
|
main.add_command(tabs_group)
|
||||||
@@ -247,6 +379,7 @@ def clients_group(ctx):
|
|||||||
browser_alias = (ctx.obj or {}).get("browser")
|
browser_alias = (ctx.obj or {}).get("browser")
|
||||||
remote = (ctx.obj or {}).get("remote") or os.environ.get("BROWSER_CLI_REMOTE")
|
remote = (ctx.obj or {}).get("remote") or os.environ.get("BROWSER_CLI_REMOTE")
|
||||||
token = (ctx.obj or {}).get("token") or os.environ.get("BROWSER_CLI_TOKEN")
|
token = (ctx.obj or {}).get("token") or os.environ.get("BROWSER_CLI_TOKEN")
|
||||||
|
key = (ctx.obj or {}).get("key")
|
||||||
|
|
||||||
if not remote and browser_alias:
|
if not remote and browser_alias:
|
||||||
# --browser <host> without --remote: resolve host alias to a remote endpoint,
|
# --browser <host> without --remote: resolve host alias to a remote endpoint,
|
||||||
@@ -261,7 +394,7 @@ def clients_group(ctx):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
for target in targets:
|
for target in targets:
|
||||||
try:
|
try:
|
||||||
result = send_command("clients.list", profile=target.profile, remote=resolved.remote, token=resolved_token)
|
result = send_command("clients.list", profile=target.profile, remote=resolved.remote, token=resolved_token, key=key)
|
||||||
for c in (result or []):
|
for c in (result or []):
|
||||||
c["profile"] = target.display_name
|
c["profile"] = target.display_name
|
||||||
all_clients.append(c)
|
all_clients.append(c)
|
||||||
@@ -269,7 +402,7 @@ def clients_group(ctx):
|
|||||||
continue
|
continue
|
||||||
elif remote:
|
elif remote:
|
||||||
try:
|
try:
|
||||||
result = send_command("clients.list", profile=browser_alias, remote=remote, token=token)
|
result = send_command("clients.list", profile=browser_alias, remote=remote, token=token, key=key)
|
||||||
for c in (result or []):
|
for c in (result or []):
|
||||||
c["profile"] = c.get("profile") or browser_alias or "remote"
|
c["profile"] = c.get("profile") or browser_alias or "remote"
|
||||||
all_clients.append(c)
|
all_clients.append(c)
|
||||||
|
|||||||
+63
-9
@@ -23,6 +23,7 @@ from browser_cli.registry import load_registry
|
|||||||
|
|
||||||
REGISTRY_PATH = registry_path()
|
REGISTRY_PATH = registry_path()
|
||||||
REMOTE_REGISTRY_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "browser-cli" / "remotes.json"
|
REMOTE_REGISTRY_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "browser-cli" / "remotes.json"
|
||||||
|
_DEFAULT_KEY_PATH = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "browser-cli" / "client.key.pem"
|
||||||
|
|
||||||
|
|
||||||
class BrowserNotConnected(Exception):
|
class BrowserNotConnected(Exception):
|
||||||
@@ -72,11 +73,9 @@ def save_remote_token(endpoint: str, token: str | None) -> None:
|
|||||||
current["token"] = token
|
current["token"] = token
|
||||||
remotes[endpoint] = current
|
remotes[endpoint] = current
|
||||||
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
REMOTE_REGISTRY_PATH.write_text(json.dumps(remotes, indent=2, sort_keys=True), encoding="utf-8")
|
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
try:
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
REMOTE_REGISTRY_PATH.chmod(0o600)
|
f.write(json.dumps(remotes, indent=2, sort_keys=True))
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def token_for_remote(endpoint: str | None) -> str | None:
|
def token_for_remote(endpoint: str | None) -> str | None:
|
||||||
@@ -193,12 +192,61 @@ def _resolve_socket(profile: str | None = None) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _send_remote(endpoint: str, framed: bytes) -> bytes:
|
def _load_private_key(key_path: "Path | str | None" = None):
|
||||||
|
"""Load an Ed25519 signing key.
|
||||||
|
|
||||||
|
Sources (in priority order):
|
||||||
|
1. Explicit key_path / --key flag
|
||||||
|
2. BROWSER_CLI_KEY environment variable
|
||||||
|
3. Default PEM file (~/.config/browser-cli/client.key.pem)
|
||||||
|
|
||||||
|
Pass "agent" or "agent:<selector>" to use a key from the SSH agent
|
||||||
|
(works with YubiKey via gpg-agent, TPM, or regular ssh-agent).
|
||||||
|
"""
|
||||||
|
raw = str(key_path) if key_path is not None else os.environ.get("BROWSER_CLI_KEY", str(_DEFAULT_KEY_PATH))
|
||||||
|
|
||||||
|
if raw == "agent" or raw.startswith("agent:"):
|
||||||
|
selector = raw[6:] or None # "agent:cardno:..." → "cardno:..."
|
||||||
|
from browser_cli.auth import agent_find_key
|
||||||
|
return agent_find_key(selector)
|
||||||
|
|
||||||
|
path = Path(raw)
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from browser_cli.auth import load_private_key
|
||||||
|
return load_private_key(path)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _send_remote(endpoint: str, msg: dict, private_key=None) -> bytes | None:
|
||||||
host, _, port_str = endpoint.rpartition(":")
|
host, _, port_str = endpoint.rpartition(":")
|
||||||
if not host or not port_str:
|
if not host or not port_str:
|
||||||
raise BrowserNotConnected(f"Invalid remote endpoint '{endpoint}': expected host:port")
|
raise BrowserNotConnected(f"Invalid remote endpoint '{endpoint}': expected host:port")
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.settimeout(30)
|
||||||
sock.connect((host, int(port_str)))
|
sock.connect((host, int(port_str)))
|
||||||
|
|
||||||
|
# receive challenge
|
||||||
|
challenge_raw = _recv_all(sock)
|
||||||
|
if challenge_raw is None:
|
||||||
|
raise BrowserNotConnected(f"No challenge received from {endpoint}")
|
||||||
|
try:
|
||||||
|
challenge = json.loads(challenge_raw)
|
||||||
|
nonce_hex = challenge.get("nonce") if challenge.get("type") == "challenge" else None
|
||||||
|
except (json.JSONDecodeError, AttributeError):
|
||||||
|
nonce_hex = None
|
||||||
|
|
||||||
|
if nonce_hex and private_key is not None:
|
||||||
|
from browser_cli.auth import sign, public_key_hex
|
||||||
|
nonce = bytes.fromhex(nonce_hex)
|
||||||
|
clean_msg = {k: v for k, v in msg.items() if k not in {"token", "pubkey", "sig"}}
|
||||||
|
sig = sign(private_key, nonce, clean_msg)
|
||||||
|
msg = {**clean_msg, "pubkey": public_key_hex(private_key), "sig": sig.hex()}
|
||||||
|
|
||||||
|
payload = json.dumps(msg).encode("utf-8")
|
||||||
|
framed = struct.pack("<I", len(payload)) + payload
|
||||||
sock.sendall(framed)
|
sock.sendall(framed)
|
||||||
return _recv_all(sock)
|
return _recv_all(sock)
|
||||||
|
|
||||||
@@ -217,7 +265,7 @@ def _auto_route_remote(endpoint: str, token: str | None) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def send_command(command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, token: str | None = None) -> Any:
|
def send_command(command: str, args: dict | None = None, profile: str | None = None, remote: str | None = None, token: str | None = None, key: "Path | None" = None) -> Any:
|
||||||
"""Send a command to the browser and return the response data."""
|
"""Send a command to the browser and return the response data."""
|
||||||
requested_profile = profile or os.environ.get("BROWSER_CLI_PROFILE")
|
requested_profile = profile or os.environ.get("BROWSER_CLI_PROFILE")
|
||||||
remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
|
remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
|
||||||
@@ -235,19 +283,23 @@ def send_command(command: str, args: dict | None = None, profile: str | None = N
|
|||||||
"args": args or {},
|
"args": args or {},
|
||||||
}
|
}
|
||||||
if remote_endpoint:
|
if remote_endpoint:
|
||||||
if resolved_token:
|
private_key = _load_private_key(key)
|
||||||
|
# use token auth only when no Ed25519 key is available
|
||||||
|
if private_key is None and resolved_token:
|
||||||
msg["token"] = resolved_token
|
msg["token"] = resolved_token
|
||||||
route_profile = requested_profile
|
route_profile = requested_profile
|
||||||
if not route_profile and command != "browser-cli.targets":
|
if not route_profile and command != "browser-cli.targets":
|
||||||
route_profile = _auto_route_remote(remote_endpoint, resolved_token)
|
route_profile = _auto_route_remote(remote_endpoint, resolved_token)
|
||||||
if route_profile:
|
if route_profile:
|
||||||
msg["_route"] = route_profile
|
msg["_route"] = route_profile
|
||||||
|
else:
|
||||||
|
private_key = None
|
||||||
payload = json.dumps(msg).encode("utf-8")
|
payload = json.dumps(msg).encode("utf-8")
|
||||||
framed = struct.pack("<I", len(payload)) + payload
|
framed = struct.pack("<I", len(payload)) + payload
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if remote_endpoint:
|
if remote_endpoint:
|
||||||
response = _send_remote(remote_endpoint, framed)
|
response = _send_remote(remote_endpoint, msg, private_key)
|
||||||
elif is_windows():
|
elif is_windows():
|
||||||
sock_path = _resolve_socket(profile)
|
sock_path = _resolve_socket(profile)
|
||||||
with PipeClient(sock_path, family="AF_PIPE") as conn:
|
with PipeClient(sock_path, family="AF_PIPE") as conn:
|
||||||
@@ -275,6 +327,8 @@ def send_command(command: str, args: dict | None = None, profile: str | None = N
|
|||||||
" Tip: use BROWSER_CLI_PROFILE=<name> to select a specific profile"
|
" Tip: use BROWSER_CLI_PROFILE=<name> to select a specific profile"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
raise ConnectionError("Connection closed before full response received")
|
||||||
result = json.loads(response)
|
result = json.loads(response)
|
||||||
if not result.get("success", True):
|
if not result.get("success", True):
|
||||||
raise RuntimeError(result.get("error", "unknown error from browser"))
|
raise RuntimeError(result.get("error", "unknown error from browser"))
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import threading, secrets, socket, struct, click, json, sys
|
import hmac, threading, secrets, socket, struct, click, json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_CONN_LIMIT = threading.BoundedSemaphore(64)
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ def _log(addr:tuple, command:str, profile:str|None, status:str, error:str|None=N
|
|||||||
else:
|
else:
|
||||||
console.print(f"[dim]{ts}[/dim] {addr_str} {profile_str}[cyan]{command}[/cyan] [green]{status}[/green]")
|
console.print(f"[dim]{ts}[/dim] {addr_str} {profile_str}[cyan]{command}[/cyan] [green]{status}[/green]")
|
||||||
|
|
||||||
def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None) -> None:
|
def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None, auth_keys:list[str]|None, nonce:str) -> None:
|
||||||
from browser_cli.client import _resolve_socket, BrowserNotConnected
|
from browser_cli.client import _resolve_socket, BrowserNotConnected
|
||||||
from browser_cli.platform import is_windows
|
from browser_cli.platform import is_windows
|
||||||
|
|
||||||
@@ -50,8 +53,25 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
|||||||
msg_id = msg.get("id")
|
msg_id = msg.get("id")
|
||||||
command = msg.get("command", "?")
|
command = msg.get("command", "?")
|
||||||
|
|
||||||
if server_token is not None:
|
# ── auth ──────────────────────────────────────────────────────────────────
|
||||||
if msg.get("token") != server_token:
|
if auth_keys is not None:
|
||||||
|
pub = msg.get("pubkey") or ""
|
||||||
|
sig = msg.get("sig") or ""
|
||||||
|
if not pub or not sig:
|
||||||
|
_send_error(msg_id, "unauthorized: pubkey auth required — run 'browser-cli auth keygen' on the client")
|
||||||
|
_log(addr, command, None, "DENIED", "missing pubkey/sig")
|
||||||
|
return
|
||||||
|
if pub not in auth_keys:
|
||||||
|
_send_error(msg_id, "unauthorized: untrusted public key")
|
||||||
|
_log(addr, command, None, "DENIED", "untrusted key")
|
||||||
|
return
|
||||||
|
from browser_cli.auth import verify
|
||||||
|
if not verify(pub, bytes.fromhex(nonce), msg, sig):
|
||||||
|
_send_error(msg_id, "unauthorized: invalid signature")
|
||||||
|
_log(addr, command, None, "DENIED", "bad signature")
|
||||||
|
return
|
||||||
|
elif server_token is not None:
|
||||||
|
if not hmac.compare_digest(msg.get("token") or "", server_token):
|
||||||
_send_error(msg_id, "unauthorized: invalid or missing token")
|
_send_error(msg_id, "unauthorized: invalid or missing token")
|
||||||
_log(addr, command, None, "DENIED", "bad token")
|
_log(addr, command, None, "DENIED", "bad token")
|
||||||
return
|
return
|
||||||
@@ -69,7 +89,7 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
|||||||
|
|
||||||
resolved_profile = msg.get("_route") or profile
|
resolved_profile = msg.get("_route") or profile
|
||||||
|
|
||||||
strip = {"token", "_route"}
|
strip = {"token", "_route", "pubkey", "sig"}
|
||||||
if strip & msg.keys():
|
if strip & msg.keys():
|
||||||
clean_payload = json.dumps({k: v for k, v in msg.items() if k not in strip}).encode()
|
clean_payload = json.dumps({k: v for k, v in msg.items() if k not in strip}).encode()
|
||||||
clean_header = struct.pack("<I", len(clean_payload))
|
clean_header = struct.pack("<I", len(clean_payload))
|
||||||
@@ -105,30 +125,54 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, serv
|
|||||||
_log(addr, command, resolved_profile, "OK")
|
_log(addr, command, resolved_profile, "OK")
|
||||||
else:
|
else:
|
||||||
_log(addr, command, resolved_profile, "ERROR", resp_data.get("error", ""))
|
_log(addr, command, resolved_profile, "ERROR", resp_data.get("error", ""))
|
||||||
except OSError as e:
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
_send_error(msg_id, str(e))
|
_send_error(msg_id, str(e))
|
||||||
_log(addr, command, resolved_profile, "ERROR", str(e))
|
_log(addr, command, resolved_profile, "ERROR", str(e))
|
||||||
|
|
||||||
def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None) -> None:
|
def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, server_token:str|None, auth_keys:list[str]|None) -> None:
|
||||||
|
if not _CONN_LIMIT.acquire(blocking=False):
|
||||||
|
client_sock.close()
|
||||||
|
return
|
||||||
|
client_sock.settimeout(30)
|
||||||
|
try:
|
||||||
with client_sock:
|
with client_sock:
|
||||||
_proxy_request(client_sock, addr, profile, server_token)
|
nonce = secrets.token_hex(32)
|
||||||
|
challenge = json.dumps({"type": "challenge", "nonce": nonce}).encode()
|
||||||
|
try:
|
||||||
|
client_sock.sendall(struct.pack("<I", len(challenge)) + challenge)
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
_proxy_request(client_sock, addr, profile, server_token, auth_keys, nonce)
|
||||||
|
finally:
|
||||||
|
_CONN_LIMIT.release()
|
||||||
|
|
||||||
@click.command("serve")
|
@click.command("serve")
|
||||||
@click.option("--host", default="127.0.0.1", show_default=True, help="Address to bind.")
|
@click.option("--host", default="127.0.0.1", show_default=True, help="Address to bind.")
|
||||||
@click.option("--port", default=8765, show_default=True, type=int, help="TCP port to listen on.")
|
@click.option("--port", default=8765, show_default=True, type=int, help="TCP port to listen on.")
|
||||||
@click.option("--token", default=None, metavar="TOKEN", help="Auth token (auto-generated if omitted).")
|
@click.option("--token", default=None, metavar="TOKEN", help="Auth token (auto-generated if omitted).")
|
||||||
@click.option("--no-auth", is_flag=True, default=False, help="Disable token authentication.")
|
@click.option("--no-auth", is_flag=True, default=False, help="Disable authentication (dangerous).")
|
||||||
|
@click.option("--authorized-keys", "auth_keys_file", default=None, metavar="FILE",
|
||||||
|
help="File of trusted Ed25519 public keys (one hex per line). Enables pubkey auth.")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cmd_serve(ctx, host, port, token, no_auth):
|
def cmd_serve(ctx, host, port, token, no_auth, auth_keys_file):
|
||||||
"""Expose this browser over TCP so remote hosts can control it."""
|
"""Expose this browser over TCP so remote hosts can control it."""
|
||||||
profile = ctx.obj.get("browser") if ctx.obj else None
|
profile = ctx.obj.get("browser") if ctx.obj else None
|
||||||
|
|
||||||
if host in ("0.0.0.0", "::"):
|
if host in ("0.0.0.0", "::"):
|
||||||
console.print("[yellow]Warning:[/yellow] Binding to all interfaces — anyone who can reach this port controls your browser.")
|
console.print("[yellow]Warning:[/yellow] Binding to all interfaces — anyone who can reach this port controls your browser.")
|
||||||
|
|
||||||
if no_auth:
|
if auth_keys_file:
|
||||||
|
from browser_cli.auth import load_authorized_keys
|
||||||
|
path = Path(auth_keys_file)
|
||||||
|
auth_keys = load_authorized_keys(path)
|
||||||
|
if not auth_keys:
|
||||||
|
console.print(f"[yellow]Warning:[/yellow] No authorized keys found in {path}")
|
||||||
|
server_token = None
|
||||||
|
elif no_auth:
|
||||||
|
auth_keys = None
|
||||||
server_token = None
|
server_token = None
|
||||||
else:
|
else:
|
||||||
|
auth_keys = None
|
||||||
server_token = token or secrets.token_urlsafe(32)
|
server_token = token or secrets.token_urlsafe(32)
|
||||||
|
|
||||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
@@ -136,6 +180,7 @@ def cmd_serve(ctx, host, port, token, no_auth):
|
|||||||
try:
|
try:
|
||||||
server.bind((host, port))
|
server.bind((host, port))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
server.close()
|
||||||
console.print(f"[red]Cannot bind to {host}:{port}:[/red] {e}")
|
console.print(f"[red]Cannot bind to {host}:{port}:[/red] {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
server.listen(16)
|
server.listen(16)
|
||||||
@@ -143,7 +188,11 @@ def cmd_serve(ctx, host, port, token, no_auth):
|
|||||||
browser_hint = f" (browser: {profile})" if profile else ""
|
browser_hint = f" (browser: {profile})" if profile else ""
|
||||||
console.print(f"[green]Serving browser{browser_hint} →[/green] [cyan]{host}:{port}[/cyan]")
|
console.print(f"[green]Serving browser{browser_hint} →[/green] [cyan]{host}:{port}[/cyan]")
|
||||||
|
|
||||||
if server_token:
|
if auth_keys is not None:
|
||||||
|
console.print(f" Auth: [bold green]Ed25519 pubkey[/bold green] ({len(auth_keys)} trusted key{'s' if len(auth_keys) != 1 else ''})")
|
||||||
|
console.print(f" CLI: [dim]browser-cli --remote {host}:{port} tabs list[/dim]")
|
||||||
|
console.print(f" Python: [dim]BrowserCLI(remote=\"{host}:{port}\").tabs_list()[/dim]")
|
||||||
|
elif server_token:
|
||||||
console.print(f" Token: [bold yellow]{server_token}[/bold yellow]")
|
console.print(f" Token: [bold yellow]{server_token}[/bold yellow]")
|
||||||
console.print(f" CLI: [dim]browser-cli --remote {host}:{port} --token {server_token} tabs list[/dim]")
|
console.print(f" CLI: [dim]browser-cli --remote {host}:{port} --token {server_token} tabs list[/dim]")
|
||||||
console.print(f" Python: [dim]BrowserCLI(remote=\"{host}:{port}\", token=\"{server_token}\").tabs_list()[/dim]")
|
console.print(f" Python: [dim]BrowserCLI(remote=\"{host}:{port}\", token=\"{server_token}\").tabs_list()[/dim]")
|
||||||
@@ -157,7 +206,7 @@ def cmd_serve(ctx, host, port, token, no_auth):
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
conn, addr = server.accept()
|
conn, addr = server.accept()
|
||||||
threading.Thread(target=_handle_client, args=(conn, addr, profile, server_token), daemon=True).start()
|
threading.Thread(target=_handle_client, args=(conn, addr, profile, server_token, auth_keys), daemon=True).start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
console.print("[yellow]Stopped.[/yellow]")
|
console.print("[yellow]Stopped.[/yellow]")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import click
|
import click
|
||||||
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
from browser_cli.client import BrowserNotConnected, active_browser_targets, remote_browser_targets, send_command
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@@ -288,7 +289,12 @@ def tabs_screenshot(output, tab_id, fmt, quality):
|
|||||||
data_url = result.get("dataUrl", "") if isinstance(result, dict) else ""
|
data_url = result.get("dataUrl", "") if isinstance(result, dict) else ""
|
||||||
if output:
|
if output:
|
||||||
header = f"data:image/{fmt};base64,"
|
header = f"data:image/{fmt};base64,"
|
||||||
|
if not data_url.startswith(header):
|
||||||
|
raise click.ClickException("Empty or unexpected screenshot response (incognito/protected tab?)")
|
||||||
|
try:
|
||||||
raw = base64.b64decode(data_url[len(header):])
|
raw = base64.b64decode(data_url[len(header):])
|
||||||
|
except binascii.Error as e:
|
||||||
|
raise click.ClickException(f"Failed to decode screenshot data: {e}")
|
||||||
with open(output, "wb") as f:
|
with open(output, "wb") as f:
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
console.print(f"[green]Screenshot saved:[/green] {output}")
|
console.print(f"[green]Screenshot saved:[/green] {output}")
|
||||||
|
|||||||
+42
-15
@@ -7,6 +7,7 @@ It relays messages between extension (stdin/stdout Native Messaging protocol)
|
|||||||
and CLI (local IPC endpoint: Unix socket on Unix, named pipe on Windows).
|
and CLI (local IPC endpoint: Unix socket on Unix, named pipe on Windows).
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import socket
|
import socket
|
||||||
@@ -46,13 +47,23 @@ PAGEABLE_COMMANDS = {
|
|||||||
|
|
||||||
# --- Native Messaging protocol (4-byte LE length prefix + UTF-8 JSON) ---
|
# --- Native Messaging protocol (4-byte LE length prefix + UTF-8 JSON) ---
|
||||||
|
|
||||||
|
def _read_exact_stream(stream, n: int) -> bytes | None:
|
||||||
|
buf = b""
|
||||||
|
while len(buf) < n:
|
||||||
|
chunk = stream.read(n - len(buf))
|
||||||
|
if not chunk:
|
||||||
|
return None # real EOF
|
||||||
|
buf += chunk
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
def read_native_message(stream) -> dict | None:
|
def read_native_message(stream) -> dict | None:
|
||||||
raw_len = stream.read(4)
|
raw_len = _read_exact_stream(stream, 4)
|
||||||
if len(raw_len) < 4:
|
if raw_len is None:
|
||||||
return None
|
return None
|
||||||
msg_len = struct.unpack("<I", raw_len)[0]
|
msg_len = struct.unpack("<I", raw_len)[0]
|
||||||
data = stream.read(msg_len)
|
data = _read_exact_stream(stream, msg_len)
|
||||||
if len(data) < msg_len:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
return json.loads(data.decode("utf-8"))
|
return json.loads(data.decode("utf-8"))
|
||||||
|
|
||||||
@@ -121,21 +132,28 @@ def stdin_reader(alias: str):
|
|||||||
|
|
||||||
# --- Thread B: accept CLI socket connections ---
|
# --- Thread B: accept CLI socket connections ---
|
||||||
|
|
||||||
def socket_server(sock_path: str):
|
def socket_server(sock_path: str, bound_sock: "socket.socket | None" = None):
|
||||||
if is_windows():
|
if is_windows():
|
||||||
while True:
|
while True:
|
||||||
|
listener = None
|
||||||
try:
|
try:
|
||||||
listener = Listener(sock_path, family="AF_PIPE")
|
listener = Listener(sock_path, family="AF_PIPE")
|
||||||
conn = listener.accept()
|
conn = listener.accept()
|
||||||
except OSError:
|
except OSError:
|
||||||
|
if listener is not None:
|
||||||
|
try:
|
||||||
|
listener.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
break
|
break
|
||||||
threading.Thread(target=handle_cli_connection, args=(conn, listener), daemon=True).start()
|
threading.Thread(target=handle_cli_connection, args=(conn, listener), daemon=True).start()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
sock = bound_sock
|
||||||
|
if sock is None:
|
||||||
path = Path(sock_path)
|
path = Path(sock_path)
|
||||||
if path.exists():
|
if path.exists():
|
||||||
path.unlink()
|
path.unlink()
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.bind(sock_path)
|
sock.bind(sock_path)
|
||||||
sock.listen(16)
|
sock.listen(16)
|
||||||
@@ -212,8 +230,13 @@ def _collect_paged_browser_command(cmd: dict) -> dict:
|
|||||||
offset = 0
|
offset = 0
|
||||||
items = []
|
items = []
|
||||||
total = None
|
total = None
|
||||||
|
max_pages = math.ceil(10_000 / PAGE_SIZE)
|
||||||
|
pages_fetched = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
if pages_fetched >= max_pages:
|
||||||
|
return {"id": original_id, "success": False, "error": f"paging loop exceeded {max_pages} pages — extension bug?"}
|
||||||
|
pages_fetched += 1
|
||||||
page_cmd = dict(cmd)
|
page_cmd = dict(cmd)
|
||||||
page_cmd["id"] = str(uuid.uuid4())
|
page_cmd["id"] = str(uuid.uuid4())
|
||||||
page_args = dict(cmd.get("args") or {})
|
page_args = dict(cmd.get("args") or {})
|
||||||
@@ -284,21 +307,25 @@ def main():
|
|||||||
if first_msg and first_msg.get("type") == "hello":
|
if first_msg and first_msg.get("type") == "hello":
|
||||||
alias = _resolve_profile_alias(first_msg)
|
alias = _resolve_profile_alias(first_msg)
|
||||||
else:
|
else:
|
||||||
# No hello — use a generated alias and re-queue the first command if needed.
|
# No hello — use a generated alias; first_msg is dropped (no response path).
|
||||||
alias = str(uuid.uuid4())
|
alias = str(uuid.uuid4())
|
||||||
if first_msg:
|
|
||||||
msg_id = first_msg.get("id")
|
|
||||||
if msg_id:
|
|
||||||
q: queue.Queue = queue.Queue()
|
|
||||||
with PENDING_LOCK:
|
|
||||||
PENDING[msg_id] = q
|
|
||||||
write_native_message(sys.stdout.buffer, first_msg)
|
|
||||||
|
|
||||||
runtime_dir().mkdir(mode=0o700, exist_ok=True)
|
runtime_dir().mkdir(mode=0o700, exist_ok=True)
|
||||||
sock_path = _socket_path_for(alias)
|
sock_path = _socket_path_for(alias)
|
||||||
|
|
||||||
|
if not is_windows():
|
||||||
|
path = Path(sock_path)
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
bound_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
bound_sock.bind(sock_path)
|
||||||
|
bound_sock.listen(16)
|
||||||
|
else:
|
||||||
|
bound_sock = None
|
||||||
|
|
||||||
_registry_add(alias, sock_path)
|
_registry_add(alias, sock_path)
|
||||||
|
|
||||||
t = threading.Thread(target=socket_server, args=(sock_path,), daemon=True)
|
t = threading.Thread(target=socket_server, args=(sock_path,), kwargs={"bound_sock": bound_sock}, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
stdin_reader(alias)
|
stdin_reader(alias)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.8.6",
|
"version": "0.9.0",
|
||||||
"description": "Control your browser from the terminal via browser-cli",
|
"description": "Control your browser from the terminal via browser-cli",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ export async function sessionDiff({ nameA, nameB }) {
|
|||||||
|
|
||||||
export async function sessionAutoSave({ enabled }) {
|
export async function sessionAutoSave({ enabled }) {
|
||||||
await chrome.storage.local.set({ autoSave: enabled });
|
await chrome.storage.local.set({ autoSave: enabled });
|
||||||
|
chrome.tabs.onUpdated.removeListener(autoSaveHandler);
|
||||||
|
chrome.tabs.onRemoved.removeListener(autoSaveHandler);
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
chrome.tabs.onUpdated.addListener(autoSaveHandler);
|
chrome.tabs.onUpdated.addListener(autoSaveHandler);
|
||||||
chrome.tabs.onRemoved.addListener(autoSaveHandler);
|
chrome.tabs.onRemoved.addListener(autoSaveHandler);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export async function tabsClose({ tabId, inactive, duplicates }) {
|
|||||||
const all = await chrome.tabs.query({});
|
const all = await chrome.tabs.query({});
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const t of all) {
|
for (const t of all) {
|
||||||
|
if (!t.url) continue;
|
||||||
if (seen.has(t.url)) toClose.push(t.id);
|
if (seen.has(t.url)) toClose.push(t.id);
|
||||||
else seen.add(t.url);
|
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 === "title") return (a.title || "").localeCompare(b.title || "");
|
||||||
if (by === "time") return a.id - b.id; // lower id = opened earlier
|
if (by === "time") return a.id - b.id; // lower id = opened earlier
|
||||||
// domain (default)
|
// domain (default)
|
||||||
const da = new URL(a.url || "about:blank").hostname;
|
const da = new URL(a.url || a.pendingUrl || "about:blank").hostname;
|
||||||
const db = new URL(b.url || "about:blank").hostname;
|
const db = new URL(b.url || b.pendingUrl || "about:blank").hostname;
|
||||||
return da.localeCompare(db);
|
return da.localeCompare(db);
|
||||||
});
|
});
|
||||||
for (let i = 0; i < sorted.length; i++) {
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
@@ -146,7 +147,6 @@ export async function tabsSort({ by }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function tabsMergeWindows() {
|
export async function tabsMergeWindows() {
|
||||||
const [focused] = await chrome.windows.getAll({ populate: false });
|
|
||||||
const current = await chrome.windows.getCurrent();
|
const current = await chrome.windows.getCurrent();
|
||||||
const all = await chrome.windows.getAll({ populate: true });
|
const all = await chrome.windows.getAll({ populate: true });
|
||||||
let moved = 0;
|
let moved = 0;
|
||||||
|
|||||||
Generated
+116
-116
@@ -6,15 +6,15 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "browser-cli-extension-build",
|
"name": "browser-cli-extension-build",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.0.326",
|
"@types/chrome": "^0.1.40",
|
||||||
"esbuild": "^0.25.3",
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -46,9 +46,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -80,9 +80,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -97,9 +97,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -114,9 +114,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -131,9 +131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -148,9 +148,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -165,9 +165,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -182,9 +182,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -199,9 +199,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -216,9 +216,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -233,9 +233,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -250,9 +250,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -267,9 +267,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -284,9 +284,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -301,9 +301,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -318,9 +318,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -335,9 +335,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -352,9 +352,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -369,9 +369,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -386,9 +386,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -403,9 +403,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -420,9 +420,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -437,9 +437,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -454,9 +454,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/chrome": {
|
"node_modules/@types/chrome": {
|
||||||
"version": "0.0.326",
|
"version": "0.1.40",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.326.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.40.tgz",
|
||||||
"integrity": "sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw==",
|
"integrity": "sha512-UnfyRAe8ORu9HSuTH0EqyOEUin3JrWW9Nl/gDXezNfTUrfIoxw+WRZgKOxGz0t5BnjbfXBnS2eCYfW2PxH1wcA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -489,9 +489,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -502,38 +502,38 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.25.12",
|
"@esbuild/aix-ppc64": "0.28.0",
|
||||||
"@esbuild/android-arm": "0.25.12",
|
"@esbuild/android-arm": "0.28.0",
|
||||||
"@esbuild/android-arm64": "0.25.12",
|
"@esbuild/android-arm64": "0.28.0",
|
||||||
"@esbuild/android-x64": "0.25.12",
|
"@esbuild/android-x64": "0.28.0",
|
||||||
"@esbuild/darwin-arm64": "0.25.12",
|
"@esbuild/darwin-arm64": "0.28.0",
|
||||||
"@esbuild/darwin-x64": "0.25.12",
|
"@esbuild/darwin-x64": "0.28.0",
|
||||||
"@esbuild/freebsd-arm64": "0.25.12",
|
"@esbuild/freebsd-arm64": "0.28.0",
|
||||||
"@esbuild/freebsd-x64": "0.25.12",
|
"@esbuild/freebsd-x64": "0.28.0",
|
||||||
"@esbuild/linux-arm": "0.25.12",
|
"@esbuild/linux-arm": "0.28.0",
|
||||||
"@esbuild/linux-arm64": "0.25.12",
|
"@esbuild/linux-arm64": "0.28.0",
|
||||||
"@esbuild/linux-ia32": "0.25.12",
|
"@esbuild/linux-ia32": "0.28.0",
|
||||||
"@esbuild/linux-loong64": "0.25.12",
|
"@esbuild/linux-loong64": "0.28.0",
|
||||||
"@esbuild/linux-mips64el": "0.25.12",
|
"@esbuild/linux-mips64el": "0.28.0",
|
||||||
"@esbuild/linux-ppc64": "0.25.12",
|
"@esbuild/linux-ppc64": "0.28.0",
|
||||||
"@esbuild/linux-riscv64": "0.25.12",
|
"@esbuild/linux-riscv64": "0.28.0",
|
||||||
"@esbuild/linux-s390x": "0.25.12",
|
"@esbuild/linux-s390x": "0.28.0",
|
||||||
"@esbuild/linux-x64": "0.25.12",
|
"@esbuild/linux-x64": "0.28.0",
|
||||||
"@esbuild/netbsd-arm64": "0.25.12",
|
"@esbuild/netbsd-arm64": "0.28.0",
|
||||||
"@esbuild/netbsd-x64": "0.25.12",
|
"@esbuild/netbsd-x64": "0.28.0",
|
||||||
"@esbuild/openbsd-arm64": "0.25.12",
|
"@esbuild/openbsd-arm64": "0.28.0",
|
||||||
"@esbuild/openbsd-x64": "0.25.12",
|
"@esbuild/openbsd-x64": "0.28.0",
|
||||||
"@esbuild/openharmony-arm64": "0.25.12",
|
"@esbuild/openharmony-arm64": "0.28.0",
|
||||||
"@esbuild/sunos-x64": "0.25.12",
|
"@esbuild/sunos-x64": "0.28.0",
|
||||||
"@esbuild/win32-arm64": "0.25.12",
|
"@esbuild/win32-arm64": "0.28.0",
|
||||||
"@esbuild/win32-ia32": "0.25.12",
|
"@esbuild/win32-ia32": "0.28.0",
|
||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.28.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+3
-3
@@ -7,8 +7,8 @@
|
|||||||
"check:extension": "tsc -p tsconfig.json --noEmit && npm run build:extension && node --check extension/background.js"
|
"check:extension": "tsc -p tsconfig.json --noEmit && npm run build:extension && node --check extension/background.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.0.326",
|
"@types/chrome": "^0.1.40",
|
||||||
"esbuild": "^0.25.3",
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.8.6"
|
version = "0.9.0"
|
||||||
description = "Control your real running browser from the terminal via a browser extension"
|
description = "Control your real running browser from the terminal via a browser extension"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click>=8",
|
"click>=8",
|
||||||
|
"cryptography>=42",
|
||||||
"rich>=13",
|
"rich>=13",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+44
-44
@@ -65,7 +65,7 @@ class TestBrowserCLIInit:
|
|||||||
assert b._browser == "chrome"
|
assert b._browser == "chrome"
|
||||||
|
|
||||||
def test_remote_options_stored(self):
|
def test_remote_options_stored(self):
|
||||||
b = BrowserCLI(browser="work", remote="host:8765", token="secret")
|
b = BrowserCLI(browser="work", remote="host:8765", token="secret", key=None)
|
||||||
assert b._browser == "work"
|
assert b._browser == "work"
|
||||||
assert b._remote == "host:8765"
|
assert b._remote == "host:8765"
|
||||||
assert b._token == "secret"
|
assert b._token == "secret"
|
||||||
@@ -129,7 +129,7 @@ class TestNavigation:
|
|||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"navigate.open",
|
"navigate.open",
|
||||||
{"url": "https://example.com", "background": False, "window": None, "group": None},
|
{"url": "https://example.com", "background": False, "window": None, "group": None},
|
||||||
profile=None, remote=None, token=None,
|
profile=None, remote=None, token=None, key=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_open_background(self, b, mock_send):
|
def test_open_background(self, b, mock_send):
|
||||||
@@ -143,38 +143,38 @@ class TestNavigation:
|
|||||||
|
|
||||||
def test_reload(self, b, mock_send):
|
def test_reload(self, b, mock_send):
|
||||||
b.reload(tab_id=5)
|
b.reload(tab_id=5)
|
||||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_hard_reload(self, b, mock_send):
|
def test_hard_reload(self, b, mock_send):
|
||||||
b.hard_reload(tab_id=7)
|
b.hard_reload(tab_id=7)
|
||||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_back(self, b, mock_send):
|
def test_back(self, b, mock_send):
|
||||||
b.back(tab_id=3)
|
b.back(tab_id=3)
|
||||||
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_forward(self, b, mock_send):
|
def test_forward(self, b, mock_send):
|
||||||
b.forward(tab_id=3)
|
b.forward(tab_id=3)
|
||||||
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_focus_url(self, b, mock_send):
|
def test_focus_url(self, b, mock_send):
|
||||||
b.focus_url("github.com")
|
b.focus_url("github.com")
|
||||||
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_navigate_tab(self, b, mock_send):
|
def test_navigate_tab(self, b, mock_send):
|
||||||
b.navigate_tab(5, "https://example.com")
|
b.navigate_tab(5, "https://example.com")
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, token=None
|
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, token=None, key=None
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_profile_forwarded(self, b_profile, mock_send):
|
def test_profile_forwarded(self, b_profile, mock_send):
|
||||||
b_profile.reload()
|
b_profile.reload()
|
||||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_remote_forwarded(self, mock_send):
|
def test_remote_forwarded(self, mock_send):
|
||||||
b = BrowserCLI(browser="work", remote="host:8765", token="secret")
|
b = BrowserCLI(browser="work", remote="host:8765", token="secret", key=None)
|
||||||
b.reload()
|
b.reload()
|
||||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", token="secret")
|
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", token="secret", key=None)
|
||||||
|
|
||||||
|
|
||||||
# ── Search ────────────────────────────────────────────────────────────────────
|
# ── Search ────────────────────────────────────────────────────────────────────
|
||||||
@@ -207,12 +207,12 @@ class TestExtract:
|
|||||||
result = b.extract_markdown()
|
result = b.extract_markdown()
|
||||||
|
|
||||||
assert result == "# Title"
|
assert result == "# Title"
|
||||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_extract_markdown_selector(self, b, mock_send):
|
def test_extract_markdown_selector(self, b, mock_send):
|
||||||
b.extract_markdown("article")
|
b.extract_markdown("article")
|
||||||
|
|
||||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
|
|
||||||
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -247,7 +247,7 @@ class TestTabs:
|
|||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"tabs.close",
|
"tabs.close",
|
||||||
{"tabId": 10, "inactive": False, "duplicates": False},
|
{"tabId": 10, "inactive": False, "duplicates": False},
|
||||||
profile=None, remote=None, token=None,
|
profile=None, remote=None, token=None, key=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tabs_move(self, b, mock_send):
|
def test_tabs_move(self, b, mock_send):
|
||||||
@@ -255,19 +255,19 @@ class TestTabs:
|
|||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"tabs.move",
|
"tabs.move",
|
||||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||||
profile=None, remote=None, token=None,
|
profile=None, remote=None, token=None, key=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tabs_active(self, b, mock_send):
|
def test_tabs_active(self, b, mock_send):
|
||||||
b.tabs_active(10)
|
b.tabs_active(10)
|
||||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_window_active_tab(self, b, mock_send):
|
def test_window_active_tab(self, b, mock_send):
|
||||||
mock_send.return_value = TAB_DATA
|
mock_send.return_value = TAB_DATA
|
||||||
tab = b.window_active_tab(1)
|
tab = b.window_active_tab(1)
|
||||||
assert isinstance(tab, Tab)
|
assert isinstance(tab, Tab)
|
||||||
assert tab.id == 10
|
assert tab.id == 10
|
||||||
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_window_active_tab_missing_raises(self, b, mock_send):
|
def test_window_active_tab_missing_raises(self, b, mock_send):
|
||||||
mock_send.return_value = None
|
mock_send.return_value = None
|
||||||
@@ -319,11 +319,11 @@ class TestTabs:
|
|||||||
assert mock_send.call_args_list == [
|
assert mock_send.call_args_list == [
|
||||||
call("tabs.list", {}, profile="default"),
|
call("tabs.list", {}, profile="default"),
|
||||||
call("tabs.list", {}, profile="work"),
|
call("tabs.list", {}, profile="work"),
|
||||||
call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None),
|
call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None, key=None),
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_tabs_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
|
def test_tabs_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
|
||||||
b = BrowserCLI(remote="host:8765", token="secret")
|
b = BrowserCLI(remote="host:8765", token="secret", key=None)
|
||||||
with patch(
|
with patch(
|
||||||
"browser_cli.active_browser_targets",
|
"browser_cli.active_browser_targets",
|
||||||
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
||||||
@@ -337,8 +337,8 @@ class TestTabs:
|
|||||||
|
|
||||||
assert [tab.browser for tab in tabs] == ["host:work"]
|
assert [tab.browser for tab in tabs] == ["host:work"]
|
||||||
assert mock_send.call_args_list == [
|
assert mock_send.call_args_list == [
|
||||||
call("tabs.list", {}, profile="work", remote="host:8765", token="secret"),
|
call("tabs.list", {}, profile="work", remote="host:8765", token="secret", key=None),
|
||||||
call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", token="secret"),
|
call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", token="secret", key=None),
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||||
@@ -381,7 +381,7 @@ class TestTabs:
|
|||||||
|
|
||||||
def test_tabs_sort(self, b, mock_send):
|
def test_tabs_sort(self, b, mock_send):
|
||||||
b.tabs_sort(by="title")
|
b.tabs_sort(by="title")
|
||||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_tabs_merge_windows(self, b, mock_send):
|
def test_tabs_merge_windows(self, b, mock_send):
|
||||||
mock_send.return_value = {"moved": 4}
|
mock_send.return_value = {"moved": 4}
|
||||||
@@ -414,7 +414,7 @@ class TestGroups:
|
|||||||
mock_send.return_value = [TAB_DATA]
|
mock_send.return_value = [TAB_DATA]
|
||||||
tabs = b.group_tabs(42)
|
tabs = b.group_tabs(42)
|
||||||
assert isinstance(tabs[0], Tab)
|
assert isinstance(tabs[0], Tab)
|
||||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_group_count(self, b, mock_send):
|
def test_group_count(self, b, mock_send):
|
||||||
mock_send.return_value = 7
|
mock_send.return_value = 7
|
||||||
@@ -442,11 +442,11 @@ class TestGroups:
|
|||||||
assert mock_send.call_args_list == [
|
assert mock_send.call_args_list == [
|
||||||
call("group.list", {}, profile="default"),
|
call("group.list", {}, profile="default"),
|
||||||
call("group.list", {}, profile="work"),
|
call("group.list", {}, profile="work"),
|
||||||
call("group.close", {"groupId": 99}, profile="work", remote=None, token=None),
|
call("group.close", {"groupId": 99}, profile="work", remote=None, token=None, key=None),
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_group_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
|
def test_group_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
|
||||||
b = BrowserCLI(remote="host:8765", token="secret")
|
b = BrowserCLI(remote="host:8765", token="secret", key=None)
|
||||||
with patch(
|
with patch(
|
||||||
"browser_cli.active_browser_targets",
|
"browser_cli.active_browser_targets",
|
||||||
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
||||||
@@ -460,8 +460,8 @@ class TestGroups:
|
|||||||
|
|
||||||
assert [group.browser for group in groups] == ["host:work"]
|
assert [group.browser for group in groups] == ["host:work"]
|
||||||
assert mock_send.call_args_list == [
|
assert mock_send.call_args_list == [
|
||||||
call("group.list", {}, profile="work", remote="host:8765", token="secret"),
|
call("group.list", {}, profile="work", remote="host:8765", token="secret", key=None),
|
||||||
call("group.close", {"groupId": 42}, profile="work", remote="host:8765", token="secret"),
|
call("group.close", {"groupId": 42}, profile="work", remote="host:8765", token="secret", key=None),
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||||
@@ -484,7 +484,7 @@ class TestGroups:
|
|||||||
|
|
||||||
def test_group_close(self, b, mock_send):
|
def test_group_close(self, b, mock_send):
|
||||||
b.group_close(42)
|
b.group_close(42)
|
||||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_group_create_dict_response(self, b, mock_send):
|
def test_group_create_dict_response(self, b, mock_send):
|
||||||
mock_send.return_value = GROUP_DATA
|
mock_send.return_value = GROUP_DATA
|
||||||
@@ -504,7 +504,7 @@ class TestGroups:
|
|||||||
tab_id = b.group_add_tab(42, "https://example.com")
|
tab_id = b.group_add_tab(42, "https://example.com")
|
||||||
assert tab_id == 55
|
assert tab_id == 55
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, token=None
|
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, token=None, key=None
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_group_add_tab_non_dict_response(self, b, mock_send):
|
def test_group_add_tab_non_dict_response(self, b, mock_send):
|
||||||
@@ -514,7 +514,7 @@ class TestGroups:
|
|||||||
def test_group_move_forward(self, b, mock_send):
|
def test_group_move_forward(self, b, mock_send):
|
||||||
b.group_move(42, forward=True)
|
b.group_move(42, forward=True)
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None
|
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None, key=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -544,7 +544,7 @@ class TestWindows:
|
|||||||
result = b.windows_open()
|
result = b.windows_open()
|
||||||
|
|
||||||
assert result == {"id": 5}
|
assert result == {"id": 5}
|
||||||
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_windows_open_with_url(self, b, mock_send):
|
def test_windows_open_with_url(self, b, mock_send):
|
||||||
mock_send.return_value = {"id": 9}
|
mock_send.return_value = {"id": 9}
|
||||||
@@ -552,7 +552,7 @@ class TestWindows:
|
|||||||
result = b.windows_open("https://example.com")
|
result = b.windows_open("https://example.com")
|
||||||
|
|
||||||
assert result == {"id": 9}
|
assert result == {"id": 9}
|
||||||
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
|
|
||||||
class TestSession:
|
class TestSession:
|
||||||
@@ -562,7 +562,7 @@ class TestSession:
|
|||||||
result = b.session_list()
|
result = b.session_list()
|
||||||
|
|
||||||
assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}]
|
assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}]
|
||||||
mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_session_list_multi_browser_adds_browser(self, b, mock_send):
|
def test_session_list_multi_browser_adds_browser(self, b, mock_send):
|
||||||
with patch(
|
with patch(
|
||||||
@@ -597,26 +597,26 @@ class TestTabModel:
|
|||||||
|
|
||||||
def test_close(self, tab, mock_send):
|
def test_close(self, tab, mock_send):
|
||||||
tab.close()
|
tab.close()
|
||||||
mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_activate(self, tab, mock_send):
|
def test_activate(self, tab, mock_send):
|
||||||
tab.activate()
|
tab.activate()
|
||||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_reload(self, tab, mock_send):
|
def test_reload(self, tab, mock_send):
|
||||||
tab.reload()
|
tab.reload()
|
||||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_hard_reload(self, tab, mock_send):
|
def test_hard_reload(self, tab, mock_send):
|
||||||
tab.hard_reload()
|
tab.hard_reload()
|
||||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_move_forward(self, tab, mock_send):
|
def test_move_forward(self, tab, mock_send):
|
||||||
tab.move(forward=True)
|
tab.move(forward=True)
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"tabs.move",
|
"tabs.move",
|
||||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||||
profile=None, remote=None, token=None,
|
profile=None, remote=None, token=None, key=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_move_to_group(self, tab, mock_send):
|
def test_move_to_group(self, tab, mock_send):
|
||||||
@@ -626,12 +626,12 @@ class TestTabModel:
|
|||||||
def test_html(self, tab, mock_send):
|
def test_html(self, tab, mock_send):
|
||||||
mock_send.return_value = "<html/>"
|
mock_send.return_value = "<html/>"
|
||||||
assert tab.html() == "<html/>"
|
assert tab.html() == "<html/>"
|
||||||
mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_open(self, tab, mock_send):
|
def test_open(self, tab, mock_send):
|
||||||
tab.open("https://new.example.com")
|
tab.open("https://new.example.com")
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, token=None
|
"navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, token=None, key=None
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_open_background_changes_same_tab(self, tab, mock_send):
|
def test_open_background_changes_same_tab(self, tab, mock_send):
|
||||||
@@ -639,7 +639,7 @@ class TestTabModel:
|
|||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"navigate.to",
|
"navigate.to",
|
||||||
{"tabId": 10, "url": "https://new.example.com"},
|
{"tabId": 10, "url": "https://new.example.com"},
|
||||||
profile=None, remote=None, token=None,
|
profile=None, remote=None, token=None, key=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_unbound_raises(self):
|
def test_unbound_raises(self):
|
||||||
@@ -657,18 +657,18 @@ class TestGroupModel:
|
|||||||
|
|
||||||
def test_close(self, group, mock_send):
|
def test_close(self, group, mock_send):
|
||||||
group.close()
|
group.close()
|
||||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_tabs(self, group, mock_send):
|
def test_tabs(self, group, mock_send):
|
||||||
mock_send.return_value = [TAB_DATA]
|
mock_send.return_value = [TAB_DATA]
|
||||||
tabs = group.tabs()
|
tabs = group.tabs()
|
||||||
assert isinstance(tabs[0], Tab)
|
assert isinstance(tabs[0], Tab)
|
||||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None)
|
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||||
|
|
||||||
def test_move_forward(self, group, mock_send):
|
def test_move_forward(self, group, mock_send):
|
||||||
group.move(forward=True)
|
group.move(forward=True)
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None
|
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None, key=None
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_move_backward(self, group, mock_send):
|
def test_move_backward(self, group, mock_send):
|
||||||
|
|||||||
+3
-3
@@ -168,7 +168,7 @@ def test_clients_reads_registry_with_trailing_garbage(tmp_path):
|
|||||||
assert "0.8.2" in result.output
|
assert "0.8.2" in result.output
|
||||||
|
|
||||||
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
||||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None):
|
def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None):
|
||||||
assert command == "clients.list"
|
assert command == "clients.list"
|
||||||
assert profile is None
|
assert profile is None
|
||||||
assert remote == "127.0.0.1:8765"
|
assert remote == "127.0.0.1:8765"
|
||||||
@@ -194,7 +194,7 @@ def test_clients_remote_respects_global_browser_route():
|
|||||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
||||||
|
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", token=None)
|
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", token=None, key=None)
|
||||||
|
|
||||||
|
|
||||||
def test_clients_browser_alias_resolves_to_remote():
|
def test_clients_browser_alias_resolves_to_remote():
|
||||||
@@ -211,7 +211,7 @@ def test_clients_browser_alias_resolves_to_remote():
|
|||||||
)
|
)
|
||||||
all_remote_targets = [resolved_target]
|
all_remote_targets = [resolved_target]
|
||||||
|
|
||||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None):
|
def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None):
|
||||||
assert command == "clients.list"
|
assert command == "clients.list"
|
||||||
assert profile == "automatisation"
|
assert profile == "automatisation"
|
||||||
assert remote == "192.168.188.104:8765"
|
assert remote == "192.168.188.104:8765"
|
||||||
|
|||||||
+5
-11
@@ -118,15 +118,13 @@ def test_send_command_auto_routes_single_remote_target(monkeypatch):
|
|||||||
lambda endpoint, token=None: [BrowserTarget("work", "host:work", "", remote=endpoint, token=token)],
|
lambda endpoint, token=None: [BrowserTarget("work", "host:work", "", remote=endpoint, token=token)],
|
||||||
)
|
)
|
||||||
|
|
||||||
def fake_send_remote(endpoint, framed):
|
def fake_send_remote(endpoint, msg, private_key=None):
|
||||||
payload_len = int.from_bytes(framed[:4], "little")
|
|
||||||
msg = json.loads(framed[4:4 + payload_len])
|
|
||||||
sent.update(msg)
|
sent.update(msg)
|
||||||
return json.dumps({"success": True, "data": "ok"}).encode("utf-8")
|
return json.dumps({"success": True, "data": "ok"}).encode("utf-8")
|
||||||
|
|
||||||
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
|
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
|
||||||
|
|
||||||
assert send_command("tabs.list", remote="host:8765", token="secret") == "ok"
|
assert send_command("tabs.list", remote="host:8765", token="secret", key=None) == "ok"
|
||||||
assert sent["_route"] == "work"
|
assert sent["_route"] == "work"
|
||||||
assert sent["token"] == "secret"
|
assert sent["token"] == "secret"
|
||||||
|
|
||||||
@@ -142,9 +140,7 @@ def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch):
|
|||||||
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
|
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
|
||||||
)
|
)
|
||||||
|
|
||||||
def fake_send_remote(endpoint, framed):
|
def fake_send_remote(endpoint, msg, private_key=None):
|
||||||
payload_len = int.from_bytes(framed[:4], "little")
|
|
||||||
msg = json.loads(framed[4:4 + payload_len])
|
|
||||||
sent["endpoint"] = endpoint
|
sent["endpoint"] = endpoint
|
||||||
sent.update(msg)
|
sent.update(msg)
|
||||||
return json.dumps({"success": True, "data": []}).encode("utf-8")
|
return json.dumps({"success": True, "data": []}).encode("utf-8")
|
||||||
@@ -198,9 +194,7 @@ def test_send_command_resolves_host_alias_to_single_remote_target(monkeypatch):
|
|||||||
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint, token="secret")],
|
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint, token="secret")],
|
||||||
)
|
)
|
||||||
|
|
||||||
def fake_send_remote(endpoint, framed):
|
def fake_send_remote(endpoint, msg, private_key=None):
|
||||||
payload_len = int.from_bytes(framed[:4], "little")
|
|
||||||
msg = json.loads(framed[4:4 + payload_len])
|
|
||||||
sent["endpoint"] = endpoint
|
sent["endpoint"] = endpoint
|
||||||
sent.update(msg)
|
sent.update(msg)
|
||||||
return json.dumps({"success": True, "data": []}).encode("utf-8")
|
return json.dumps({"success": True, "data": []}).encode("utf-8")
|
||||||
@@ -246,7 +240,7 @@ def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path):
|
|||||||
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json")
|
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json")
|
||||||
monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path)
|
monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path)
|
||||||
|
|
||||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None):
|
def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None):
|
||||||
assert command == "browser-cli.targets"
|
assert command == "browser-cli.targets"
|
||||||
assert remote == endpoint
|
assert remote == endpoint
|
||||||
assert token == "secret-token"
|
assert token == "secret-token"
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ requires-python = ">=3.10"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.8.6"
|
version = "0.9.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
{ name = "cryptography" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -19,12 +20,95 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "click", specifier = ">=8" },
|
{ name = "click", specifier = ">=8" },
|
||||||
|
{ name = "cryptography", specifier = ">=42" },
|
||||||
{ name = "rich", specifier = ">=13" },
|
{ name = "rich", specifier = ">=13" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "pytest", specifier = ">=8" }]
|
dev = [{ name = "pytest", specifier = ">=8" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.3"
|
version = "8.3.3"
|
||||||
@@ -46,6 +130,66 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "47.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/a0/928c9ce0d120a40a81aa99e3ba383e87337b9ac9ef9f6db02e4d7822424d/cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", size = 3909893, upload-time = "2026-04-24T19:54:38.334Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -106,6 +250,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.20.0"
|
version = "2.20.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user