real-browser-cli 0.16.3__tar.gz → 0.16.4__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.3 → real_browser_cli-0.16.4}/.gitea/workflows/testing.yml +2 -2
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/PKG-INFO +8 -3
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/README.md +7 -2
- real_browser_cli-0.16.4/browser_cli/auth/server_identity.py +57 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/client/core.py +29 -0
- real_browser_cli-0.16.4/browser_cli/commands/remote.py +153 -0
- real_browser_cli-0.16.4/browser_cli/compat/auth.py +30 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/compat/commands.py +19 -16
- real_browser_cli-0.16.4/browser_cli/remote/known_hosts.py +108 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/remote/registry.py +28 -7
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/remote/transport.py +7 -1
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/challenge.py +4 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/manifest.json +1 -1
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/tabs-query.ts +22 -2
- real_browser_cli-0.16.4/extension/test/url-match.test.ts +43 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/README.md +42 -7
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/credentials/BrowserCliApi.credentials.ts +17 -0
- real_browser_cli-0.16.4/n8n-nodes-browser-cli/nodes/BrowserCli/BrowserCli.node.ts +870 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/nodes/BrowserCli/browserCli.svg +1 -1
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/nodes/BrowserCli/protocol.ts +78 -0
- real_browser_cli-0.16.4/n8n-nodes-browser-cli/nodes/BrowserCli/request.ts +303 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/nodes/BrowserCli/serveClient.ts +12 -2
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/package-lock.json +2 -2
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/package.json +1 -1
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/test/protocol.test.ts +39 -0
- real_browser_cli-0.16.4/n8n-nodes-browser-cli/test/request.test.ts +189 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/test-dist/protocol.test.mjs +91 -0
- real_browser_cli-0.16.4/n8n-nodes-browser-cli/test-dist/request.test.mjs +413 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/pyproject.toml +1 -1
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_client.py +49 -0
- real_browser_cli-0.16.4/tests/test_compat.py +48 -0
- real_browser_cli-0.16.4/tests/test_known_hosts.py +64 -0
- real_browser_cli-0.16.4/tests/test_remote_registry.py +42 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_serve.py +0 -27
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/uv.lock +166 -181
- real_browser_cli-0.16.3/browser_cli/commands/remote.py +0 -78
- real_browser_cli-0.16.3/browser_cli/compat/auth.py +0 -50
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/BrowserCli.node.ts +0 -382
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/request.ts +0 -129
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/test/request.test.ts +0 -73
- real_browser_cli-0.16.3/n8n-nodes-browser-cli/test-dist/request.test.mjs +0 -147
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/.claude/settings.local.json +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/.gitea/workflows/package-extension.yml +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/.gitea/workflows/publish.yml +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/.gitignore +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/.python-version +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/CLAUDE.md +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/LICENSE +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/PRIVACY.md +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/async_sdk.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/auth/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/auth/agent.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/auth/keys.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/auth/pq.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/auth/signing.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/cli.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/client/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/client/auth.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/client/messages.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/client/targets.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/command_security.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/auth.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/clients.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/completion.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/doctor.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/dom.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/events.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/extension.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/extract.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/groups.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/install.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/navigate.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/page.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/perf.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/raw.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/rendering/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/rendering/common.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/rendering/labels.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/rendering/tabs_tree.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/rendering/windows_tree.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/script.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/search.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/serve.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/serve_http.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/session.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/storage.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/tabs.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/watch.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/windows.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/commands/workspace.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/compat/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/constants.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/endpoints.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/errors.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/framing.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/local_transport.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/markdown/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/markdown/html.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/markdown/render.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/models.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/native/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/native/host.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/native/local_server.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/native/protocol.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/platform.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/registry.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/remote/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/remote/auth.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/remote/pool.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/remote/socket.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/base.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/browser_data.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/decorators.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/dom.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/extension.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/factories.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/groups.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/navigation.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/perf.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/routing.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/session.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/tabs.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/windows.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/sdk/workflow_decorators.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/search/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/search/engines.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/auth.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/control.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/logging.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/proxy.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/runtime.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/serve/security.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/transport/__init__.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/transport/binary.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/transport/codecs.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/browser_cli/version_manager.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/examples/demo.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/examples/demo.sh +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/content.js +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/icon.svg +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/icons/icon-128.png +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/icons/icon-16.png +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/icons/icon-32.png +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/icons/icon-48.png +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/browser-api.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/classes/CommandGroup.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/classes/CommandRegistry.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/classes/JobManager.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/classes/NativeConnection.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/autosave.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/browser-data.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/dom.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/extension.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/groups.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/injected.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/navigation.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/perf.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/session-snapshot.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/session.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/tabs.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/commands/windows.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/dispatch.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/dom-ops.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/extract.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/markdown/code.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/markdown/index.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/markdown/renderer.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/markdown/root.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/markdown/utils.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content/page-info.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/content-dispatch.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/errors.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/group-helpers.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/index.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/scripting.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/storage.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/tab-groups.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/tab-helpers.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/core/throttle.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/index.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/command-args.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/index.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/jobs.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/json.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/messages.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/session.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/tabs.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/src/types/webextension.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/autosave.test.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/browser-api.test.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/chrome-mock.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/clients.test.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/jobs.test.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/navigation.test.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/extension/test/paging.test.ts +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/justfile +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/.claude/settings.local.json +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/.gitignore +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/scripts/copy-assets.mjs +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/scripts/run-tests.mjs +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/n8n-nodes-browser-cli/tsconfig.json +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/package-lock.json +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/package.json +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/scripts/gen_extension_key.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/scripts/package_extension.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/scripts/package_verified_crx.sh +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/scripts/setup_verified_crx_key.sh +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/shell.nix +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/conftest.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_api.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_auth.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_cli.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_commands_cli.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_dom.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_extension_error_page_handling.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_extension_packaging.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_extract.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_framing.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_groups.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_markdown_security.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_native_host.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_nav.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_new_feature_commands.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_page.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_perf.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_performance_integration.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_platform.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_refactor_boundaries.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_registry.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_remote_pool.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_remote_protocol_matrix.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_rendering.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_serve_security.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_session.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_storage.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_tabs.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_transport.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tests/test_windows.py +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/tsconfig.json +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/webstore-assets/screenshots/screenshot-1.png +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/webstore-assets/screenshots/screenshot-2.png +0 -0
- {real_browser_cli-0.16.3 → real_browser_cli-0.16.4}/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.4
|
|
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
|
|
@@ -290,8 +290,9 @@ browser-cli search so click choices
|
|
|
290
290
|
```sh
|
|
291
291
|
browser-cli tabs list # list all open tabs (all windows)
|
|
292
292
|
browser-cli tabs count # count all tabs
|
|
293
|
-
browser-cli tabs count youtube # count tabs
|
|
294
|
-
browser-cli tabs filter youtube # list tabs
|
|
293
|
+
browser-cli tabs count youtube # count tabs whose URL contains "youtube"
|
|
294
|
+
browser-cli tabs filter youtube # list tabs whose URL contains "youtube"
|
|
295
|
+
browser-cli tabs filter 'twitch.tv/*' # glob: list every twitch.tv tab
|
|
295
296
|
browser-cli tabs query "pull request" # search tabs by URL or title
|
|
296
297
|
|
|
297
298
|
browser-cli tabs active 1234 # switch browser focus to tab
|
|
@@ -313,6 +314,10 @@ browser-cli tabs sort --by time
|
|
|
313
314
|
browser-cli tabs merge-windows # pull all tabs into the current window
|
|
314
315
|
```
|
|
315
316
|
|
|
317
|
+
> URL patterns for `tabs filter` / `tabs count` match against the full tab URL.
|
|
318
|
+
> A plain string is a case-sensitive substring (`youtube`); a pattern containing
|
|
319
|
+
> `*` or `?` is treated as a glob (`twitch.tv/*`, `*.twitch.tv`).
|
|
320
|
+
|
|
316
321
|
### Tab groups
|
|
317
322
|
```sh
|
|
318
323
|
browser-cli groups list # list all tab groups
|
|
@@ -196,8 +196,9 @@ browser-cli search so click choices
|
|
|
196
196
|
```sh
|
|
197
197
|
browser-cli tabs list # list all open tabs (all windows)
|
|
198
198
|
browser-cli tabs count # count all tabs
|
|
199
|
-
browser-cli tabs count youtube # count tabs
|
|
200
|
-
browser-cli tabs filter youtube # list tabs
|
|
199
|
+
browser-cli tabs count youtube # count tabs whose URL contains "youtube"
|
|
200
|
+
browser-cli tabs filter youtube # list tabs whose URL contains "youtube"
|
|
201
|
+
browser-cli tabs filter 'twitch.tv/*' # glob: list every twitch.tv tab
|
|
201
202
|
browser-cli tabs query "pull request" # search tabs by URL or title
|
|
202
203
|
|
|
203
204
|
browser-cli tabs active 1234 # switch browser focus to tab
|
|
@@ -219,6 +220,10 @@ browser-cli tabs sort --by time
|
|
|
219
220
|
browser-cli tabs merge-windows # pull all tabs into the current window
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
> URL patterns for `tabs filter` / `tabs count` match against the full tab URL.
|
|
224
|
+
> A plain string is a case-sensitive substring (`youtube`); a pattern containing
|
|
225
|
+
> `*` or `?` is treated as a glob (`twitch.tv/*`, `*.twitch.tv`).
|
|
226
|
+
|
|
222
227
|
### Tab groups
|
|
223
228
|
```sh
|
|
224
229
|
browser-cli groups list # list all tab groups
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Persistent server identity keys for SSH-style remote host pinning."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cryptography.exceptions import InvalidSignature
|
|
8
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
9
|
+
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key
|
|
10
|
+
|
|
11
|
+
from browser_cli.constants import CONFIG_DIR
|
|
12
|
+
|
|
13
|
+
SERVER_IDENTITY_PATH = CONFIG_DIR / "server_identity.pem"
|
|
14
|
+
|
|
15
|
+
def load_or_create_server_identity(path: Path = SERVER_IDENTITY_PATH) -> Ed25519PrivateKey:
|
|
16
|
+
"""Load the persistent serve identity key, creating it on first start."""
|
|
17
|
+
if path.exists():
|
|
18
|
+
key = load_pem_private_key(path.read_bytes(), password=None)
|
|
19
|
+
if not isinstance(key, Ed25519PrivateKey):
|
|
20
|
+
raise ValueError(f"server identity key is not Ed25519: {path}")
|
|
21
|
+
return key
|
|
22
|
+
|
|
23
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
key = Ed25519PrivateKey.generate()
|
|
25
|
+
pem = key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
|
26
|
+
fd = path.open("xb")
|
|
27
|
+
try:
|
|
28
|
+
fd.write(pem)
|
|
29
|
+
finally:
|
|
30
|
+
fd.close()
|
|
31
|
+
path.chmod(0o600)
|
|
32
|
+
return key
|
|
33
|
+
|
|
34
|
+
def public_key_hex(key: Ed25519PrivateKey) -> str:
|
|
35
|
+
return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex()
|
|
36
|
+
|
|
37
|
+
def _signed_challenge_fields(challenge: dict) -> dict:
|
|
38
|
+
return {key: value for key, value in challenge.items() if key != "server_sig"}
|
|
39
|
+
|
|
40
|
+
def challenge_payload(challenge: dict) -> bytes:
|
|
41
|
+
"""Canonical bytes signed by the server identity key."""
|
|
42
|
+
return json.dumps(_signed_challenge_fields(challenge), sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
43
|
+
|
|
44
|
+
def sign_challenge(challenge: dict, key: Ed25519PrivateKey) -> str:
|
|
45
|
+
return key.sign(challenge_payload(challenge)).hex()
|
|
46
|
+
|
|
47
|
+
def verify_challenge_signature(challenge: dict) -> bool:
|
|
48
|
+
pub_hex = challenge.get("server_pubkey")
|
|
49
|
+
sig_hex = challenge.get("server_sig")
|
|
50
|
+
if not isinstance(pub_hex, str) or not isinstance(sig_hex, str):
|
|
51
|
+
return False
|
|
52
|
+
try:
|
|
53
|
+
pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_hex))
|
|
54
|
+
pub.verify(bytes.fromhex(sig_hex), challenge_payload(challenge))
|
|
55
|
+
return True
|
|
56
|
+
except (InvalidSignature, ValueError):
|
|
57
|
+
return False
|
|
@@ -243,6 +243,35 @@ def collect_browser_clients(
|
|
|
243
243
|
return rows
|
|
244
244
|
|
|
245
245
|
if remote:
|
|
246
|
+
targets = remote_browser_targets(remote, key=key)
|
|
247
|
+
if browser_alias:
|
|
248
|
+
targets = [target for target in targets if target.profile == browser_alias or target.display_name == browser_alias]
|
|
249
|
+
if targets:
|
|
250
|
+
uncached = []
|
|
251
|
+
for target in targets:
|
|
252
|
+
cached = _cached_client_row(target)
|
|
253
|
+
if cached is not None:
|
|
254
|
+
rows.append(cached)
|
|
255
|
+
else:
|
|
256
|
+
uncached.append(target)
|
|
257
|
+
results = _run_concurrent([
|
|
258
|
+
(lambda t=t: _client_rows_async(
|
|
259
|
+
t.display_name,
|
|
260
|
+
profile=t.profile,
|
|
261
|
+
remote=remote,
|
|
262
|
+
key=key,
|
|
263
|
+
profile_group=t.display_group,
|
|
264
|
+
))
|
|
265
|
+
for t in uncached
|
|
266
|
+
])
|
|
267
|
+
for result in results:
|
|
268
|
+
if isinstance(result, (BrowserNotConnected, RuntimeError)):
|
|
269
|
+
continue
|
|
270
|
+
if isinstance(result, BaseException):
|
|
271
|
+
raise result
|
|
272
|
+
rows.extend(result)
|
|
273
|
+
return rows
|
|
274
|
+
|
|
246
275
|
result = send_command("clients.list", profile=browser_alias, remote=remote, key=key)
|
|
247
276
|
for item in result or []:
|
|
248
277
|
row = dict(item)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from browser_cli.errors import BrowserNotConnected
|
|
8
|
+
|
|
9
|
+
from browser_cli import BrowserCLI
|
|
10
|
+
from browser_cli.commands import handle_errors
|
|
11
|
+
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
|
12
|
+
from browser_cli.remote.known_hosts import fingerprint, load_known_hosts, remove_known_host, save_known_host
|
|
13
|
+
from browser_cli.remote.registry import load_remotes, remove_remote, save_remote, save_remote_key
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
def _print_remotes() -> None:
|
|
18
|
+
remotes = load_remotes()
|
|
19
|
+
if not remotes:
|
|
20
|
+
console.print("[yellow]No remembered remotes[/yellow]")
|
|
21
|
+
return
|
|
22
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
23
|
+
table.add_column("Endpoint")
|
|
24
|
+
table.add_column("Key")
|
|
25
|
+
for endpoint, cfg in sorted(remotes.items()):
|
|
26
|
+
table.add_row(endpoint, str(cfg.get("key", "")))
|
|
27
|
+
console.print(table)
|
|
28
|
+
|
|
29
|
+
def _remove_remote(endpoint: str, *, verb: str) -> None:
|
|
30
|
+
if not remove_remote(endpoint):
|
|
31
|
+
console.print(f"[yellow]Remote {endpoint} not remembered[/yellow]")
|
|
32
|
+
return
|
|
33
|
+
console.print(f"[green]{verb} {endpoint}[/green]")
|
|
34
|
+
|
|
35
|
+
def _fetch_server_pubkey(endpoint: str) -> str:
|
|
36
|
+
from browser_cli.auth.server_identity import verify_challenge_signature
|
|
37
|
+
from browser_cli.remote.auth import parse_challenge
|
|
38
|
+
from browser_cli.remote.socket import connect_socket, recv_all
|
|
39
|
+
|
|
40
|
+
sock = connect_socket(endpoint)
|
|
41
|
+
try:
|
|
42
|
+
challenge, _nonce = parse_challenge(recv_all(sock) or b"")
|
|
43
|
+
finally:
|
|
44
|
+
sock.close()
|
|
45
|
+
if not isinstance(challenge, dict) or not isinstance(challenge.get("server_pubkey"), str):
|
|
46
|
+
raise BrowserNotConnected("remote server did not advertise a server identity key")
|
|
47
|
+
if not verify_challenge_signature(challenge):
|
|
48
|
+
raise BrowserNotConnected("remote server identity signature is invalid")
|
|
49
|
+
return str(challenge["server_pubkey"])
|
|
50
|
+
|
|
51
|
+
@click.group("remote")
|
|
52
|
+
def remote_group():
|
|
53
|
+
"""Manage remembered browser-cli remote endpoints."""
|
|
54
|
+
|
|
55
|
+
@remote_group.command("status")
|
|
56
|
+
@click.argument("endpoint")
|
|
57
|
+
@click.option("--key", default=None, help="Key spec/path to use for this probe")
|
|
58
|
+
@handle_errors
|
|
59
|
+
def remote_status(endpoint, key):
|
|
60
|
+
"""Probe a remote endpoint and show server/client status."""
|
|
61
|
+
client = BrowserCLI(remote=endpoint, key=key)
|
|
62
|
+
clients = [
|
|
63
|
+
{**item, "profileLabel": item.get("profile", ""), "profileGroup": endpoint}
|
|
64
|
+
for item in client.clients()
|
|
65
|
+
]
|
|
66
|
+
columns = [
|
|
67
|
+
("Browser", lambda item: item.get("name", "")),
|
|
68
|
+
("Extension", lambda item: item.get("extensionVersion", "")),
|
|
69
|
+
]
|
|
70
|
+
print_browser_grouped_table_rows(
|
|
71
|
+
clients,
|
|
72
|
+
columns,
|
|
73
|
+
console=console,
|
|
74
|
+
empty_message="[yellow]No browser clients found[/yellow]",
|
|
75
|
+
browser_getter=lambda item: item.get("profileLabel", ""),
|
|
76
|
+
group_getter=lambda item: item.get("profileGroup", ""),
|
|
77
|
+
browser_header="Profile",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@remote_group.command("add")
|
|
81
|
+
@click.argument("endpoint")
|
|
82
|
+
@click.option("--key", "key_spec", default=None, help="Key spec/path to remember for this endpoint")
|
|
83
|
+
def remote_add(endpoint, key_spec):
|
|
84
|
+
"""Remember a remote endpoint for global multi-browser commands."""
|
|
85
|
+
save_remote(endpoint, key_spec)
|
|
86
|
+
if key_spec:
|
|
87
|
+
console.print(f"[green]Added remote {endpoint} with key {key_spec}[/green]")
|
|
88
|
+
else:
|
|
89
|
+
console.print(f"[green]Added remote {endpoint}[/green]")
|
|
90
|
+
|
|
91
|
+
@remote_group.command("trust")
|
|
92
|
+
@click.argument("endpoint")
|
|
93
|
+
@click.argument("key_spec")
|
|
94
|
+
def remote_trust(endpoint, key_spec):
|
|
95
|
+
"""Remember which key spec to use for ENDPOINT."""
|
|
96
|
+
save_remote_key(endpoint, key_spec)
|
|
97
|
+
console.print(f"[green]Trusted remote {endpoint} with key {key_spec}[/green]")
|
|
98
|
+
|
|
99
|
+
@remote_group.command("list")
|
|
100
|
+
def remote_list():
|
|
101
|
+
"""List remembered remote endpoints."""
|
|
102
|
+
_print_remotes()
|
|
103
|
+
|
|
104
|
+
@remote_group.command("trust-host")
|
|
105
|
+
@click.argument("endpoint")
|
|
106
|
+
@click.option("--pubkey", default=None, help="Pin this server public key instead of probing the endpoint")
|
|
107
|
+
@handle_errors
|
|
108
|
+
def remote_trust_host(endpoint, pubkey):
|
|
109
|
+
"""Pin a remote server identity key, SSH known_hosts style."""
|
|
110
|
+
server_pubkey = pubkey or _fetch_server_pubkey(endpoint)
|
|
111
|
+
save_known_host(endpoint, server_pubkey)
|
|
112
|
+
console.print(f"[green]Trusted server {endpoint}[/green] [dim]{fingerprint(server_pubkey)}[/dim]")
|
|
113
|
+
|
|
114
|
+
@remote_group.command("known-hosts")
|
|
115
|
+
def remote_known_hosts():
|
|
116
|
+
"""List pinned remote server identity keys."""
|
|
117
|
+
known = load_known_hosts()
|
|
118
|
+
if not known:
|
|
119
|
+
console.print("[yellow]No known remote server identities[/yellow]")
|
|
120
|
+
return
|
|
121
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
122
|
+
table.add_column("Endpoint")
|
|
123
|
+
table.add_column("Fingerprint")
|
|
124
|
+
table.add_column("Public Key")
|
|
125
|
+
for endpoint, pubkey in sorted(known.items()):
|
|
126
|
+
table.add_row(endpoint, fingerprint(pubkey), pubkey)
|
|
127
|
+
console.print(table)
|
|
128
|
+
|
|
129
|
+
@remote_group.command("untrust-host")
|
|
130
|
+
@click.argument("endpoint")
|
|
131
|
+
def remote_untrust_host(endpoint):
|
|
132
|
+
"""Remove a pinned remote server identity key."""
|
|
133
|
+
if not remove_known_host(endpoint):
|
|
134
|
+
console.print(f"[yellow]Remote server {endpoint} is not in known hosts[/yellow]")
|
|
135
|
+
return
|
|
136
|
+
console.print(f"[green]Removed server identity for {endpoint}[/green]")
|
|
137
|
+
|
|
138
|
+
@remote_group.command("keys")
|
|
139
|
+
def remote_keys():
|
|
140
|
+
"""List remembered remote key specs."""
|
|
141
|
+
_print_remotes()
|
|
142
|
+
|
|
143
|
+
@remote_group.command("remove")
|
|
144
|
+
@click.argument("endpoint")
|
|
145
|
+
def remote_remove(endpoint):
|
|
146
|
+
"""Remove a remembered remote endpoint."""
|
|
147
|
+
_remove_remote(endpoint, verb="Removed")
|
|
148
|
+
|
|
149
|
+
@remote_group.command("revoke")
|
|
150
|
+
@click.argument("endpoint")
|
|
151
|
+
def remote_revoke(endpoint):
|
|
152
|
+
"""Remove remembered key/config for ENDPOINT."""
|
|
153
|
+
_remove_remote(endpoint, verb="Revoked")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth-field normalizers — applied to the raw incoming message *before* the
|
|
3
|
+
auth check runs. Protocol fields (pubkey, sig, …) are still present here.
|
|
4
|
+
|
|
5
|
+
Add one entry per breaking auth-field change:
|
|
6
|
+
("X.Y.Z", transformer_fn)
|
|
7
|
+
|
|
8
|
+
Entries must stay in ascending version order.
|
|
9
|
+
|
|
10
|
+
The registry is intentionally empty: the first public release was 0.14.1, so no
|
|
11
|
+
legacy-client shim has ever been needed. This module is the seam — add a tuple
|
|
12
|
+
here (and a unit test for it) the day a breaking auth-field change ships.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from typing import Callable
|
|
16
|
+
from browser_cli.version_manager import parse_version
|
|
17
|
+
|
|
18
|
+
# ── registry ──────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
_AUTH_COMPAT: list[tuple[str, Callable[[dict], dict]]] = []
|
|
21
|
+
|
|
22
|
+
def adapt_auth(msg: dict, client_version: str) -> dict:
|
|
23
|
+
"""Apply all auth normalizers needed to bring msg up to the current format."""
|
|
24
|
+
if not _AUTH_COMPAT:
|
|
25
|
+
return msg
|
|
26
|
+
cv = parse_version(client_version)
|
|
27
|
+
for version, fn in _AUTH_COMPAT:
|
|
28
|
+
if cv < parse_version(version):
|
|
29
|
+
msg = fn(msg)
|
|
30
|
+
return msg
|
|
@@ -3,7 +3,7 @@ Command-format shims — applied to clean_msg (protocol fields already stripped)
|
|
|
3
3
|
before forwarding to the native host, and to responses before sending back.
|
|
4
4
|
|
|
5
5
|
Add one entry per breaking command-format change:
|
|
6
|
-
|
|
6
|
+
("X.Y.Z", request_fn, response_fn)
|
|
7
7
|
|
|
8
8
|
- request_fn(msg: dict) -> dict or None
|
|
9
9
|
- response_fn(resp: bytes, command: str) -> bytes or None
|
|
@@ -11,33 +11,36 @@ Add one entry per breaking command-format change:
|
|
|
11
11
|
Entries must stay in ascending version order.
|
|
12
12
|
adapt_request walks forward (oldest first); adapt_response walks backward.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
The registry is intentionally empty: no command-format shim has been needed
|
|
15
|
+
since the first public release (0.14.1). This module is the seam — add a tuple
|
|
16
|
+
here (and a unit test for it) the day a breaking command-format change ships.
|
|
15
17
|
"""
|
|
16
18
|
from __future__ import annotations
|
|
17
19
|
from typing import Callable
|
|
18
20
|
from browser_cli.version_manager import parse_version
|
|
19
21
|
|
|
20
|
-
|
|
21
22
|
# ── registry ──────────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
23
24
|
_COMPAT: list[tuple[str, Callable[[dict], dict] | None, Callable[[bytes, str], bytes] | None]] = [
|
|
24
|
-
|
|
25
|
+
# ("1.0.0", _req_1_0_0, _resp_1_0_0),
|
|
25
26
|
]
|
|
26
27
|
|
|
27
|
-
|
|
28
28
|
def adapt_request(msg: dict, client_version: str) -> dict:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for version, req_fn, _ in _COMPAT:
|
|
32
|
-
if cv < parse_version(version) and req_fn is not None:
|
|
33
|
-
msg = req_fn(msg)
|
|
29
|
+
"""Upgrade a client message to the current browser command format."""
|
|
30
|
+
if not _COMPAT:
|
|
34
31
|
return msg
|
|
35
|
-
|
|
32
|
+
cv = parse_version(client_version)
|
|
33
|
+
for version, req_fn, _ in _COMPAT:
|
|
34
|
+
if cv < parse_version(version) and req_fn is not None:
|
|
35
|
+
msg = req_fn(msg)
|
|
36
|
+
return msg
|
|
36
37
|
|
|
37
38
|
def adapt_response(resp: bytes, command: str, client_version: str) -> bytes:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for version, _, resp_fn in reversed(_COMPAT):
|
|
41
|
-
if cv < parse_version(version) and resp_fn is not None:
|
|
42
|
-
resp = resp_fn(resp, command)
|
|
39
|
+
"""Downgrade a native-host response to the format the client expects."""
|
|
40
|
+
if not _COMPAT:
|
|
43
41
|
return resp
|
|
42
|
+
cv = parse_version(client_version)
|
|
43
|
+
for version, _, resp_fn in reversed(_COMPAT):
|
|
44
|
+
if cv < parse_version(version) and resp_fn is not None:
|
|
45
|
+
resp = resp_fn(resp, command)
|
|
46
|
+
return resp
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""SSH-style known-hosts pinning for browser-cli remote servers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from browser_cli.constants import CONFIG_DIR
|
|
10
|
+
from browser_cli.endpoints import _normalize_endpoint
|
|
11
|
+
from browser_cli.errors import BrowserNotConnected
|
|
12
|
+
|
|
13
|
+
KNOWN_HOSTS_PATH = CONFIG_DIR / "known_hosts.json"
|
|
14
|
+
|
|
15
|
+
def fingerprint(pubkey_hex: str) -> str:
|
|
16
|
+
"""Return a compact SHA256 fingerprint for display."""
|
|
17
|
+
import base64
|
|
18
|
+
import hashlib
|
|
19
|
+
|
|
20
|
+
digest = hashlib.sha256(bytes.fromhex(pubkey_hex)).digest()
|
|
21
|
+
return "SHA256:" + base64.b64encode(digest).decode("ascii").rstrip("=")
|
|
22
|
+
|
|
23
|
+
def load_known_hosts(path: Path | None = None) -> dict[str, str]:
|
|
24
|
+
path = path or KNOWN_HOSTS_PATH
|
|
25
|
+
if not path.exists():
|
|
26
|
+
return {}
|
|
27
|
+
try:
|
|
28
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
29
|
+
except Exception:
|
|
30
|
+
return {}
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
return {}
|
|
33
|
+
return {_normalize_endpoint(str(endpoint)): str(pubkey) for endpoint, pubkey in data.items() if isinstance(pubkey, str)}
|
|
34
|
+
|
|
35
|
+
def save_known_host(endpoint: str, pubkey_hex: str, path: Path | None = None) -> None:
|
|
36
|
+
path = path or KNOWN_HOSTS_PATH
|
|
37
|
+
known = load_known_hosts(path)
|
|
38
|
+
known[_normalize_endpoint(endpoint)] = pubkey_hex
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
41
|
+
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
|
42
|
+
file.write(json.dumps(known, indent=2, sort_keys=True) + "\n")
|
|
43
|
+
|
|
44
|
+
def remove_known_host(endpoint: str, path: Path | None = None) -> bool:
|
|
45
|
+
path = path or KNOWN_HOSTS_PATH
|
|
46
|
+
known = load_known_hosts(path)
|
|
47
|
+
normalized = _normalize_endpoint(endpoint)
|
|
48
|
+
if normalized not in known:
|
|
49
|
+
return False
|
|
50
|
+
del known[normalized]
|
|
51
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
53
|
+
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
|
54
|
+
file.write(json.dumps(known, indent=2, sort_keys=True) + "\n")
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def _is_loopback_endpoint(endpoint: str) -> bool:
|
|
58
|
+
host, sep, _port = endpoint.rpartition(":")
|
|
59
|
+
check = host if sep else endpoint
|
|
60
|
+
return check in {"127.0.0.1", "localhost", "::1"}
|
|
61
|
+
|
|
62
|
+
def verify_known_host(endpoint: str, challenge: dict | None) -> None:
|
|
63
|
+
"""Verify and pin the server identity from a challenge frame.
|
|
64
|
+
|
|
65
|
+
First contact auto-adds the host when the process is interactive, mirroring
|
|
66
|
+
SSH's trust-on-first-use flow. Non-interactive clients must pin explicitly via
|
|
67
|
+
`browser-cli remote trust-host ENDPOINT`.
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(challenge, dict):
|
|
70
|
+
return
|
|
71
|
+
pubkey = challenge.get("server_pubkey")
|
|
72
|
+
if not isinstance(pubkey, str) or not pubkey:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
from browser_cli.auth.server_identity import verify_challenge_signature
|
|
76
|
+
if not verify_challenge_signature(challenge):
|
|
77
|
+
raise BrowserNotConnected("Remote server identity signature is invalid")
|
|
78
|
+
|
|
79
|
+
normalized = _normalize_endpoint(endpoint)
|
|
80
|
+
known = load_known_hosts()
|
|
81
|
+
expected = known.get(normalized)
|
|
82
|
+
if expected is None and _is_loopback_endpoint(endpoint):
|
|
83
|
+
return
|
|
84
|
+
if expected is None:
|
|
85
|
+
if not sys.stdin.isatty():
|
|
86
|
+
raise BrowserNotConnected(
|
|
87
|
+
f"Unknown remote server identity for {normalized} ({fingerprint(pubkey)}).\n"
|
|
88
|
+
f"Run: browser-cli remote trust-host {normalized}"
|
|
89
|
+
)
|
|
90
|
+
sys.stderr.write(
|
|
91
|
+
f"The authenticity of remote '{normalized}' can't be established.\n"
|
|
92
|
+
f"Server key fingerprint is {fingerprint(pubkey)}.\n"
|
|
93
|
+
"Trust this server and add it to known hosts? [y/N] "
|
|
94
|
+
)
|
|
95
|
+
answer = sys.stdin.readline().strip().lower()
|
|
96
|
+
if answer not in {"y", "yes"}:
|
|
97
|
+
raise BrowserNotConnected("Remote server identity was not trusted")
|
|
98
|
+
save_known_host(normalized, pubkey)
|
|
99
|
+
sys.stderr.write(f"Added {normalized} to browser-cli known hosts.\n")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if expected != pubkey:
|
|
103
|
+
raise BrowserNotConnected(
|
|
104
|
+
f"REMOTE SERVER IDENTITY CHANGED for {normalized}!\n"
|
|
105
|
+
f"Known: {fingerprint(expected)}\n"
|
|
106
|
+
f"Seen: {fingerprint(pubkey)}\n"
|
|
107
|
+
f"If this is expected, run: browser-cli remote untrust-host {normalized} && browser-cli remote trust-host {normalized}"
|
|
108
|
+
)
|
|
@@ -28,18 +28,39 @@ def is_valid_key_spec(value: str) -> bool:
|
|
|
28
28
|
not value.startswith("<") and ("/" in value or Path(value).suffix in {".pem", ".key"})
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
def
|
|
32
|
-
"""Persist
|
|
33
|
-
if not endpoint
|
|
31
|
+
def save_remote(endpoint: str, key_spec: str | None = None) -> None:
|
|
32
|
+
"""Persist a remote endpoint, optionally with a key spec."""
|
|
33
|
+
if not endpoint:
|
|
34
34
|
return
|
|
35
|
+
normalized = _normalize_endpoint(endpoint)
|
|
35
36
|
remotes = load_remotes()
|
|
36
|
-
current = remotes.get(
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
current = remotes.get(normalized, {})
|
|
38
|
+
if key_spec:
|
|
39
|
+
if not is_valid_key_spec(key_spec):
|
|
40
|
+
return
|
|
41
|
+
current["key"] = key_spec
|
|
42
|
+
remotes[normalized] = current
|
|
39
43
|
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
40
44
|
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
41
45
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
42
|
-
f.write(json.dumps(remotes, indent=2, sort_keys=True))
|
|
46
|
+
f.write(json.dumps(remotes, indent=2, sort_keys=True) + "\n")
|
|
47
|
+
|
|
48
|
+
def remove_remote(endpoint: str) -> bool:
|
|
49
|
+
"""Remove a remembered remote endpoint. Returns True when it existed."""
|
|
50
|
+
normalized = _normalize_endpoint(endpoint)
|
|
51
|
+
remotes = load_remotes()
|
|
52
|
+
if normalized not in remotes:
|
|
53
|
+
return False
|
|
54
|
+
del remotes[normalized]
|
|
55
|
+
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
57
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
58
|
+
f.write(json.dumps(remotes, indent=2, sort_keys=True) + "\n")
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def save_remote_key(endpoint: str, key_spec: str) -> None:
|
|
62
|
+
"""Persist the key spec (e.g. 'agent' or a file path) for a remote endpoint."""
|
|
63
|
+
save_remote(endpoint, key_spec)
|
|
43
64
|
|
|
44
65
|
def key_for_remote(endpoint: str | None) -> str | None:
|
|
45
66
|
if not endpoint:
|
|
@@ -28,6 +28,7 @@ from browser_cli.remote.socket import (
|
|
|
28
28
|
split_endpoint as _split_endpoint,
|
|
29
29
|
)
|
|
30
30
|
from browser_cli.remote import pool as _pool
|
|
31
|
+
from browser_cli.remote.known_hosts import verify_known_host
|
|
31
32
|
|
|
32
33
|
def _send_remote(endpoint: str, msg: dict, private_key=None, *, warn_no_pq: bool | None = None) -> bytes | None:
|
|
33
34
|
# Reuse an already-authenticated connection when one is idle for this endpoint.
|
|
@@ -51,7 +52,10 @@ def _send_remote_handshake(endpoint: str, msg: dict, private_key=None, *, warn_n
|
|
|
51
52
|
|
|
52
53
|
sock = _connect_socket(endpoint)
|
|
53
54
|
try:
|
|
54
|
-
|
|
55
|
+
challenge_raw = _recv_all(sock)
|
|
56
|
+
challenge, _nonce_hex = _parse_challenge(challenge_raw)
|
|
57
|
+
verify_known_host(endpoint, challenge)
|
|
58
|
+
payload_msg, pq_shared_secret = _with_challenge(challenge_raw, msg, private_key, build_auth)
|
|
55
59
|
sock.sendall(frame(json.dumps(payload_msg).encode("utf-8")))
|
|
56
60
|
response = _decode_pq_response(_recv_all(sock), pq_shared_secret)
|
|
57
61
|
except BaseException:
|
|
@@ -69,6 +73,8 @@ async def _send_remote_async(endpoint: str, msg: dict, private_key=None, *, warn
|
|
|
69
73
|
reader, writer = await _open_async_connection(endpoint)
|
|
70
74
|
try:
|
|
71
75
|
challenge_raw = await _async_recv_all(reader)
|
|
76
|
+
challenge, _nonce_hex = _parse_challenge(challenge_raw)
|
|
77
|
+
verify_known_host(endpoint, challenge)
|
|
72
78
|
warn = _should_warn_no_pq(msg) if warn_no_pq is None else warn_no_pq
|
|
73
79
|
|
|
74
80
|
async def build_auth(sync_msg: dict, challenge: dict | None, nonce_hex: str | None, key):
|
|
@@ -28,4 +28,8 @@ async def build_challenge(auth_keys_path: Path | None) -> tuple[str, object | No
|
|
|
28
28
|
if pq_keypair is not None:
|
|
29
29
|
pq_private_key, pq_public_key = pq_keypair
|
|
30
30
|
challenge_msg["pq_kex"] = {"alg": PQ_KEX_ALG, "public_key": pq_public_key.hex()}
|
|
31
|
+
from browser_cli.auth.server_identity import load_or_create_server_identity, public_key_hex, sign_challenge
|
|
32
|
+
server_key = await asyncio.to_thread(load_or_create_server_identity)
|
|
33
|
+
challenge_msg["server_pubkey"] = public_key_hex(server_key)
|
|
34
|
+
challenge_msg["server_sig"] = sign_challenge(challenge_msg, server_key)
|
|
31
35
|
return nonce, pq_private_key, challenge_msg
|
|
@@ -4,6 +4,26 @@ import { CommandGroup } from '../classes/CommandGroup';
|
|
|
4
4
|
import type { CommandEntry } from '../classes/CommandGroup';
|
|
5
5
|
import type { TabIdArgs, TabsActiveInWindowArgs, TabsPatternArgs, TabsQueryArgs, TabsWatchUrlArgs } from '../types';
|
|
6
6
|
|
|
7
|
+
/** Convert a shell-style glob (`*` = any run, `?` = any single char) to an
|
|
8
|
+
* unanchored RegExp. Every other character is matched literally. Unanchored so
|
|
9
|
+
* `twitch.tv/*` matches anywhere inside `https://www.twitch.tv/foo`. */
|
|
10
|
+
function globToRegExp(glob: string): RegExp {
|
|
11
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
return new RegExp(escaped.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Match a tab URL against a pattern. Backward-compatible: a pattern with no
|
|
17
|
+
* glob metacharacters is a plain case-sensitive substring match (the historic
|
|
18
|
+
* behavior); a pattern containing `*` or `?` is treated as a glob, so
|
|
19
|
+
* `twitch.tv/*` matches every Twitch tab.
|
|
20
|
+
*/
|
|
21
|
+
export function urlMatchesPattern(url: string | undefined, pattern: string): boolean {
|
|
22
|
+
if (!url || !pattern) return false;
|
|
23
|
+
if (/[*?]/.test(pattern)) return globToRegExp(pattern).test(url);
|
|
24
|
+
return url.includes(pattern);
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
export class TabsQueryCommands extends CommandGroup {
|
|
8
28
|
readonly namespace = "tabs";
|
|
9
29
|
readonly commands: Record<string, CommandEntry> = {
|
|
@@ -50,12 +70,12 @@ export class TabsQueryCommands extends CommandGroup {
|
|
|
50
70
|
|
|
51
71
|
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
|
52
72
|
const all = await api.tabs.query({});
|
|
53
|
-
return all.filter(t => t.url
|
|
73
|
+
return all.filter(t => urlMatchesPattern(t.url, pattern)).map(tabInfo);
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
private async tabsCount({ pattern }: TabsPatternArgs) {
|
|
57
77
|
const all = await api.tabs.query({});
|
|
58
|
-
if (pattern) return all.filter(t => t.url
|
|
78
|
+
if (pattern) return all.filter(t => urlMatchesPattern(t.url, pattern)).length;
|
|
59
79
|
return all.length;
|
|
60
80
|
}
|
|
61
81
|
|