real-browser-cli 0.16.2__tar.gz → 0.16.3__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.16.2 → real_browser_cli-0.16.3}/PKG-INFO +9 -26
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/README.md +8 -25
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/auth.py +0 -4
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/serve_http.py +42 -12
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/markdown/html.py +26 -4
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/manifest.json +1 -1
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/.claude/settings.local.json +7 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/.gitignore +4 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/README.md +89 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/credentials/BrowserCliApi.credentials.ts +85 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/BrowserCli.node.ts +382 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/browserCli.svg +31 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/protocol.ts +302 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/request.ts +129 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/serveClient.ts +147 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/package-lock.json +1825 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/package.json +45 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/scripts/copy-assets.mjs +17 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/scripts/run-tests.mjs +27 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/test/protocol.test.ts +141 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/test/request.test.ts +73 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/test-dist/protocol.test.mjs +285 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/test-dist/request.test.mjs +147 -0
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/tsconfig.json +19 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/package-lock.json +107 -107
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/pyproject.toml +1 -1
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_client.py +4 -8
- real_browser_cli-0.16.3/tests/test_markdown_security.py +50 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_new_feature_commands.py +42 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_serve.py +42 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_serve_security.py +45 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/uv.lock +1 -1
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.claude/settings.local.json +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitea/workflows/package-extension.yml +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitea/workflows/publish.yml +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitea/workflows/testing.yml +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitignore +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.python-version +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/CLAUDE.md +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/LICENSE +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/PRIVACY.md +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/async_sdk.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/agent.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/keys.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/pq.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/signing.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/cli.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/core.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/messages.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/targets.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/command_security.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/auth.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/clients.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/completion.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/doctor.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/dom.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/events.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/extension.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/extract.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/groups.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/install.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/navigate.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/page.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/perf.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/raw.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/remote.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/common.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/labels.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/tabs_tree.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/windows_tree.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/script.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/search.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/serve.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/session.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/storage.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/tabs.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/watch.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/windows.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/workspace.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/compat/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/compat/auth.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/compat/commands.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/constants.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/endpoints.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/errors.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/framing.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/local_transport.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/markdown/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/markdown/render.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/models.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/host.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/local_server.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/protocol.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/platform.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/registry.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/auth.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/pool.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/registry.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/socket.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/transport.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/base.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/browser_data.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/decorators.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/dom.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/extension.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/factories.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/groups.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/navigation.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/perf.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/routing.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/session.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/tabs.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/windows.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/workflow_decorators.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/search/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/search/engines.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/auth.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/challenge.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/control.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/logging.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/proxy.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/runtime.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/security.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/transport/__init__.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/transport/binary.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/transport/codecs.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/version_manager.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/examples/demo.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/examples/demo.sh +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/content.js +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icon.svg +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-128.png +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-16.png +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-32.png +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-48.png +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/browser-api.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/CommandGroup.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/CommandRegistry.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/JobManager.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/NativeConnection.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/autosave.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/browser-data.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/dom.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/extension.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/groups.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/injected.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/navigation.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/perf.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/session-snapshot.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/session.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/tabs-query.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/tabs.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/windows.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/dispatch.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/dom-ops.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/extract.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/code.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/index.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/renderer.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/root.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/utils.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/page-info.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content-dispatch.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/errors.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/group-helpers.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/index.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/scripting.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/storage.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/tab-groups.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/tab-helpers.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/throttle.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/index.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/command-args.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/index.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/jobs.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/json.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/messages.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/session.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/tabs.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/webextension.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/autosave.test.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/browser-api.test.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/chrome-mock.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/clients.test.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/jobs.test.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/navigation.test.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/paging.test.ts +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/justfile +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/package.json +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/gen_extension_key.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/package_extension.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/package_verified_crx.sh +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/setup_verified_crx_key.sh +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/shell.nix +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/conftest.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_api.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_auth.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_cli.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_commands_cli.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_dom.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_extension_error_page_handling.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_extension_packaging.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_extract.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_framing.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_groups.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_native_host.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_nav.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_page.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_perf.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_performance_integration.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_platform.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_refactor_boundaries.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_registry.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_remote_pool.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_remote_protocol_matrix.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_rendering.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_session.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_storage.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_tabs.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_transport.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_windows.py +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tsconfig.json +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/webstore-assets/screenshots/screenshot-1.png +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/webstore-assets/screenshots/screenshot-2.png +0 -0
- {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/webstore-assets/screenshots/screenshot-3.png +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: real-browser-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.3
|
|
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
|
|
@@ -131,7 +131,6 @@ terminal / python script / remote client
|
|
|
131
131
|
No local server needs to be running beforehand. The browser manages the native host's lifecycle. For cross-machine control, `browser-cli serve` starts an explicit TCP listener protected by Ed25519 public-key authentication unless you opt out with `--no-auth`.
|
|
132
132
|
|
|
133
133
|
**Message format**
|
|
134
|
-
|
|
135
134
|
Every command is a JSON object:
|
|
136
135
|
```json
|
|
137
136
|
{ "id": "uuid", "command": "tabs.list", "args": {} }
|
|
@@ -144,7 +143,6 @@ Every response:
|
|
|
144
143
|
---
|
|
145
144
|
|
|
146
145
|
## Installation
|
|
147
|
-
|
|
148
146
|
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox
|
|
149
147
|
|
|
150
148
|
browser-cli has two parts: the **CLI / native host** (a Python package) and the **browser extension** (published on the public stores).
|
|
@@ -203,7 +201,6 @@ Only the `browser-cli` command needs to be on your `PATH`. The browser launches
|
|
|
203
201
|
---
|
|
204
202
|
|
|
205
203
|
## Project structure
|
|
206
|
-
|
|
207
204
|
```text
|
|
208
205
|
browser-cli/
|
|
209
206
|
├── browser_cli/
|
|
@@ -239,7 +236,6 @@ browser-cli/
|
|
|
239
236
|
---
|
|
240
237
|
|
|
241
238
|
## CLI reference
|
|
242
|
-
|
|
243
239
|
During source development, commands are usually run as `uv run browser-cli [--browser ALIAS] <command>`. After tool installation, use `browser-cli ...` directly. Add `--remote HOST[:PORT]` and optionally `--key PATH` to target a browser exposed by `browser-cli serve`.
|
|
244
240
|
|
|
245
241
|
If exactly one browser instance is connected, commands auto-target it. Use `--browser ALIAS` when multiple browser instances are connected. `tabs list`, `tabs count`, `groups list`, `groups count`, `windows list`, and `session list` aggregate across all active browsers when `--browser` is omitted; in that mode they show the source browser alias or UUID. When local and saved remote browsers are mixed, tables group rows by source (`local` or the remote endpoint) and indent the browser profile below that group. You can inspect active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli clients rename --browser <current-alias> <new-alias>`. Closed browsers are removed from the client registry automatically.
|
|
@@ -247,7 +243,6 @@ If exactly one browser instance is connected, commands auto-target it. Use `--br
|
|
|
247
243
|
Important: profile aliases are browser-instance aliases, not window aliases. Window aliases created with `windows rename` are only for targeting windows in commands like `nav open --window work`. If a browser instance has no explicit profile alias set, the native host gives it a generated UUID alias so multiple unaliased browsers stay distinct.
|
|
248
244
|
|
|
249
245
|
### Navigation (`nav`)
|
|
250
|
-
|
|
251
246
|
```sh
|
|
252
247
|
# Open a URL (no focus stealing by default)
|
|
253
248
|
browser-cli nav open https://example.com
|
|
@@ -269,7 +264,6 @@ browser-cli nav focus github # focuses first tab whose URL contains "
|
|
|
269
264
|
```
|
|
270
265
|
|
|
271
266
|
### Search
|
|
272
|
-
|
|
273
267
|
Each search command opens the search results in your browser using the same flags as `nav open`.
|
|
274
268
|
|
|
275
269
|
```sh
|
|
@@ -293,7 +287,6 @@ browser-cli search so click choices
|
|
|
293
287
|
```
|
|
294
288
|
|
|
295
289
|
### Tabs
|
|
296
|
-
|
|
297
290
|
```sh
|
|
298
291
|
browser-cli tabs list # list all open tabs (all windows)
|
|
299
292
|
browser-cli tabs count # count all tabs
|
|
@@ -321,7 +314,6 @@ browser-cli tabs merge-windows # pull all tabs into the current wi
|
|
|
321
314
|
```
|
|
322
315
|
|
|
323
316
|
### Tab groups
|
|
324
|
-
|
|
325
317
|
```sh
|
|
326
318
|
browser-cli groups list # list all tab groups
|
|
327
319
|
browser-cli groups count # count groups
|
|
@@ -343,7 +335,6 @@ browser-cli groups move 42 -l # short left alias
|
|
|
343
335
|
```
|
|
344
336
|
|
|
345
337
|
### Windows
|
|
346
|
-
|
|
347
338
|
```sh
|
|
348
339
|
browser-cli windows list # list all windows
|
|
349
340
|
browser-cli windows open # open a new window
|
|
@@ -353,7 +344,6 @@ browser-cli windows close 1 # close a window
|
|
|
353
344
|
```
|
|
354
345
|
|
|
355
346
|
### DOM
|
|
356
|
-
|
|
357
347
|
These commands run on the **active tab**. The tab must be on a regular `http://` or `https://` page — not a browser internal page like `brave://newtab`.
|
|
358
348
|
|
|
359
349
|
```sh
|
|
@@ -366,7 +356,6 @@ browser-cli dom type "#search" "hello" # type text into an input
|
|
|
366
356
|
```
|
|
367
357
|
|
|
368
358
|
### Extract
|
|
369
|
-
|
|
370
359
|
```sh
|
|
371
360
|
browser-cli extract links # all <a href> links on the page
|
|
372
361
|
browser-cli extract images # all <img> tags (src + alt)
|
|
@@ -378,7 +367,6 @@ browser-cli extract markdown --selector "article" # specific DOM subtree as Ma
|
|
|
378
367
|
```
|
|
379
368
|
|
|
380
369
|
### Sessions
|
|
381
|
-
|
|
382
370
|
A session is a snapshot of all open tab URLs, stored inside the extension via `chrome.storage.local`. Sessions survive browser restarts but are lost if the extension is uninstalled or extension data is cleared.
|
|
383
371
|
|
|
384
372
|
```sh
|
|
@@ -392,7 +380,6 @@ browser-cli session auto-save off
|
|
|
392
380
|
```
|
|
393
381
|
|
|
394
382
|
### Misc
|
|
395
|
-
|
|
396
383
|
```sh
|
|
397
384
|
browser-cli clients # show connected browser info from the registry
|
|
398
385
|
browser-cli clients rename --browser abcd1234 work # rename one connected browser instance
|
|
@@ -403,7 +390,6 @@ browser-cli completion zsh --script # output raw completion script
|
|
|
403
390
|
```
|
|
404
391
|
|
|
405
392
|
### Remote control, auth, and gateways
|
|
406
|
-
|
|
407
393
|
```sh
|
|
408
394
|
# On the machine with the browser
|
|
409
395
|
browser-cli auth keygen --output ~/.config/browser-cli/client.key
|
|
@@ -428,22 +414,24 @@ browser-cli serve-http --port 8766
|
|
|
428
414
|
curl -H "Authorization: Bearer <token>" http://127.0.0.1:8766/tabs
|
|
429
415
|
```
|
|
430
416
|
|
|
431
|
-
Remote auth uses Ed25519 challenge/response. `--remote` domains default to port 443; explicit `host:port` endpoints are also supported. Saved remote endpoints participate in aggregate list/count commands, where output is grouped by endpoint.
|
|
417
|
+
Remote auth uses Ed25519 challenge/response. `--remote` domains default to port 443; explicit `host:port` endpoints are also supported. Use `browser-cli remote trust ENDPOINT KEY` to remember a key for later calls. Saved remote endpoints participate in aggregate list/count commands, where output is grouped by endpoint.
|
|
432
418
|
|
|
433
|
-
####
|
|
419
|
+
#### n8n integration
|
|
420
|
+
The n8n community node is published as [`n8n-nodes-browser-cli`](https://www.npmjs.com/package/n8n-nodes-browser-cli) on npm. It talks directly to a remote `browser-cli serve` endpoint over the same Ed25519-authenticated, ML-KEM-encrypted TCP protocol as the CLI remote client. Install it from n8n's Community Nodes UI, run `browser-cli serve` on the browser machine, paste the client key into the node credential, and drive tabs/DOM/extraction/raw commands from a workflow. See [`n8n-nodes-browser-cli/README.md`](n8n-nodes-browser-cli/README.md).
|
|
434
421
|
|
|
435
|
-
|
|
436
|
-
|
|
422
|
+
#### Security model
|
|
423
|
+
- **`serve` (TCP)** authenticates every connection with an Ed25519 signature over a fresh server nonce and, for modern clients, wraps the transport in an ML-KEM-768 (post-quantum) AEAD channel. Commands are gated by a **safe-only policy by default** — even a trusted key can only run read-only status/listing commands until you open more with `--allow-read-page`, `--allow-control`, `--allow-dangerous`, `--allow-keys`, or `--allow-all` (full control, including `dom.eval`/`storage.*`). `--no-auth` is rejected on non-loopback hosts.
|
|
424
|
+
- **Per-key authorization:** a key in `authorized_keys` can carry an optional `allow:` token (`<pubkey> <name> allow:read-page,control`) listing its categories (`all`, `safe`, `read-page`, `control`, `dangerous`, `keys`). That key uses its own policy, overriding the server-wide `--allow-*` default; keys without a token fall back to the default. Set it with `auth trust <pubkey> --allow-control …` when adding a key, or change it later with `auth policy <pubkey|name> …` (interactive picker when run with no args; `--safe`/`--server-default`/`--allow-*` for scripting). Both work locally and over `--remote`; `auth keys` shows each key's policy.
|
|
425
|
+
- **Key-management is its own category:** listing/trusting/repolicing keys (`auth keys`/`auth trust`/`auth policy` over `--remote`) requires the `keys` category. A key trusted only for browsing — even with full `control`+`dangerous` — cannot manage the trust store unless granted `allow:keys` (or `allow:all`). This prevents a compromised browser key from escalating by trusting its own.
|
|
437
426
|
- **Rate limiting:** `--rate-limit N` caps commands/second per client key (token bucket, default `100`, `0` disables) so a compromised key can't hammer the browser.
|
|
438
427
|
- **Audit logging:** request logs include the acting key (its name from `authorized_keys` plus a short pubkey), not just the client address.
|
|
439
|
-
- **`serve-http`** is a convenience gateway with the inverse trade-off: commands are gated by the same `--allow-*` policy (safe-only by default), but the bearer token travels in **clear text over plain HTTP**. It binds to loopback by default; `--no-auth` is only permitted there. If you must expose it
|
|
428
|
+
- **`serve-http`** is a convenience gateway with the inverse trade-off: commands are gated by the same `--allow-*` policy (safe-only by default) and requests are throttled per client address (`--rate-limit`, default `100`/s) with an 8 MB body cap, but the bearer token travels in **clear text over plain HTTP**. It binds to loopback by default; `--no-auth` is only permitted there, and binding beyond loopback prints a loud cleartext warning. If you must expose it, put it behind a TLS-terminating reverse proxy — never send the token over an untrusted network unencrypted, and prefer `serve` (encrypted) for real remote use.
|
|
440
429
|
|
|
441
430
|
For low latency, an authenticated encrypted remote connection is kept open and reused for further commands in the same process — so SDK scripts and multi-browser fan-out avoid repeating the TCP/TLS/challenge handshake on every command. Aggregate commands also fan out to remote targets concurrently. Both degrade gracefully against older servers that handle one command per connection.
|
|
442
431
|
|
|
443
432
|
---
|
|
444
433
|
|
|
445
434
|
## Python SDK
|
|
446
|
-
|
|
447
435
|
```python
|
|
448
436
|
from browser_cli import AsyncBrowserCLI, BrowserCLI
|
|
449
437
|
|
|
@@ -579,7 +567,6 @@ raw = b.command("tabs.count", {"pattern": "github"}) # escape hatch for raw com
|
|
|
579
567
|
```
|
|
580
568
|
|
|
581
569
|
**Error handling**
|
|
582
|
-
|
|
583
570
|
```python
|
|
584
571
|
from browser_cli import BrowserCLI, BrowserNotConnected
|
|
585
572
|
|
|
@@ -611,7 +598,6 @@ if isinstance(counts, BrowserCounts):
|
|
|
611
598
|
---
|
|
612
599
|
|
|
613
600
|
## Example scripts
|
|
614
|
-
|
|
615
601
|
See `examples/demo.py` (Python) and `examples/demo.sh` (Bash) for full walkthroughs covering tabs, groups, DOM extraction, and session management.
|
|
616
602
|
|
|
617
603
|
```sh
|
|
@@ -622,7 +608,6 @@ bash examples/demo.sh
|
|
|
622
608
|
---
|
|
623
609
|
|
|
624
610
|
## Development
|
|
625
|
-
|
|
626
611
|
```sh
|
|
627
612
|
npm ci
|
|
628
613
|
npm run check:extension # type-check, build extension bundles, syntax-check bundle
|
|
@@ -663,7 +648,6 @@ For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `
|
|
|
663
648
|
---
|
|
664
649
|
|
|
665
650
|
## Limitations
|
|
666
|
-
|
|
667
651
|
- **Browser internal pages** (`chrome://`, `brave://`, `edge://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
|
668
652
|
- **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>`.
|
|
669
653
|
- **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.
|
|
@@ -672,7 +656,6 @@ For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `
|
|
|
672
656
|
---
|
|
673
657
|
|
|
674
658
|
## License
|
|
675
|
-
|
|
676
659
|
PolyForm Noncommercial License 1.0.0. See [LICENSE](LICENSE).
|
|
677
660
|
|
|
678
661
|
Commercial use is not permitted under this license. For commercial licensing, contact the project maintainer.
|
|
@@ -37,7 +37,6 @@ terminal / python script / remote client
|
|
|
37
37
|
No local server needs to be running beforehand. The browser manages the native host's lifecycle. For cross-machine control, `browser-cli serve` starts an explicit TCP listener protected by Ed25519 public-key authentication unless you opt out with `--no-auth`.
|
|
38
38
|
|
|
39
39
|
**Message format**
|
|
40
|
-
|
|
41
40
|
Every command is a JSON object:
|
|
42
41
|
```json
|
|
43
42
|
{ "id": "uuid", "command": "tabs.list", "args": {} }
|
|
@@ -50,7 +49,6 @@ Every response:
|
|
|
50
49
|
---
|
|
51
50
|
|
|
52
51
|
## Installation
|
|
53
|
-
|
|
54
52
|
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox
|
|
55
53
|
|
|
56
54
|
browser-cli has two parts: the **CLI / native host** (a Python package) and the **browser extension** (published on the public stores).
|
|
@@ -109,7 +107,6 @@ Only the `browser-cli` command needs to be on your `PATH`. The browser launches
|
|
|
109
107
|
---
|
|
110
108
|
|
|
111
109
|
## Project structure
|
|
112
|
-
|
|
113
110
|
```text
|
|
114
111
|
browser-cli/
|
|
115
112
|
├── browser_cli/
|
|
@@ -145,7 +142,6 @@ browser-cli/
|
|
|
145
142
|
---
|
|
146
143
|
|
|
147
144
|
## CLI reference
|
|
148
|
-
|
|
149
145
|
During source development, commands are usually run as `uv run browser-cli [--browser ALIAS] <command>`. After tool installation, use `browser-cli ...` directly. Add `--remote HOST[:PORT]` and optionally `--key PATH` to target a browser exposed by `browser-cli serve`.
|
|
150
146
|
|
|
151
147
|
If exactly one browser instance is connected, commands auto-target it. Use `--browser ALIAS` when multiple browser instances are connected. `tabs list`, `tabs count`, `groups list`, `groups count`, `windows list`, and `session list` aggregate across all active browsers when `--browser` is omitted; in that mode they show the source browser alias or UUID. When local and saved remote browsers are mixed, tables group rows by source (`local` or the remote endpoint) and indent the browser profile below that group. You can inspect active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli clients rename --browser <current-alias> <new-alias>`. Closed browsers are removed from the client registry automatically.
|
|
@@ -153,7 +149,6 @@ If exactly one browser instance is connected, commands auto-target it. Use `--br
|
|
|
153
149
|
Important: profile aliases are browser-instance aliases, not window aliases. Window aliases created with `windows rename` are only for targeting windows in commands like `nav open --window work`. If a browser instance has no explicit profile alias set, the native host gives it a generated UUID alias so multiple unaliased browsers stay distinct.
|
|
154
150
|
|
|
155
151
|
### Navigation (`nav`)
|
|
156
|
-
|
|
157
152
|
```sh
|
|
158
153
|
# Open a URL (no focus stealing by default)
|
|
159
154
|
browser-cli nav open https://example.com
|
|
@@ -175,7 +170,6 @@ browser-cli nav focus github # focuses first tab whose URL contains "
|
|
|
175
170
|
```
|
|
176
171
|
|
|
177
172
|
### Search
|
|
178
|
-
|
|
179
173
|
Each search command opens the search results in your browser using the same flags as `nav open`.
|
|
180
174
|
|
|
181
175
|
```sh
|
|
@@ -199,7 +193,6 @@ browser-cli search so click choices
|
|
|
199
193
|
```
|
|
200
194
|
|
|
201
195
|
### Tabs
|
|
202
|
-
|
|
203
196
|
```sh
|
|
204
197
|
browser-cli tabs list # list all open tabs (all windows)
|
|
205
198
|
browser-cli tabs count # count all tabs
|
|
@@ -227,7 +220,6 @@ browser-cli tabs merge-windows # pull all tabs into the current wi
|
|
|
227
220
|
```
|
|
228
221
|
|
|
229
222
|
### Tab groups
|
|
230
|
-
|
|
231
223
|
```sh
|
|
232
224
|
browser-cli groups list # list all tab groups
|
|
233
225
|
browser-cli groups count # count groups
|
|
@@ -249,7 +241,6 @@ browser-cli groups move 42 -l # short left alias
|
|
|
249
241
|
```
|
|
250
242
|
|
|
251
243
|
### Windows
|
|
252
|
-
|
|
253
244
|
```sh
|
|
254
245
|
browser-cli windows list # list all windows
|
|
255
246
|
browser-cli windows open # open a new window
|
|
@@ -259,7 +250,6 @@ browser-cli windows close 1 # close a window
|
|
|
259
250
|
```
|
|
260
251
|
|
|
261
252
|
### DOM
|
|
262
|
-
|
|
263
253
|
These commands run on the **active tab**. The tab must be on a regular `http://` or `https://` page — not a browser internal page like `brave://newtab`.
|
|
264
254
|
|
|
265
255
|
```sh
|
|
@@ -272,7 +262,6 @@ browser-cli dom type "#search" "hello" # type text into an input
|
|
|
272
262
|
```
|
|
273
263
|
|
|
274
264
|
### Extract
|
|
275
|
-
|
|
276
265
|
```sh
|
|
277
266
|
browser-cli extract links # all <a href> links on the page
|
|
278
267
|
browser-cli extract images # all <img> tags (src + alt)
|
|
@@ -284,7 +273,6 @@ browser-cli extract markdown --selector "article" # specific DOM subtree as Ma
|
|
|
284
273
|
```
|
|
285
274
|
|
|
286
275
|
### Sessions
|
|
287
|
-
|
|
288
276
|
A session is a snapshot of all open tab URLs, stored inside the extension via `chrome.storage.local`. Sessions survive browser restarts but are lost if the extension is uninstalled or extension data is cleared.
|
|
289
277
|
|
|
290
278
|
```sh
|
|
@@ -298,7 +286,6 @@ browser-cli session auto-save off
|
|
|
298
286
|
```
|
|
299
287
|
|
|
300
288
|
### Misc
|
|
301
|
-
|
|
302
289
|
```sh
|
|
303
290
|
browser-cli clients # show connected browser info from the registry
|
|
304
291
|
browser-cli clients rename --browser abcd1234 work # rename one connected browser instance
|
|
@@ -309,7 +296,6 @@ browser-cli completion zsh --script # output raw completion script
|
|
|
309
296
|
```
|
|
310
297
|
|
|
311
298
|
### Remote control, auth, and gateways
|
|
312
|
-
|
|
313
299
|
```sh
|
|
314
300
|
# On the machine with the browser
|
|
315
301
|
browser-cli auth keygen --output ~/.config/browser-cli/client.key
|
|
@@ -334,22 +320,24 @@ browser-cli serve-http --port 8766
|
|
|
334
320
|
curl -H "Authorization: Bearer <token>" http://127.0.0.1:8766/tabs
|
|
335
321
|
```
|
|
336
322
|
|
|
337
|
-
Remote auth uses Ed25519 challenge/response. `--remote` domains default to port 443; explicit `host:port` endpoints are also supported. Saved remote endpoints participate in aggregate list/count commands, where output is grouped by endpoint.
|
|
323
|
+
Remote auth uses Ed25519 challenge/response. `--remote` domains default to port 443; explicit `host:port` endpoints are also supported. Use `browser-cli remote trust ENDPOINT KEY` to remember a key for later calls. Saved remote endpoints participate in aggregate list/count commands, where output is grouped by endpoint.
|
|
338
324
|
|
|
339
|
-
####
|
|
325
|
+
#### n8n integration
|
|
326
|
+
The n8n community node is published as [`n8n-nodes-browser-cli`](https://www.npmjs.com/package/n8n-nodes-browser-cli) on npm. It talks directly to a remote `browser-cli serve` endpoint over the same Ed25519-authenticated, ML-KEM-encrypted TCP protocol as the CLI remote client. Install it from n8n's Community Nodes UI, run `browser-cli serve` on the browser machine, paste the client key into the node credential, and drive tabs/DOM/extraction/raw commands from a workflow. See [`n8n-nodes-browser-cli/README.md`](n8n-nodes-browser-cli/README.md).
|
|
340
327
|
|
|
341
|
-
|
|
342
|
-
|
|
328
|
+
#### Security model
|
|
329
|
+
- **`serve` (TCP)** authenticates every connection with an Ed25519 signature over a fresh server nonce and, for modern clients, wraps the transport in an ML-KEM-768 (post-quantum) AEAD channel. Commands are gated by a **safe-only policy by default** — even a trusted key can only run read-only status/listing commands until you open more with `--allow-read-page`, `--allow-control`, `--allow-dangerous`, `--allow-keys`, or `--allow-all` (full control, including `dom.eval`/`storage.*`). `--no-auth` is rejected on non-loopback hosts.
|
|
330
|
+
- **Per-key authorization:** a key in `authorized_keys` can carry an optional `allow:` token (`<pubkey> <name> allow:read-page,control`) listing its categories (`all`, `safe`, `read-page`, `control`, `dangerous`, `keys`). That key uses its own policy, overriding the server-wide `--allow-*` default; keys without a token fall back to the default. Set it with `auth trust <pubkey> --allow-control …` when adding a key, or change it later with `auth policy <pubkey|name> …` (interactive picker when run with no args; `--safe`/`--server-default`/`--allow-*` for scripting). Both work locally and over `--remote`; `auth keys` shows each key's policy.
|
|
331
|
+
- **Key-management is its own category:** listing/trusting/repolicing keys (`auth keys`/`auth trust`/`auth policy` over `--remote`) requires the `keys` category. A key trusted only for browsing — even with full `control`+`dangerous` — cannot manage the trust store unless granted `allow:keys` (or `allow:all`). This prevents a compromised browser key from escalating by trusting its own.
|
|
343
332
|
- **Rate limiting:** `--rate-limit N` caps commands/second per client key (token bucket, default `100`, `0` disables) so a compromised key can't hammer the browser.
|
|
344
333
|
- **Audit logging:** request logs include the acting key (its name from `authorized_keys` plus a short pubkey), not just the client address.
|
|
345
|
-
- **`serve-http`** is a convenience gateway with the inverse trade-off: commands are gated by the same `--allow-*` policy (safe-only by default), but the bearer token travels in **clear text over plain HTTP**. It binds to loopback by default; `--no-auth` is only permitted there. If you must expose it
|
|
334
|
+
- **`serve-http`** is a convenience gateway with the inverse trade-off: commands are gated by the same `--allow-*` policy (safe-only by default) and requests are throttled per client address (`--rate-limit`, default `100`/s) with an 8 MB body cap, but the bearer token travels in **clear text over plain HTTP**. It binds to loopback by default; `--no-auth` is only permitted there, and binding beyond loopback prints a loud cleartext warning. If you must expose it, put it behind a TLS-terminating reverse proxy — never send the token over an untrusted network unencrypted, and prefer `serve` (encrypted) for real remote use.
|
|
346
335
|
|
|
347
336
|
For low latency, an authenticated encrypted remote connection is kept open and reused for further commands in the same process — so SDK scripts and multi-browser fan-out avoid repeating the TCP/TLS/challenge handshake on every command. Aggregate commands also fan out to remote targets concurrently. Both degrade gracefully against older servers that handle one command per connection.
|
|
348
337
|
|
|
349
338
|
---
|
|
350
339
|
|
|
351
340
|
## Python SDK
|
|
352
|
-
|
|
353
341
|
```python
|
|
354
342
|
from browser_cli import AsyncBrowserCLI, BrowserCLI
|
|
355
343
|
|
|
@@ -485,7 +473,6 @@ raw = b.command("tabs.count", {"pattern": "github"}) # escape hatch for raw com
|
|
|
485
473
|
```
|
|
486
474
|
|
|
487
475
|
**Error handling**
|
|
488
|
-
|
|
489
476
|
```python
|
|
490
477
|
from browser_cli import BrowserCLI, BrowserNotConnected
|
|
491
478
|
|
|
@@ -517,7 +504,6 @@ if isinstance(counts, BrowserCounts):
|
|
|
517
504
|
---
|
|
518
505
|
|
|
519
506
|
## Example scripts
|
|
520
|
-
|
|
521
507
|
See `examples/demo.py` (Python) and `examples/demo.sh` (Bash) for full walkthroughs covering tabs, groups, DOM extraction, and session management.
|
|
522
508
|
|
|
523
509
|
```sh
|
|
@@ -528,7 +514,6 @@ bash examples/demo.sh
|
|
|
528
514
|
---
|
|
529
515
|
|
|
530
516
|
## Development
|
|
531
|
-
|
|
532
517
|
```sh
|
|
533
518
|
npm ci
|
|
534
519
|
npm run check:extension # type-check, build extension bundles, syntax-check bundle
|
|
@@ -569,7 +554,6 @@ For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `
|
|
|
569
554
|
---
|
|
570
555
|
|
|
571
556
|
## Limitations
|
|
572
|
-
|
|
573
557
|
- **Browser internal pages** (`chrome://`, `brave://`, `edge://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
|
574
558
|
- **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>`.
|
|
575
559
|
- **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.
|
|
@@ -578,7 +562,6 @@ For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `
|
|
|
578
562
|
---
|
|
579
563
|
|
|
580
564
|
## License
|
|
581
|
-
|
|
582
565
|
PolyForm Noncommercial License 1.0.0. See [LICENSE](LICENSE).
|
|
583
566
|
|
|
584
567
|
Commercial use is not permitted under this license. For commercial licensing, contact the project maintainer.
|
|
@@ -35,8 +35,6 @@ def add_remote_auth_fields(msg: dict, command: str, requested_profile: str | Non
|
|
|
35
35
|
msg["accept_encoding"] = transport.client_accept_encoding()
|
|
36
36
|
key_spec = key if key is not None else remote_registry.key_for_remote(remote_endpoint)
|
|
37
37
|
private_key = load_private_key(key_spec)
|
|
38
|
-
if key is not None:
|
|
39
|
-
remote_registry.save_remote_key(remote_endpoint, str(key))
|
|
40
38
|
|
|
41
39
|
route_profile = requested_profile
|
|
42
40
|
if not route_profile and command not in NO_ROUTE_COMMANDS:
|
|
@@ -52,8 +50,6 @@ async def add_remote_auth_fields_async(msg: dict, command: str, requested_profil
|
|
|
52
50
|
msg["accept_encoding"] = transport.client_accept_encoding()
|
|
53
51
|
key_spec = key if key is not None else await asyncio.to_thread(remote_registry.key_for_remote, remote_endpoint)
|
|
54
52
|
private_key = await asyncio.to_thread(load_private_key, key_spec)
|
|
55
|
-
if key is not None:
|
|
56
|
-
await asyncio.to_thread(remote_registry.save_remote_key, remote_endpoint, str(key))
|
|
57
53
|
|
|
58
54
|
route_profile = requested_profile
|
|
59
55
|
if not route_profile and command not in NO_ROUTE_COMMANDS:
|
|
@@ -11,9 +11,13 @@ from rich.console import Console
|
|
|
11
11
|
from browser_cli import BrowserCLI
|
|
12
12
|
from browser_cli.command_security import CommandPolicy, assert_command_allowed
|
|
13
13
|
from browser_cli.commands import command_policy_from_options, command_policy_options
|
|
14
|
+
from browser_cli.serve.security import RateLimiter
|
|
14
15
|
|
|
15
16
|
console = Console()
|
|
16
17
|
|
|
18
|
+
# Hard cap on request body size so a bogus Content-Length can't exhaust memory.
|
|
19
|
+
MAX_BODY_BYTES = 8 * 1024 * 1024
|
|
20
|
+
|
|
17
21
|
def _is_loopback(host: str) -> bool:
|
|
18
22
|
return host in {"127.0.0.1", "localhost", "::1"}
|
|
19
23
|
|
|
@@ -21,6 +25,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
21
25
|
client: BrowserCLI
|
|
22
26
|
token: str | None = None
|
|
23
27
|
policy: CommandPolicy = CommandPolicy()
|
|
28
|
+
rate_limiter: RateLimiter | None = None
|
|
24
29
|
|
|
25
30
|
def _authorized(self) -> bool:
|
|
26
31
|
if self.token is None:
|
|
@@ -37,6 +42,12 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
37
42
|
self._send(401, {"error": "missing or invalid token"})
|
|
38
43
|
return False
|
|
39
44
|
|
|
45
|
+
def _within_rate_limit(self) -> bool:
|
|
46
|
+
if self.rate_limiter is None or self.rate_limiter.allow(self.client_address[0]):
|
|
47
|
+
return True
|
|
48
|
+
self._send(429, {"error": "rate limit exceeded; slow down and retry"})
|
|
49
|
+
return False
|
|
50
|
+
|
|
40
51
|
def _send(self, status: int, payload):
|
|
41
52
|
raw = json.dumps(payload, default=str).encode("utf-8")
|
|
42
53
|
self.send_response(status)
|
|
@@ -48,8 +59,11 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
48
59
|
def do_GET(self):
|
|
49
60
|
path = urlparse(self.path).path
|
|
50
61
|
try:
|
|
51
|
-
if path != "/health"
|
|
52
|
-
|
|
62
|
+
if path != "/health":
|
|
63
|
+
if not self._require_auth():
|
|
64
|
+
return
|
|
65
|
+
if not self._within_rate_limit():
|
|
66
|
+
return
|
|
53
67
|
if path == "/tabs":
|
|
54
68
|
self._send(200, [t.__dict__ for t in self.client.tabs.list()])
|
|
55
69
|
elif path == "/clients":
|
|
@@ -64,16 +78,21 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
64
78
|
def do_POST(self):
|
|
65
79
|
path = urlparse(self.path).path
|
|
66
80
|
try:
|
|
81
|
+
if path != "/command":
|
|
82
|
+
self._send(404, {"error": "not found"})
|
|
83
|
+
return
|
|
84
|
+
if not self._require_auth():
|
|
85
|
+
return
|
|
86
|
+
if not self._within_rate_limit():
|
|
87
|
+
return
|
|
67
88
|
length = int(self.headers.get("Content-Length", "0"))
|
|
89
|
+
if length > MAX_BODY_BYTES:
|
|
90
|
+
self._send(413, {"error": f"request body too large (max {MAX_BODY_BYTES} bytes)"})
|
|
91
|
+
return
|
|
68
92
|
body = json.loads(self.rfile.read(length) or b"{}")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
command = body.get("command")
|
|
73
|
-
assert_command_allowed(command, self.policy)
|
|
74
|
-
self._send(200, {"result": self.client.command(command, body.get("args") or {})})
|
|
75
|
-
else:
|
|
76
|
-
self._send(404, {"error": "not found"})
|
|
93
|
+
command = body.get("command")
|
|
94
|
+
assert_command_allowed(command, self.policy)
|
|
95
|
+
self._send(200, {"result": self.client.command(command, body.get("args") or {})})
|
|
77
96
|
except PermissionError as exc:
|
|
78
97
|
self._send(403, {"error": str(exc)})
|
|
79
98
|
except Exception as exc:
|
|
@@ -90,21 +109,32 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
90
109
|
@click.option("--key", default=None, help="Remote auth key spec")
|
|
91
110
|
@click.option("--token", default=None, help="Bearer token required for HTTP access (generated by default)")
|
|
92
111
|
@click.option("--no-auth", is_flag=True, help="Disable HTTP auth (only allowed on loopback hosts)")
|
|
112
|
+
@click.option("--rate-limit", default=100.0, show_default=True, type=float, help="Max requests/sec per client address (0 disables)")
|
|
93
113
|
@command_policy_options
|
|
94
|
-
def cmd_serve_http(host, port, browser, remote, key, token, no_auth, allow_read_page, allow_control, allow_dangerous, allow_keys, allow_all):
|
|
114
|
+
def cmd_serve_http(host, port, browser, remote, key, token, no_auth, rate_limit, allow_read_page, allow_control, allow_dangerous, allow_keys, allow_all):
|
|
95
115
|
"""Expose a tiny local HTTP JSON gateway (/tabs, /clients, /command).
|
|
96
116
|
|
|
97
117
|
Auth is enabled by default. Pass the printed token as either
|
|
98
118
|
``Authorization: Bearer <token>`` or ``X-Browser-CLI-Token: <token>``.
|
|
119
|
+
|
|
120
|
+
This gateway speaks plain HTTP — the token is sent in clear text. Keep it on
|
|
121
|
+
loopback, or put a TLS-terminating reverse proxy in front before exposing it.
|
|
99
122
|
"""
|
|
100
123
|
if no_auth and not _is_loopback(host):
|
|
101
124
|
raise click.ClickException("--no-auth is only allowed on loopback hosts")
|
|
125
|
+
if not _is_loopback(host):
|
|
126
|
+
console.print(
|
|
127
|
+
"[yellow]Warning:[/yellow] binding beyond loopback — this gateway is plain HTTP and the "
|
|
128
|
+
"token travels in clear text. Put a TLS-terminating reverse proxy in front, or use "
|
|
129
|
+
"[bold]browser-cli serve[/bold] (encrypted) instead."
|
|
130
|
+
)
|
|
102
131
|
auth_token = None if no_auth else (token or secrets.token_urlsafe(32))
|
|
103
132
|
policy = command_policy_from_options(allow_read_page=allow_read_page, allow_control=allow_control, allow_dangerous=allow_dangerous, allow_keys=allow_keys, allow_all=allow_all)
|
|
133
|
+
rate_limiter = RateLimiter(rate_limit) if rate_limit and rate_limit > 0 else None
|
|
104
134
|
handler = type(
|
|
105
135
|
"BrowserCLIHTTPHandler",
|
|
106
136
|
(_Handler,),
|
|
107
|
-
{"client": BrowserCLI(browser=browser, remote=remote, key=key), "token": auth_token, "policy": policy},
|
|
137
|
+
{"client": BrowserCLI(browser=browser, remote=remote, key=key), "token": auth_token, "policy": policy, "rate_limiter": rate_limiter},
|
|
108
138
|
)
|
|
109
139
|
server = ThreadingHTTPServer((host, port), handler)
|
|
110
140
|
console.print(f"[green]HTTP gateway listening on http://{host}:{port}[/green]")
|
|
@@ -11,6 +11,13 @@ class _HtmlNode:
|
|
|
11
11
|
self.text = text
|
|
12
12
|
self.children = []
|
|
13
13
|
|
|
14
|
+
# Cap how deep the parsed tree may nest. Hostile page content (thousands of
|
|
15
|
+
# nested elements) would otherwise blow Python's recursion limit in the
|
|
16
|
+
# depth-first render walkers below. Bounding here protects every walker at once.
|
|
17
|
+
# 200 levels is far beyond any real document; deeper content is flattened, not
|
|
18
|
+
# dropped (its text still reaches the output).
|
|
19
|
+
_MAX_TREE_DEPTH = 200
|
|
20
|
+
|
|
14
21
|
class _HtmlTreeBuilder(HTMLParser):
|
|
15
22
|
_VOID_TAGS = {"br", "hr", "img"}
|
|
16
23
|
|
|
@@ -22,7 +29,9 @@ class _HtmlTreeBuilder(HTMLParser):
|
|
|
22
29
|
def handle_starttag(self, tag, attrs):
|
|
23
30
|
node = _HtmlNode(tag=tag.lower(), attrs=dict(attrs))
|
|
24
31
|
self._stack[-1].children.append(node)
|
|
25
|
-
|
|
32
|
+
# Only descend while under the depth cap; beyond it, children of this node
|
|
33
|
+
# attach to the current (capped) parent — flattened but preserved.
|
|
34
|
+
if node.tag not in self._VOID_TAGS and len(self._stack) < _MAX_TREE_DEPTH:
|
|
26
35
|
self._stack.append(node)
|
|
27
36
|
|
|
28
37
|
def handle_startendtag(self, tag, attrs):
|
|
@@ -57,6 +66,14 @@ def _collapse_blank_lines(value):
|
|
|
57
66
|
def _escape_markdown(text):
|
|
58
67
|
return re.sub(r"([\\`[\]])", r"\\\1", text)
|
|
59
68
|
|
|
69
|
+
# Schemes that are dangerous if the produced markdown is later rendered as HTML
|
|
70
|
+
# by a downstream consumer. The output is plain text here, but neutralising them
|
|
71
|
+
# keeps the converter from laundering an XSS payload through to such a consumer.
|
|
72
|
+
_UNSAFE_URL_SCHEME = re.compile(r"^\s*(?:javascript|vbscript|data)\s*:", re.IGNORECASE)
|
|
73
|
+
|
|
74
|
+
def _safe_url(url):
|
|
75
|
+
return "" if _UNSAFE_URL_SCHEME.match(url or "") else url
|
|
76
|
+
|
|
60
77
|
def _escape_table_cell(text):
|
|
61
78
|
return text.replace("|", r"\|").replace("\n", " ").strip()
|
|
62
79
|
|
|
@@ -86,14 +103,14 @@ def _inline_text(node):
|
|
|
86
103
|
if tag == "br":
|
|
87
104
|
return "\n"
|
|
88
105
|
if tag == "img":
|
|
89
|
-
src = node.attrs.get("src") or ""
|
|
106
|
+
src = _safe_url(node.attrs.get("src") or "")
|
|
90
107
|
alt = _normalize_text(node.attrs.get("alt") or "")
|
|
91
108
|
if not src:
|
|
92
109
|
return ""
|
|
93
110
|
return f"" if alt else f""
|
|
94
111
|
if tag == "a":
|
|
95
112
|
text = _normalize_inline("".join(_inline_text(child) for child in node.children))
|
|
96
|
-
href = node.attrs.get("href") or ""
|
|
113
|
+
href = _safe_url(node.attrs.get("href") or "")
|
|
97
114
|
return f"[{text or href}]({href})" if href else text
|
|
98
115
|
if tag == "code":
|
|
99
116
|
text = _normalize_inline("".join(_inline_text(child) for child in node.children))
|
|
@@ -235,5 +252,10 @@ def _block_to_markdown(node):
|
|
|
235
252
|
def convert_html_to_markdown(html, clean_markdown_output):
|
|
236
253
|
parser = _HtmlTreeBuilder()
|
|
237
254
|
parser.feed(html or "")
|
|
238
|
-
|
|
255
|
+
try:
|
|
256
|
+
markdown = _block_to_markdown(parser.root)
|
|
257
|
+
except RecursionError:
|
|
258
|
+
# The depth cap should prevent this, but never let hostile page content
|
|
259
|
+
# crash the caller: fall back to a flat, tag-stripped text extraction.
|
|
260
|
+
markdown = _normalize_inline(re.sub(r"<[^>]*>", " ", html or ""))
|
|
239
261
|
return clean_markdown_output(markdown)
|