real-browser-cli 0.14.3__tar.gz → 0.15.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- real_browser_cli-0.15.5/.gitea/workflows/publish.yml +51 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/.gitignore +5 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/PKG-INFO +22 -10
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/README.md +21 -9
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/__init__.py +1 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/async_sdk.py +29 -7
- real_browser_cli-0.15.5/browser_cli/auth/__init__.py +67 -0
- real_browser_cli-0.15.5/browser_cli/auth/agent.py +103 -0
- real_browser_cli-0.15.5/browser_cli/auth/keys.py +59 -0
- real_browser_cli-0.15.5/browser_cli/auth/pq.py +65 -0
- real_browser_cli-0.15.5/browser_cli/auth/signing.py +42 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/client/__init__.py +2 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/client/core.py +27 -12
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/client/targets.py +2 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/install.py +32 -11
- real_browser_cli-0.15.5/browser_cli/commands/rendering/__init__.py +58 -0
- real_browser_cli-0.15.5/browser_cli/commands/rendering/common.py +84 -0
- real_browser_cli-0.15.5/browser_cli/commands/rendering/labels.py +63 -0
- real_browser_cli-0.15.5/browser_cli/commands/rendering/tabs_tree.py +185 -0
- real_browser_cli-0.15.5/browser_cli/commands/rendering/windows_tree.py +38 -0
- real_browser_cli-0.15.5/browser_cli/commands/script.py +70 -0
- real_browser_cli-0.15.5/browser_cli/commands/tabs.py +219 -0
- real_browser_cli-0.15.5/browser_cli/commands/windows.py +63 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/constants.py +7 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/markdown/html.py +0 -20
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/models.py +6 -0
- real_browser_cli-0.15.5/browser_cli/remote/auth.py +145 -0
- real_browser_cli-0.15.5/browser_cli/remote/socket.py +52 -0
- real_browser_cli-0.15.5/browser_cli/remote/transport.py +59 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/base.py +3 -3
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/decorators.py +5 -5
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/factories.py +14 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/routing.py +22 -3
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/tabs.py +19 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/workflow_decorators.py +35 -14
- real_browser_cli-0.15.5/browser_cli/serve/challenge.py +31 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/serve/control.py +13 -5
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/serve/runtime.py +1 -25
- real_browser_cli-0.15.5/browser_cli/transport/__init__.py +72 -0
- real_browser_cli-0.15.5/browser_cli/transport/binary.py +44 -0
- real_browser_cli-0.15.5/browser_cli/transport/codecs.py +84 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/manifest.json +7 -1
- real_browser_cli-0.15.5/extension/src/browser-api.ts +31 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/classes/CommandRegistry.ts +2 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/classes/JobManager.ts +6 -5
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/classes/NativeConnection.ts +15 -14
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/autosave.ts +23 -21
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/dom.ts +7 -2
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/extension.ts +4 -3
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/groups.ts +20 -19
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/navigation.ts +44 -19
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/session-snapshot.ts +6 -4
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/session.ts +22 -19
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/tabs-query.ts +9 -8
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/tabs.ts +26 -24
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/windows.ts +7 -5
- real_browser_cli-0.15.5/extension/src/content/markdown/code.ts +63 -0
- real_browser_cli-0.15.5/extension/src/content/markdown/index.ts +9 -0
- real_browser_cli-0.15.5/extension/src/content/markdown/renderer.ts +217 -0
- real_browser_cli-0.15.5/extension/src/content/markdown/root.ts +47 -0
- real_browser_cli-0.15.5/extension/src/content/markdown/utils.ts +85 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/group-helpers.ts +6 -3
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/index.ts +1 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/scripting.ts +6 -4
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/storage.ts +5 -4
- real_browser_cli-0.15.5/extension/src/core/tab-groups.ts +57 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/tab-helpers.ts +18 -15
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/throttle.ts +4 -3
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/index.ts +2 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/command-args.ts +1 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/index.ts +1 -0
- real_browser_cli-0.15.5/extension/src/types/webextension.ts +41 -0
- real_browser_cli-0.15.5/extension/test/browser-api.test.ts +43 -0
- real_browser_cli-0.15.5/extension/test/clients.test.ts +86 -0
- real_browser_cli-0.15.5/extension/test/navigation.test.ts +66 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/package-lock.json +8 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/package.json +4 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/pyproject.toml +1 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/scripts/package_extension.py +21 -10
- real_browser_cli-0.15.5/scripts/package_verified_crx.sh +102 -0
- real_browser_cli-0.15.5/scripts/setup_verified_crx_key.sh +83 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_api.py +55 -2
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_cli.py +108 -3
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_client.py +3 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_extension_error_page_handling.py +29 -5
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_extension_packaging.py +21 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_new_feature_commands.py +84 -1
- real_browser_cli-0.15.5/tests/test_rendering.py +91 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tsconfig.json +1 -1
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/uv.lock +1 -1
- real_browser_cli-0.14.3/.gitea/workflows/publish.yml +0 -28
- real_browser_cli-0.14.3/browser_cli/auth.py +0 -263
- real_browser_cli-0.14.3/browser_cli/commands/script.py +0 -68
- real_browser_cli-0.14.3/browser_cli/commands/tabs.py +0 -252
- real_browser_cli-0.14.3/browser_cli/commands/windows.py +0 -87
- real_browser_cli-0.14.3/browser_cli/remote/transport.py +0 -230
- real_browser_cli-0.14.3/browser_cli/transport.py +0 -214
- real_browser_cli-0.14.3/extension/src/content/markdown.ts +0 -404
- real_browser_cli-0.14.3/webstore-assets/icon-128.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/.claude/settings.local.json +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/.gitea/workflows/package-extension.yml +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/.gitea/workflows/testing.yml +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/.python-version +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/CLAUDE.md +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/LICENSE +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/PRIVACY.md +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/cli.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/client/auth.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/client/messages.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/command_security.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/auth.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/clients.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/completion.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/doctor.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/dom.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/events.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/extension.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/extract.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/groups.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/navigate.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/page.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/perf.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/raw.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/remote.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/search.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/serve.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/serve_http.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/session.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/storage.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/watch.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/commands/workspace.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/compat/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/compat/auth.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/compat/commands.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/endpoints.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/errors.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/framing.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/local_transport.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/markdown/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/markdown/render.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/native/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/native/host.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/native/local_server.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/native/protocol.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/platform.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/registry.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/remote/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/remote/registry.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/browser_data.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/dom.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/extension.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/groups.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/navigation.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/perf.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/session.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/sdk/windows.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/serve/__init__.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/serve/auth.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/serve/logging.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/serve/proxy.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/browser_cli/version_manager.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/examples/demo.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/examples/demo.sh +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/content.js +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/icon.svg +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/icons/icon-128.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/icons/icon-16.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/icons/icon-32.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/icons/icon-48.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/classes/CommandGroup.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/browser-data.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/injected.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/commands/perf.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/content/dispatch.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/content/dom-ops.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/content/extract.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/content/page-info.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/content-dispatch.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/core/errors.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/jobs.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/json.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/messages.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/session.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/src/types/tabs.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/test/autosave.test.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/test/chrome-mock.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/extension/test/jobs.test.ts +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/justfile +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/scripts/gen_extension_key.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/shell.nix +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/conftest.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_auth.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_commands_cli.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_dom.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_extract.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_framing.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_groups.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_native_host.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_nav.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_page.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_perf.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_performance_integration.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_platform.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_refactor_boundaries.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_registry.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_remote_protocol_matrix.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_serve.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_session.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_storage.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_tabs.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_transport.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/tests/test_windows.py +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/webstore-assets/screenshots/screenshot-1.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/webstore-assets/screenshots/screenshot-2.png +0 -0
- {real_browser_cli-0.14.3 → real_browser_cli-0.15.5}/webstore-assets/screenshots/screenshot-3.png +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Build & Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*" # triggers on version tags like v0.1.0, v1.2.3
|
|
7
|
+
workflow_dispatch: # allows manual trigger from the Gitea UI
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: Build Gitea package
|
|
21
|
+
run: |
|
|
22
|
+
# Keep the public/PyPI distribution as real-browser-cli in the repo,
|
|
23
|
+
# but publish the private Gitea package under browser-cli.
|
|
24
|
+
python - <<'PY'
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
replacements = {
|
|
28
|
+
Path("pyproject.toml"): (
|
|
29
|
+
'name = "real-browser-cli"',
|
|
30
|
+
'name = "browser-cli"',
|
|
31
|
+
),
|
|
32
|
+
Path("browser_cli/constants.py"): (
|
|
33
|
+
'PYPI_PACKAGE_NAME = "real-browser-cli"',
|
|
34
|
+
'PYPI_PACKAGE_NAME = "browser-cli"',
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for path, (old, new) in replacements.items():
|
|
39
|
+
text = path.read_text()
|
|
40
|
+
if old not in text:
|
|
41
|
+
raise SystemExit(f"expected text not found in {path}: {old}")
|
|
42
|
+
path.write_text(text.replace(old, new, 1))
|
|
43
|
+
PY
|
|
44
|
+
uv build
|
|
45
|
+
|
|
46
|
+
- name: Publish to Gitea
|
|
47
|
+
run: |
|
|
48
|
+
uv publish \
|
|
49
|
+
--publish-url "${{ vars.REGISTRY_URL }}/api/packages/${{ github.repository_owner }}/pypi" \
|
|
50
|
+
--username "${{ github.repository_owner }}" \
|
|
51
|
+
--password "${{ secrets.ACTION_ACCESS_TOKEN }}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: real-browser-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.5
|
|
4
4
|
Summary: Control your real running browser from the terminal or Python SDK
|
|
5
5
|
Project-URL: Homepage, https://git.yiprawr.dev/Automatisation/browser-cli
|
|
6
6
|
Project-URL: Repository, https://git.yiprawr.dev/Automatisation/browser-cli
|
|
@@ -143,7 +143,7 @@ Every response:
|
|
|
143
143
|
|
|
144
144
|
## Installation
|
|
145
145
|
|
|
146
|
-
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, Vivaldi
|
|
146
|
+
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox
|
|
147
147
|
|
|
148
148
|
### Install with uv
|
|
149
149
|
Once published on PyPI, install the CLI as a uv tool:
|
|
@@ -151,7 +151,7 @@ Once published on PyPI, install the CLI as a uv tool:
|
|
|
151
151
|
```sh
|
|
152
152
|
uv tool install real-browser-cli
|
|
153
153
|
browser-cli --version
|
|
154
|
-
browser-cli install brave # or: chrome, chromium, edge, vivaldi
|
|
154
|
+
browser-cli install brave # or: chrome, chromium, edge, vivaldi, firefox
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
The PyPI package is named `real-browser-cli`; the installed command is still `browser-cli`.
|
|
@@ -173,12 +173,12 @@ uv tool upgrade real-browser-cli
|
|
|
173
173
|
git clone <repo>
|
|
174
174
|
cd browser-cli
|
|
175
175
|
uv sync
|
|
176
|
-
uv run browser-cli install brave # or: chrome, chromium, edge, vivaldi
|
|
176
|
+
uv run browser-cli install brave # or: chrome, chromium, edge, vivaldi, firefox
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
The `install` command will:
|
|
180
180
|
1. Ask you to load the browser-specific extension package
|
|
181
|
-
2.
|
|
181
|
+
2. Show the stable extension ID used by that browser family
|
|
182
182
|
3. Write the native messaging manifest to your OS so the browser can find the host
|
|
183
183
|
4. Copy the native host into an internal `libexec` directory and create a small wrapper outside your `PATH`
|
|
184
184
|
|
|
@@ -608,11 +608,23 @@ The extension source lives in `extension/src/`. `extension/background.js` and `e
|
|
|
608
608
|
Packaging:
|
|
609
609
|
|
|
610
610
|
```bash
|
|
611
|
-
npm run package:extension
|
|
612
|
-
npm run package:extension:webstore
|
|
611
|
+
npm run package:extension # testing/unpacked zip, keeps manifest.key for stable Chromium native-messaging ID
|
|
612
|
+
npm run package:extension:webstore # Chrome Web Store zip, strips manifest.key
|
|
613
|
+
npm run package:extension:webstore:verified # Chrome Web Store CRX signed for verified uploads
|
|
614
|
+
npm run package:extension:firefox # Firefox zip, strips manifest.key and Firefox-incompatible permissions
|
|
613
615
|
```
|
|
614
616
|
|
|
615
|
-
Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `dist/`.
|
|
617
|
+
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:
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
scripts/setup_verified_crx_key.sh --recipient '<your GPG key id or email>'
|
|
621
|
+
# Add the generated public key in Chrome Developer Dashboard -> Package -> Verified uploads.
|
|
622
|
+
npm run package:extension:webstore:verified
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
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.
|
|
626
|
+
|
|
627
|
+
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.
|
|
616
628
|
|
|
617
629
|
---
|
|
618
630
|
|
|
@@ -620,8 +632,8 @@ Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `
|
|
|
620
632
|
|
|
621
633
|
- **Browser internal pages** (`chrome://`, `brave://`, `edge://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
|
622
634
|
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli clients rename --browser <current-alias> <new-alias>`.
|
|
623
|
-
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, and
|
|
624
|
-
- **
|
|
635
|
+
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, Vivaldi, and Firefox. Other Chromium-based browsers may use different or shared native messaging manifest locations, so they need browser-specific verification before being added safely.
|
|
636
|
+
- **Firefox support is experimental**. Basic tab/window/navigation/native-messaging support is wired, including tab-group APIs on supported Firefox versions.
|
|
625
637
|
|
|
626
638
|
---
|
|
627
639
|
|
|
@@ -50,7 +50,7 @@ Every response:
|
|
|
50
50
|
|
|
51
51
|
## Installation
|
|
52
52
|
|
|
53
|
-
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, Vivaldi
|
|
53
|
+
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox
|
|
54
54
|
|
|
55
55
|
### Install with uv
|
|
56
56
|
Once published on PyPI, install the CLI as a uv tool:
|
|
@@ -58,7 +58,7 @@ Once published on PyPI, install the CLI as a uv tool:
|
|
|
58
58
|
```sh
|
|
59
59
|
uv tool install real-browser-cli
|
|
60
60
|
browser-cli --version
|
|
61
|
-
browser-cli install brave # or: chrome, chromium, edge, vivaldi
|
|
61
|
+
browser-cli install brave # or: chrome, chromium, edge, vivaldi, firefox
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
The PyPI package is named `real-browser-cli`; the installed command is still `browser-cli`.
|
|
@@ -80,12 +80,12 @@ uv tool upgrade real-browser-cli
|
|
|
80
80
|
git clone <repo>
|
|
81
81
|
cd browser-cli
|
|
82
82
|
uv sync
|
|
83
|
-
uv run browser-cli install brave # or: chrome, chromium, edge, vivaldi
|
|
83
|
+
uv run browser-cli install brave # or: chrome, chromium, edge, vivaldi, firefox
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
The `install` command will:
|
|
87
87
|
1. Ask you to load the browser-specific extension package
|
|
88
|
-
2.
|
|
88
|
+
2. Show the stable extension ID used by that browser family
|
|
89
89
|
3. Write the native messaging manifest to your OS so the browser can find the host
|
|
90
90
|
4. Copy the native host into an internal `libexec` directory and create a small wrapper outside your `PATH`
|
|
91
91
|
|
|
@@ -515,11 +515,23 @@ The extension source lives in `extension/src/`. `extension/background.js` and `e
|
|
|
515
515
|
Packaging:
|
|
516
516
|
|
|
517
517
|
```bash
|
|
518
|
-
npm run package:extension
|
|
519
|
-
npm run package:extension:webstore
|
|
518
|
+
npm run package:extension # testing/unpacked zip, keeps manifest.key for stable Chromium native-messaging ID
|
|
519
|
+
npm run package:extension:webstore # Chrome Web Store zip, strips manifest.key
|
|
520
|
+
npm run package:extension:webstore:verified # Chrome Web Store CRX signed for verified uploads
|
|
521
|
+
npm run package:extension:firefox # Firefox zip, strips manifest.key and Firefox-incompatible permissions
|
|
520
522
|
```
|
|
521
523
|
|
|
522
|
-
Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `dist/`.
|
|
524
|
+
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:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
scripts/setup_verified_crx_key.sh --recipient '<your GPG key id or email>'
|
|
528
|
+
# Add the generated public key in Chrome Developer Dashboard -> Package -> Verified uploads.
|
|
529
|
+
npm run package:extension:webstore:verified
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
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.
|
|
533
|
+
|
|
534
|
+
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.
|
|
523
535
|
|
|
524
536
|
---
|
|
525
537
|
|
|
@@ -527,8 +539,8 @@ Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `
|
|
|
527
539
|
|
|
528
540
|
- **Browser internal pages** (`chrome://`, `brave://`, `edge://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
|
529
541
|
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli clients rename --browser <current-alias> <new-alias>`.
|
|
530
|
-
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, and
|
|
531
|
-
- **
|
|
542
|
+
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, Vivaldi, and Firefox. Other Chromium-based browsers may use different or shared native messaging manifest locations, so they need browser-specific verification before being added safely.
|
|
543
|
+
- **Firefox support is experimental**. Basic tab/window/navigation/native-messaging support is wired, including tab-group APIs on supported Firefox versions.
|
|
532
544
|
|
|
533
545
|
---
|
|
534
546
|
|
|
@@ -36,7 +36,7 @@ Commands are grouped into namespaces on the client:
|
|
|
36
36
|
"""
|
|
37
37
|
from collections.abc import Callable
|
|
38
38
|
|
|
39
|
-
from browser_cli.client import active_browser_targets, remote_browser_targets, send_command, send_command_async
|
|
39
|
+
from browser_cli.client import active_browser_targets, remote_browser_targets, remote_targets_for_alias, send_command, send_command_async
|
|
40
40
|
from browser_cli.errors import BrowserNotConnected
|
|
41
41
|
from browser_cli.models import BrowserCounts, Group, Tab
|
|
42
42
|
from browser_cli.sdk import (
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import functools
|
|
14
14
|
from collections.abc import Callable
|
|
15
|
-
from typing import TypeVar
|
|
15
|
+
from typing import TypeVar, cast
|
|
16
16
|
|
|
17
17
|
from browser_cli.models import Group, Tab
|
|
18
18
|
from browser_cli.sdk import NAMESPACE_NAMES
|
|
@@ -74,7 +74,7 @@ class AsyncDecoratorsNS(WorkflowDecoratorsMixin):
|
|
|
74
74
|
finally:
|
|
75
75
|
if cleanup is not None:
|
|
76
76
|
await self._maybe_await(cleanup(value))
|
|
77
|
-
return wrapper
|
|
77
|
+
return cast(F, wrapper)
|
|
78
78
|
return decorator(func) if func is not None else decorator
|
|
79
79
|
|
|
80
80
|
def new_tab(
|
|
@@ -117,7 +117,7 @@ class AsyncDecoratorsNS(WorkflowDecoratorsMixin):
|
|
|
117
117
|
finally:
|
|
118
118
|
if previous:
|
|
119
119
|
await self._c.perf.set_profile(previous)
|
|
120
|
-
return wrapper
|
|
120
|
+
return cast(F, wrapper)
|
|
121
121
|
return decorator
|
|
122
122
|
|
|
123
123
|
def retry(
|
|
@@ -142,8 +142,8 @@ class AsyncDecoratorsNS(WorkflowDecoratorsMixin):
|
|
|
142
142
|
raise
|
|
143
143
|
if delay > 0:
|
|
144
144
|
await asyncio.sleep(delay)
|
|
145
|
-
raise last_error
|
|
146
|
-
return wrapper
|
|
145
|
+
raise cast(BaseException, last_error)
|
|
146
|
+
return cast(F, wrapper)
|
|
147
147
|
return decorator
|
|
148
148
|
|
|
149
149
|
class AsyncBrowserCLI:
|
|
@@ -220,18 +220,40 @@ class AsyncBrowserCLI:
|
|
|
220
220
|
async def clients(self) -> list[dict]:
|
|
221
221
|
return await self._cmd("clients.list", {})
|
|
222
222
|
|
|
223
|
-
def tab_from(
|
|
223
|
+
def tab_from(
|
|
224
|
+
self,
|
|
225
|
+
data: dict,
|
|
226
|
+
*,
|
|
227
|
+
browser_profile: str | None = None,
|
|
228
|
+
browser_name: str | None = None,
|
|
229
|
+
browser_remote: str | None = None,
|
|
230
|
+
browser_type: str | None = None,
|
|
231
|
+
browser_group: str | None = None,
|
|
232
|
+
) -> Tab:
|
|
224
233
|
return self._sync.tab_from(
|
|
225
234
|
data,
|
|
226
235
|
browser_profile=browser_profile,
|
|
227
236
|
browser_name=browser_name,
|
|
228
237
|
browser_remote=browser_remote,
|
|
238
|
+
browser_type=browser_type,
|
|
239
|
+
browser_group=browser_group,
|
|
229
240
|
)
|
|
230
241
|
|
|
231
|
-
def group_from(
|
|
242
|
+
def group_from(
|
|
243
|
+
self,
|
|
244
|
+
data: dict,
|
|
245
|
+
*,
|
|
246
|
+
browser_profile: str | None = None,
|
|
247
|
+
browser_name: str | None = None,
|
|
248
|
+
browser_remote: str | None = None,
|
|
249
|
+
browser_type: str | None = None,
|
|
250
|
+
browser_group: str | None = None,
|
|
251
|
+
) -> Group:
|
|
232
252
|
return self._sync.group_from(
|
|
233
253
|
data,
|
|
234
254
|
browser_profile=browser_profile,
|
|
235
255
|
browser_name=browser_name,
|
|
236
256
|
browser_remote=browser_remote,
|
|
257
|
+
browser_type=browser_type,
|
|
258
|
+
browser_group=browser_group,
|
|
237
259
|
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Public auth API for browser-cli.
|
|
2
|
+
|
|
3
|
+
Implementation lives in focused modules:
|
|
4
|
+
- ``auth.agent``: SSH-agent/YubiKey helpers
|
|
5
|
+
- ``auth.keys``: file keys and authorized_keys management
|
|
6
|
+
- ``auth.signing``: canonical payload signing/verification
|
|
7
|
+
- ``auth.pq``: ML-KEM KEX and encrypted transport helpers
|
|
8
|
+
"""
|
|
9
|
+
from browser_cli.auth.agent import (
|
|
10
|
+
AgentKey,
|
|
11
|
+
agent_find_key,
|
|
12
|
+
agent_list_keys,
|
|
13
|
+
agent_roundtrip as _agent_roundtrip,
|
|
14
|
+
agent_sign_raw,
|
|
15
|
+
pack_ssh_string as _pack_str,
|
|
16
|
+
unpack_ssh_string as _unpack_str,
|
|
17
|
+
)
|
|
18
|
+
from browser_cli.auth.keys import (
|
|
19
|
+
add_authorized_key,
|
|
20
|
+
generate_keypair,
|
|
21
|
+
load_authorized_keys,
|
|
22
|
+
load_authorized_keys_with_names,
|
|
23
|
+
load_private_key,
|
|
24
|
+
public_key_hex,
|
|
25
|
+
)
|
|
26
|
+
from browser_cli.auth.pq import (
|
|
27
|
+
new_nonce,
|
|
28
|
+
pq_decrypt,
|
|
29
|
+
pq_encrypt,
|
|
30
|
+
pq_kex_client_encapsulate,
|
|
31
|
+
pq_kex_server_decapsulate,
|
|
32
|
+
pq_kex_server_keypair,
|
|
33
|
+
pq_transport_key as _pq_transport_key,
|
|
34
|
+
)
|
|
35
|
+
from browser_cli.auth.signing import (
|
|
36
|
+
auth_message as _auth_message,
|
|
37
|
+
canonical_payload,
|
|
38
|
+
sign,
|
|
39
|
+
verify,
|
|
40
|
+
)
|
|
41
|
+
from browser_cli.constants import DEFAULT_AUTHORIZED_KEYS_PATH, DEFAULT_KEY_PATH, PQ_KEX_ALG, PQ_TRANSPORT_ALG
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AgentKey",
|
|
45
|
+
"DEFAULT_AUTHORIZED_KEYS_PATH",
|
|
46
|
+
"DEFAULT_KEY_PATH",
|
|
47
|
+
"PQ_KEX_ALG",
|
|
48
|
+
"PQ_TRANSPORT_ALG",
|
|
49
|
+
"add_authorized_key",
|
|
50
|
+
"agent_find_key",
|
|
51
|
+
"agent_list_keys",
|
|
52
|
+
"agent_sign_raw",
|
|
53
|
+
"canonical_payload",
|
|
54
|
+
"generate_keypair",
|
|
55
|
+
"load_authorized_keys",
|
|
56
|
+
"load_authorized_keys_with_names",
|
|
57
|
+
"load_private_key",
|
|
58
|
+
"new_nonce",
|
|
59
|
+
"pq_decrypt",
|
|
60
|
+
"pq_encrypt",
|
|
61
|
+
"pq_kex_client_encapsulate",
|
|
62
|
+
"pq_kex_server_decapsulate",
|
|
63
|
+
"pq_kex_server_keypair",
|
|
64
|
+
"public_key_hex",
|
|
65
|
+
"sign",
|
|
66
|
+
"verify",
|
|
67
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""SSH-agent backed Ed25519 key helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import struct
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from browser_cli.constants import (
|
|
10
|
+
SSH_AGENT_IDENTITIES_ANSWER,
|
|
11
|
+
SSH_AGENT_SIGN_RESPONSE,
|
|
12
|
+
SSH_AGENTC_REQUEST_IDENTITIES,
|
|
13
|
+
SSH_AGENTC_SIGN_REQUEST,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def pack_ssh_string(value: bytes) -> bytes:
|
|
17
|
+
return struct.pack(">I", len(value)) + value
|
|
18
|
+
|
|
19
|
+
def unpack_ssh_string(data: bytes, offset: int) -> tuple[bytes, int]:
|
|
20
|
+
length = struct.unpack_from(">I", data, offset)[0]
|
|
21
|
+
return data[offset + 4 : offset + 4 + length], offset + 4 + length
|
|
22
|
+
|
|
23
|
+
def agent_roundtrip(msg: bytes) -> bytes:
|
|
24
|
+
sock_path = os.environ.get("SSH_AUTH_SOCK")
|
|
25
|
+
if not sock_path:
|
|
26
|
+
raise RuntimeError("SSH_AUTH_SOCK not set — is gpg-agent / ssh-agent running?")
|
|
27
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
28
|
+
sock.settimeout(10)
|
|
29
|
+
sock.connect(sock_path)
|
|
30
|
+
sock.sendall(struct.pack(">I", len(msg)) + msg)
|
|
31
|
+
raw_len = b""
|
|
32
|
+
while len(raw_len) < 4:
|
|
33
|
+
chunk = sock.recv(4 - len(raw_len))
|
|
34
|
+
if not chunk:
|
|
35
|
+
raise RuntimeError("SSH agent closed connection")
|
|
36
|
+
raw_len += chunk
|
|
37
|
+
length = struct.unpack(">I", raw_len)[0]
|
|
38
|
+
response = b""
|
|
39
|
+
while len(response) < length:
|
|
40
|
+
chunk = sock.recv(length - len(response))
|
|
41
|
+
if not chunk:
|
|
42
|
+
raise RuntimeError("SSH agent closed connection mid-response")
|
|
43
|
+
response += chunk
|
|
44
|
+
return response
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class AgentKey:
|
|
48
|
+
"""Ed25519 key backed by an SSH agent (YubiKey, TPM, ssh-agent, gpg-agent …)."""
|
|
49
|
+
blob: bytes
|
|
50
|
+
comment: str
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def pubkey_bytes(self) -> bytes:
|
|
54
|
+
_algo, offset = unpack_ssh_string(self.blob, 0)
|
|
55
|
+
key_bytes, _ = unpack_ssh_string(self.blob, offset)
|
|
56
|
+
return key_bytes
|
|
57
|
+
|
|
58
|
+
def agent_list_keys() -> list[AgentKey]:
|
|
59
|
+
"""Return all Ed25519 keys currently held by the SSH agent."""
|
|
60
|
+
response = agent_roundtrip(bytes([SSH_AGENTC_REQUEST_IDENTITIES]))
|
|
61
|
+
if response[0] != SSH_AGENT_IDENTITIES_ANSWER:
|
|
62
|
+
raise RuntimeError(f"Unexpected agent response: {response[0]}")
|
|
63
|
+
key_count = struct.unpack_from(">I", response, 1)[0]
|
|
64
|
+
keys: list[AgentKey] = []
|
|
65
|
+
offset = 5
|
|
66
|
+
for _ in range(key_count):
|
|
67
|
+
blob, offset = unpack_ssh_string(response, offset)
|
|
68
|
+
comment, offset = unpack_ssh_string(response, offset)
|
|
69
|
+
algo, _ = unpack_ssh_string(blob, 0)
|
|
70
|
+
if algo == b"ssh-ed25519":
|
|
71
|
+
keys.append(AgentKey(blob=blob, comment=comment.decode("utf-8", errors="replace")))
|
|
72
|
+
return keys
|
|
73
|
+
|
|
74
|
+
def agent_find_key(selector: str | None = None) -> AgentKey | None:
|
|
75
|
+
"""Return the first agent Ed25519 key whose comment contains selector (or any if None)."""
|
|
76
|
+
try:
|
|
77
|
+
keys = agent_list_keys()
|
|
78
|
+
except Exception:
|
|
79
|
+
return None
|
|
80
|
+
for key in keys:
|
|
81
|
+
if key.comment == "(none)":
|
|
82
|
+
continue
|
|
83
|
+
if selector is None or selector in key.comment:
|
|
84
|
+
return key
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def agent_sign_raw(key: AgentKey, data: bytes) -> bytes:
|
|
88
|
+
"""Ask the SSH agent to sign data and return the raw 64-byte Ed25519 signature."""
|
|
89
|
+
msg = (
|
|
90
|
+
bytes([SSH_AGENTC_SIGN_REQUEST])
|
|
91
|
+
+ pack_ssh_string(key.blob)
|
|
92
|
+
+ pack_ssh_string(data)
|
|
93
|
+
+ struct.pack(">I", 0)
|
|
94
|
+
)
|
|
95
|
+
response = agent_roundtrip(msg)
|
|
96
|
+
if response[0] != SSH_AGENT_SIGN_RESPONSE:
|
|
97
|
+
raise RuntimeError(f"SSH agent refused to sign (response code {response[0]})")
|
|
98
|
+
sig_blob, _ = unpack_ssh_string(response, 1)
|
|
99
|
+
_algo, sig_offset = unpack_ssh_string(sig_blob, 0)
|
|
100
|
+
raw_sig, _ = unpack_ssh_string(sig_blob, sig_offset)
|
|
101
|
+
if len(raw_sig) != 64:
|
|
102
|
+
raise RuntimeError(f"Unexpected signature length {len(raw_sig)}")
|
|
103
|
+
return raw_sig
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""File-based Ed25519 keys and authorized_keys helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
7
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
8
|
+
Encoding,
|
|
9
|
+
NoEncryption,
|
|
10
|
+
PrivateFormat,
|
|
11
|
+
PublicFormat,
|
|
12
|
+
load_pem_private_key,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from browser_cli.auth.agent import AgentKey
|
|
16
|
+
|
|
17
|
+
def generate_keypair() -> tuple[bytes, str]:
|
|
18
|
+
"""Return (private_key_pem_bytes, public_key_hex)."""
|
|
19
|
+
private_key = Ed25519PrivateKey.generate()
|
|
20
|
+
pem = private_key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
|
21
|
+
public_hex = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex()
|
|
22
|
+
return pem, public_hex
|
|
23
|
+
|
|
24
|
+
def load_private_key(path: Path) -> Ed25519PrivateKey:
|
|
25
|
+
return load_pem_private_key(path.read_bytes(), password=None)
|
|
26
|
+
|
|
27
|
+
def public_key_hex(key: Ed25519PrivateKey | AgentKey) -> str:
|
|
28
|
+
if isinstance(key, AgentKey):
|
|
29
|
+
return key.pubkey_bytes.hex()
|
|
30
|
+
return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex()
|
|
31
|
+
|
|
32
|
+
def load_authorized_keys_with_names(path: Path) -> list[tuple[str, str]]:
|
|
33
|
+
"""Return list of (pubkey_hex, name) pairs. Name is empty string if not set."""
|
|
34
|
+
if not path.exists():
|
|
35
|
+
return []
|
|
36
|
+
result = []
|
|
37
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if not line or line.startswith("#"):
|
|
40
|
+
continue
|
|
41
|
+
parts = line.split(None, 1)
|
|
42
|
+
pubkey = parts[0]
|
|
43
|
+
name = parts[1].strip() if len(parts) > 1 else ""
|
|
44
|
+
result.append((pubkey, name))
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def load_authorized_keys(path: Path) -> list[str]:
|
|
48
|
+
return [pubkey for pubkey, _name in load_authorized_keys_with_names(path)]
|
|
49
|
+
|
|
50
|
+
def add_authorized_key(path: Path, pub_hex: str, name: str = "") -> bool:
|
|
51
|
+
"""Append pub_hex to authorized_keys. Returns False if already present."""
|
|
52
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
existing = {pubkey for pubkey, _name in load_authorized_keys_with_names(path)}
|
|
54
|
+
if pub_hex in existing:
|
|
55
|
+
return False
|
|
56
|
+
line = (f"{pub_hex} {name}".rstrip()) + "\n"
|
|
57
|
+
with open(path, "a", encoding="utf-8") as file:
|
|
58
|
+
file.write(line)
|
|
59
|
+
return True
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Post-quantum ML-KEM key exchange and app-layer transport encryption."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import secrets
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
|
7
|
+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
8
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
9
|
+
|
|
10
|
+
from browser_cli.constants import PQ_TRANSPORT_ALG
|
|
11
|
+
|
|
12
|
+
def pq_kex_server_keypair():
|
|
13
|
+
"""Return an ephemeral ML-KEM-768 private key and raw public key bytes.
|
|
14
|
+
|
|
15
|
+
Returns ``None`` when the installed cryptography/OpenSSL backend does not
|
|
16
|
+
support ML-KEM yet. The serve/client protocol treats this as graceful
|
|
17
|
+
downgrade instead of breaking local installs on older OpenSSL builds.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric import mlkem
|
|
21
|
+
private_key = mlkem.MLKEM768PrivateKey.generate()
|
|
22
|
+
public_key = private_key.public_key().public_bytes_raw()
|
|
23
|
+
return private_key, public_key
|
|
24
|
+
except Exception:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def pq_kex_client_encapsulate(public_key_hex: str) -> tuple[str, bytes]:
|
|
28
|
+
"""Encapsulate to a server ML-KEM public key. Returns (ciphertext_hex, secret)."""
|
|
29
|
+
from cryptography.hazmat.primitives.asymmetric import mlkem
|
|
30
|
+
public_key = mlkem.MLKEM768PublicKey.from_public_bytes(bytes.fromhex(public_key_hex))
|
|
31
|
+
shared_secret, ciphertext = public_key.encapsulate()
|
|
32
|
+
return ciphertext.hex(), shared_secret
|
|
33
|
+
|
|
34
|
+
def pq_kex_server_decapsulate(private_key, ciphertext_hex: str) -> bytes:
|
|
35
|
+
"""Decapsulate a client ML-KEM ciphertext and return the shared secret."""
|
|
36
|
+
return private_key.decapsulate(bytes.fromhex(ciphertext_hex))
|
|
37
|
+
|
|
38
|
+
def pq_transport_key(shared_secret: bytes, direction: str) -> bytes:
|
|
39
|
+
return HKDF(
|
|
40
|
+
algorithm=hashes.SHA256(),
|
|
41
|
+
length=32,
|
|
42
|
+
salt=None,
|
|
43
|
+
info=f"browser-cli pq transport v1 {direction}".encode("ascii"),
|
|
44
|
+
).derive(shared_secret)
|
|
45
|
+
|
|
46
|
+
def pq_encrypt(shared_secret: bytes, direction: str, plaintext: bytes) -> dict:
|
|
47
|
+
"""Encrypt an app-layer frame with a key derived from the ML-KEM secret."""
|
|
48
|
+
nonce = secrets.token_bytes(12)
|
|
49
|
+
key = pq_transport_key(shared_secret, direction)
|
|
50
|
+
ciphertext = ChaCha20Poly1305(key).encrypt(nonce, plaintext, None)
|
|
51
|
+
return {"alg": PQ_TRANSPORT_ALG, "nonce": nonce.hex(), "ciphertext": ciphertext.hex()}
|
|
52
|
+
|
|
53
|
+
def pq_decrypt(shared_secret: bytes, direction: str, envelope: dict) -> bytes:
|
|
54
|
+
"""Decrypt an app-layer frame produced by pq_encrypt()."""
|
|
55
|
+
if not isinstance(envelope, dict) or envelope.get("alg") != PQ_TRANSPORT_ALG:
|
|
56
|
+
raise ValueError("unsupported encrypted transport envelope")
|
|
57
|
+
key = pq_transport_key(shared_secret, direction)
|
|
58
|
+
return ChaCha20Poly1305(key).decrypt(
|
|
59
|
+
bytes.fromhex(str(envelope["nonce"])),
|
|
60
|
+
bytes.fromhex(str(envelope["ciphertext"])),
|
|
61
|
+
None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def new_nonce() -> str:
|
|
65
|
+
return secrets.token_hex(32)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Canonical browser-cli auth payload signing and verification."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from cryptography.exceptions import InvalidSignature
|
|
8
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
9
|
+
|
|
10
|
+
from browser_cli.auth.agent import AgentKey, agent_sign_raw
|
|
11
|
+
|
|
12
|
+
def canonical_payload(msg: dict) -> bytes:
|
|
13
|
+
"""Deterministic JSON encoding of msg without auth protocol fields."""
|
|
14
|
+
return json.dumps(
|
|
15
|
+
{key: value for key, value in msg.items() if key not in {"pubkey", "sig", "pq_kex"}},
|
|
16
|
+
sort_keys=True,
|
|
17
|
+
separators=(",", ":"),
|
|
18
|
+
).encode("utf-8")
|
|
19
|
+
|
|
20
|
+
def auth_message(nonce: bytes, msg: dict, pq_shared_secret: bytes | None = None) -> bytes:
|
|
21
|
+
"""Bytes signed for auth; optionally binds a post-quantum KEX secret."""
|
|
22
|
+
data = nonce + hashlib.sha256(canonical_payload(msg)).digest()
|
|
23
|
+
if pq_shared_secret is not None:
|
|
24
|
+
data += hashlib.sha256(b"browser-cli ml-kem-768 v1" + pq_shared_secret).digest()
|
|
25
|
+
return data
|
|
26
|
+
|
|
27
|
+
def sign(key: Ed25519PrivateKey | AgentKey, nonce: bytes, msg: dict, pq_shared_secret: bytes | None = None) -> bytes:
|
|
28
|
+
"""Sign nonce + payload hash, optionally bound to an ML-KEM shared secret."""
|
|
29
|
+
data = auth_message(nonce, msg, pq_shared_secret)
|
|
30
|
+
if isinstance(key, AgentKey):
|
|
31
|
+
return agent_sign_raw(key, data)
|
|
32
|
+
return key.sign(data)
|
|
33
|
+
|
|
34
|
+
def verify(pub_hex: str, nonce: bytes, msg: dict, sig_hex: str, pq_shared_secret: bytes | None = None) -> bool:
|
|
35
|
+
"""Return True if sig_hex is a valid signature over the canonical payload/auth secret."""
|
|
36
|
+
try:
|
|
37
|
+
pub_bytes = bytes.fromhex(pub_hex)
|
|
38
|
+
pub_key = Ed25519PublicKey.from_public_bytes(pub_bytes)
|
|
39
|
+
pub_key.verify(bytes.fromhex(sig_hex), auth_message(nonce, msg, pq_shared_secret))
|
|
40
|
+
return True
|
|
41
|
+
except (InvalidSignature, ValueError):
|
|
42
|
+
return False
|
|
@@ -10,6 +10,7 @@ from browser_cli.client.core import (
|
|
|
10
10
|
remote_browser_targets,
|
|
11
11
|
remote_browser_targets_async,
|
|
12
12
|
remote_target_for_alias,
|
|
13
|
+
remote_targets_for_alias,
|
|
13
14
|
send_command,
|
|
14
15
|
send_command_async,
|
|
15
16
|
)
|
|
@@ -42,6 +43,7 @@ __all__ = [
|
|
|
42
43
|
"remote_browser_targets",
|
|
43
44
|
"remote_browser_targets_async",
|
|
44
45
|
"remote_target_for_alias",
|
|
46
|
+
"remote_targets_for_alias",
|
|
45
47
|
"send_command",
|
|
46
48
|
"send_command_async",
|
|
47
49
|
]
|