Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
371b794170
|
@@ -5,6 +5,11 @@ extension/test-dist/
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# Local secrets / signing keys
|
||||||
|
secrets/
|
||||||
|
*.pem
|
||||||
|
*.pem.gpg
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
@@ -517,10 +517,19 @@ Packaging:
|
|||||||
```bash
|
```bash
|
||||||
npm run package:extension # testing/unpacked zip, keeps manifest.key for stable Chromium native-messaging ID
|
npm run package:extension # testing/unpacked zip, keeps manifest.key for stable Chromium native-messaging ID
|
||||||
npm run package:extension:webstore # Chrome Web Store zip, strips manifest.key
|
npm run package:extension:webstore # Chrome Web Store zip, strips manifest.key
|
||||||
|
npm run package:extension:webstore:verified # Chrome Web Store CRX signed for verified uploads
|
||||||
npm run package:extension:firefox # Firefox zip, strips manifest.key and Firefox-incompatible permissions
|
npm run package:extension:firefox # Firefox zip, strips manifest.key and Firefox-incompatible permissions
|
||||||
```
|
```
|
||||||
|
|
||||||
Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `dist/`. For Firefox, use the `*-firefox-*` zip.
|
Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `dist/`. For verified CRX uploads, create a dedicated RSA upload key once and protect it with your GPG key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/setup_verified_crx_key.sh --recipient '<your GPG key id or email>'
|
||||||
|
# Add the generated public key in Chrome Developer Dashboard -> Package -> Verified uploads.
|
||||||
|
npm run package:extension:webstore:verified
|
||||||
|
```
|
||||||
|
|
||||||
|
The verified-upload private key is not a GPG key; Chrome requires an RSA CRX signing key. GPG is used here to encrypt that RSA private key at rest. The signed `*.crx` from `dist/` is the upload artifact after verified uploads are enabled. For Firefox, use the `*-firefox-*` zip.
|
||||||
|
|
||||||
For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `npm run package:extension:firefox` first and load `dist/extension-package-firefox/manifest.json`. Do **not** load `extension/manifest.json` directly: it is the Chromium MV3 manifest and Firefox currently rejects `background.service_worker` for temporary add-ons.
|
For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `npm run package:extension:firefox` first and load `dist/extension-package-firefox/manifest.json`. Do **not** load `extension/manifest.json` directly: it is the Chromium MV3 manifest and Firefox currently rejects `background.service_worker` for temporary add-ons.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ from collections.abc import Callable, Iterable
|
|||||||
from browser_cli.models import BrowserCounts, Tab
|
from browser_cli.models import BrowserCounts, Tab
|
||||||
from browser_cli.sdk.base import Namespace
|
from browser_cli.sdk.base import Namespace
|
||||||
|
|
||||||
|
# Keep SDK-driven bulk closes comfortably below the native-host response
|
||||||
|
# timeout. The extension can close larger batches, but real browsers may take
|
||||||
|
# much longer when hundreds of visible tabs are involved.
|
||||||
|
BULK_CLOSE_CHUNK_SIZE = 50
|
||||||
|
|
||||||
class TabsNS(Namespace):
|
class TabsNS(Namespace):
|
||||||
"""List, open, close, move, and inspect browser tabs."""
|
"""List, open, close, move, and inspect browser tabs."""
|
||||||
|
|
||||||
@@ -75,6 +80,20 @@ class TabsNS(Namespace):
|
|||||||
ids = None
|
ids = None
|
||||||
if tab_ids is not None:
|
if tab_ids is not None:
|
||||||
ids = [t.id if isinstance(t, Tab) else t for t in tab_ids]
|
ids = [t.id if isinstance(t, Tab) else t for t in tab_ids]
|
||||||
|
if ids is not None and len(ids) > BULK_CLOSE_CHUNK_SIZE and not inactive and not duplicates and tab_id is None:
|
||||||
|
closed = 0
|
||||||
|
for start in range(0, len(ids), BULK_CLOSE_CHUNK_SIZE):
|
||||||
|
chunk = ids[start:start + BULK_CLOSE_CHUNK_SIZE]
|
||||||
|
result = self.command("tabs.close", {
|
||||||
|
"tabId": None,
|
||||||
|
"tabIds": chunk,
|
||||||
|
"inactive": False,
|
||||||
|
"duplicates": False,
|
||||||
|
"gentleMode": gentle_mode,
|
||||||
|
})
|
||||||
|
closed += self.field(result, "closed", len(chunk))
|
||||||
|
return closed
|
||||||
|
|
||||||
result = self.command("tabs.close", {
|
result = self.command("tabs.close", {
|
||||||
"tabId": tab_id,
|
"tabId": tab_id,
|
||||||
"tabIds": ids,
|
"tabIds": ids,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.15.3",
|
"version": "0.15.4",
|
||||||
"description": "Control your browser from the terminal or Python SDK",
|
"description": "Control your browser from the terminal or Python SDK",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"check:extension": "tsc -p tsconfig.json --noEmit && npm run build:extension && node --check extension/background.js && npm run test:extension",
|
"check:extension": "tsc -p tsconfig.json --noEmit && npm run build:extension && node --check extension/background.js && npm run test:extension",
|
||||||
"package:extension": "npm run build:extension && python scripts/package_extension.py",
|
"package:extension": "npm run build:extension && python scripts/package_extension.py",
|
||||||
"package:extension:webstore": "npm run build:extension && python scripts/package_extension.py --webstore",
|
"package:extension:webstore": "npm run build:extension && python scripts/package_extension.py --webstore",
|
||||||
|
"package:extension:webstore:verified": "scripts/package_verified_crx.sh",
|
||||||
"package:extension:firefox": "npm run build:extension && python scripts/package_extension.py --firefox"
|
"package:extension:firefox": "npm run build:extension && python scripts/package_extension.py --firefox"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "real-browser-cli"
|
name = "real-browser-cli"
|
||||||
version = "0.15.3"
|
version = "0.15.4"
|
||||||
description = "Control your real running browser from the terminal or Python SDK"
|
description = "Control your real running browser from the terminal or Python SDK"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|||||||
Executable
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/package_verified_crx.sh [--key FILE.gpg] [--browser COMMAND] [--out FILE.crx]
|
||||||
|
|
||||||
|
Builds the Chrome Web Store package and creates a CRX signed with the dedicated
|
||||||
|
verified-upload RSA key. The RSA private key is expected to be GPG-encrypted.
|
||||||
|
|
||||||
|
Environment alternatives:
|
||||||
|
VERIFIED_CRX_KEY_GPG Path to encrypted RSA private key
|
||||||
|
CHROME_FOR_PACKING Browser command with --pack-extension support
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
key_gpg="${VERIFIED_CRX_KEY_GPG:-secrets/verified-crx/chrome-webstore-verified-crx-private-key.pem.gpg}"
|
||||||
|
browser_cmd="${CHROME_FOR_PACKING:-}"
|
||||||
|
out=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--key)
|
||||||
|
key_gpg="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--browser)
|
||||||
|
browser_cmd="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--out)
|
||||||
|
out="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -f "$key_gpg" ]]; then
|
||||||
|
echo "Encrypted verified CRX key not found: $key_gpg" >&2
|
||||||
|
echo "Create it with: scripts/setup_verified_crx_key.sh --recipient '<your GPG key>'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$browser_cmd" ]]; then
|
||||||
|
for candidate in google-chrome chrome chromium chromium-browser brave-browser brave; do
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
browser_cmd="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$browser_cmd" ]]; then
|
||||||
|
echo "No Chromium-based browser with --pack-extension found. Pass --browser or set CHROME_FOR_PACKING." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
version="$(python - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
print(json.loads(Path('extension/manifest.json').read_text())['version'])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
out="${out:-dist/browser-cli-extension-webstore-verified-v${version}.crx}"
|
||||||
|
|
||||||
|
npm run build:extension
|
||||||
|
python scripts/package_extension.py --webstore --out "dist/browser-cli-extension-webstore-v${version}.zip" >/dev/null
|
||||||
|
|
||||||
|
staging="$PWD/dist/extension-package-webstore"
|
||||||
|
if [[ ! -d "$staging" ]]; then
|
||||||
|
echo "Missing webstore staging directory: $staging" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
private_key="$tmp_dir/verified-crx-private-key.pem"
|
||||||
|
trap 'rm -rf "$tmp_dir"' EXIT
|
||||||
|
|
||||||
|
gpg --decrypt --output "$private_key" "$key_gpg"
|
||||||
|
chmod 600 "$private_key"
|
||||||
|
|
||||||
|
rm -f "$staging.crx"
|
||||||
|
"$browser_cmd" \
|
||||||
|
--pack-extension="$staging" \
|
||||||
|
--pack-extension-key="$private_key" \
|
||||||
|
--no-message-box \
|
||||||
|
--disable-gpu \
|
||||||
|
--no-sandbox >/dev/null
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$out")"
|
||||||
|
mv "$staging.crx" "$out"
|
||||||
|
|
||||||
|
echo "$out"
|
||||||
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: scripts/setup_verified_crx_key.sh [--recipient GPG_RECIPIENT] [--out-dir DIR]
|
||||||
|
|
||||||
|
Generates a dedicated RSA private key for Chrome Web Store verified CRX uploads,
|
||||||
|
encrypts it to your GPG key, and writes the public key material for the Chrome
|
||||||
|
Developer Dashboard.
|
||||||
|
|
||||||
|
Chrome Web Store verified uploads require an RSA CRX signing key. A GPG/OpenPGP
|
||||||
|
key cannot be used directly for CRX signing, but it can protect the RSA private
|
||||||
|
key at rest.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient=""
|
||||||
|
out_dir="secrets/verified-crx"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--recipient)
|
||||||
|
recipient="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--out-dir)
|
||||||
|
out_dir="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$recipient" ]]; then
|
||||||
|
recipient="$(gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: '$1 == "uid" { print $10; exit }')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$recipient" ]]; then
|
||||||
|
echo "No GPG recipient found. Pass --recipient '<key id or email>'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
chmod 700 "$out_dir"
|
||||||
|
|
||||||
|
private_key="$(mktemp)"
|
||||||
|
public_pem="$out_dir/chrome-webstore-verified-crx-public-key.pem"
|
||||||
|
public_der_b64="$out_dir/chrome-webstore-verified-crx-public-key.der.base64.txt"
|
||||||
|
encrypted_private="$out_dir/chrome-webstore-verified-crx-private-key.pem.gpg"
|
||||||
|
trap 'rm -f "$private_key"' EXIT
|
||||||
|
|
||||||
|
if [[ -e "$encrypted_private" ]]; then
|
||||||
|
echo "Refusing to overwrite existing encrypted private key: $encrypted_private" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
openssl genrsa -out "$private_key" 2048 >/dev/null 2>&1
|
||||||
|
chmod 600 "$private_key"
|
||||||
|
openssl rsa -in "$private_key" -pubout -out "$public_pem" >/dev/null 2>&1
|
||||||
|
openssl rsa -in "$private_key" -pubout -outform DER 2>/dev/null | base64 -w0 > "$public_der_b64"
|
||||||
|
printf '\n' >> "$public_der_b64"
|
||||||
|
|
||||||
|
gpg --encrypt --recipient "$recipient" --output "$encrypted_private" "$private_key"
|
||||||
|
chmod 600 "$encrypted_private"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
Created verified CRX upload key material:
|
||||||
|
encrypted private key: $encrypted_private
|
||||||
|
public key PEM: $public_pem
|
||||||
|
public key DER/base64: $public_der_b64
|
||||||
|
|
||||||
|
Use the public key in the Chrome Developer Dashboard -> Package -> Verified uploads.
|
||||||
|
Keep the encrypted private key. Do not commit or upload the decrypted PEM.
|
||||||
|
EOF
|
||||||
@@ -364,6 +364,27 @@ class TestTabs:
|
|||||||
profile=None, remote=None, key=None,
|
profile=None, remote=None, key=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_tabs_close_by_ids_chunks_large_batches(self, b, mock_send):
|
||||||
|
mock_send.side_effect = [{"closed": 50}, {"closed": 50}, {"closed": 20}]
|
||||||
|
assert b.tabs.close(tab_ids=range(120), gentle_mode="normal") == 120
|
||||||
|
assert mock_send.call_args_list == [
|
||||||
|
call(
|
||||||
|
"tabs.close",
|
||||||
|
{"tabId": None, "tabIds": list(range(0, 50)), "inactive": False, "duplicates": False, "gentleMode": "normal"},
|
||||||
|
profile=None, remote=None, key=None,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
"tabs.close",
|
||||||
|
{"tabId": None, "tabIds": list(range(50, 100)), "inactive": False, "duplicates": False, "gentleMode": "normal"},
|
||||||
|
profile=None, remote=None, key=None,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
"tabs.close",
|
||||||
|
{"tabId": None, "tabIds": list(range(100, 120)), "inactive": False, "duplicates": False, "gentleMode": "normal"},
|
||||||
|
profile=None, remote=None, key=None,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
def test_tabs_move(self, b, mock_send):
|
def test_tabs_move(self, b, mock_send):
|
||||||
b.tabs.move(10, forward=True)
|
b.tabs.move(10, forward=True)
|
||||||
mock_send.assert_called_once_with(
|
mock_send.assert_called_once_with(
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "real-browser-cli"
|
name = "real-browser-cli"
|
||||||
version = "0.15.3"
|
version = "0.15.4"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user