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.
Files changed (234) hide show
  1. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/PKG-INFO +9 -26
  2. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/README.md +8 -25
  3. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/auth.py +0 -4
  4. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/serve_http.py +42 -12
  5. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/markdown/html.py +26 -4
  6. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/manifest.json +1 -1
  7. real_browser_cli-0.16.3/n8n-nodes-browser-cli/.claude/settings.local.json +7 -0
  8. real_browser_cli-0.16.3/n8n-nodes-browser-cli/.gitignore +4 -0
  9. real_browser_cli-0.16.3/n8n-nodes-browser-cli/README.md +89 -0
  10. real_browser_cli-0.16.3/n8n-nodes-browser-cli/credentials/BrowserCliApi.credentials.ts +85 -0
  11. real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/BrowserCli.node.ts +382 -0
  12. real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/browserCli.svg +31 -0
  13. real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/protocol.ts +302 -0
  14. real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/request.ts +129 -0
  15. real_browser_cli-0.16.3/n8n-nodes-browser-cli/nodes/BrowserCli/serveClient.ts +147 -0
  16. real_browser_cli-0.16.3/n8n-nodes-browser-cli/package-lock.json +1825 -0
  17. real_browser_cli-0.16.3/n8n-nodes-browser-cli/package.json +45 -0
  18. real_browser_cli-0.16.3/n8n-nodes-browser-cli/scripts/copy-assets.mjs +17 -0
  19. real_browser_cli-0.16.3/n8n-nodes-browser-cli/scripts/run-tests.mjs +27 -0
  20. real_browser_cli-0.16.3/n8n-nodes-browser-cli/test/protocol.test.ts +141 -0
  21. real_browser_cli-0.16.3/n8n-nodes-browser-cli/test/request.test.ts +73 -0
  22. real_browser_cli-0.16.3/n8n-nodes-browser-cli/test-dist/protocol.test.mjs +285 -0
  23. real_browser_cli-0.16.3/n8n-nodes-browser-cli/test-dist/request.test.mjs +147 -0
  24. real_browser_cli-0.16.3/n8n-nodes-browser-cli/tsconfig.json +19 -0
  25. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/package-lock.json +107 -107
  26. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/pyproject.toml +1 -1
  27. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_client.py +4 -8
  28. real_browser_cli-0.16.3/tests/test_markdown_security.py +50 -0
  29. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_new_feature_commands.py +42 -0
  30. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_serve.py +42 -0
  31. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_serve_security.py +45 -0
  32. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/uv.lock +1 -1
  33. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.claude/settings.local.json +0 -0
  34. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitea/workflows/package-extension.yml +0 -0
  35. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitea/workflows/publish.yml +0 -0
  36. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitea/workflows/testing.yml +0 -0
  37. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.gitignore +0 -0
  38. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/.python-version +0 -0
  39. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/CLAUDE.md +0 -0
  40. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/LICENSE +0 -0
  41. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/PRIVACY.md +0 -0
  42. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/__init__.py +0 -0
  43. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/async_sdk.py +0 -0
  44. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/__init__.py +0 -0
  45. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/agent.py +0 -0
  46. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/keys.py +0 -0
  47. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/pq.py +0 -0
  48. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/auth/signing.py +0 -0
  49. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/cli.py +0 -0
  50. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/__init__.py +0 -0
  51. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/core.py +0 -0
  52. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/messages.py +0 -0
  53. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/client/targets.py +0 -0
  54. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/command_security.py +0 -0
  55. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/__init__.py +0 -0
  56. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/auth.py +0 -0
  57. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/clients.py +0 -0
  58. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/completion.py +0 -0
  59. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/doctor.py +0 -0
  60. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/dom.py +0 -0
  61. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/events.py +0 -0
  62. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/extension.py +0 -0
  63. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/extract.py +0 -0
  64. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/groups.py +0 -0
  65. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/install.py +0 -0
  66. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/navigate.py +0 -0
  67. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/page.py +0 -0
  68. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/perf.py +0 -0
  69. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/raw.py +0 -0
  70. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/remote.py +0 -0
  71. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/__init__.py +0 -0
  72. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/common.py +0 -0
  73. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/labels.py +0 -0
  74. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/tabs_tree.py +0 -0
  75. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/rendering/windows_tree.py +0 -0
  76. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/script.py +0 -0
  77. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/search.py +0 -0
  78. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/serve.py +0 -0
  79. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/session.py +0 -0
  80. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/storage.py +0 -0
  81. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/tabs.py +0 -0
  82. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/watch.py +0 -0
  83. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/windows.py +0 -0
  84. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/commands/workspace.py +0 -0
  85. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/compat/__init__.py +0 -0
  86. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/compat/auth.py +0 -0
  87. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/compat/commands.py +0 -0
  88. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/constants.py +0 -0
  89. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/endpoints.py +0 -0
  90. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/errors.py +0 -0
  91. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/framing.py +0 -0
  92. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/local_transport.py +0 -0
  93. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/markdown/__init__.py +0 -0
  94. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/markdown/render.py +0 -0
  95. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/models.py +0 -0
  96. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/__init__.py +0 -0
  97. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/host.py +0 -0
  98. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/local_server.py +0 -0
  99. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/native/protocol.py +0 -0
  100. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/platform.py +0 -0
  101. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/registry.py +0 -0
  102. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/__init__.py +0 -0
  103. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/auth.py +0 -0
  104. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/pool.py +0 -0
  105. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/registry.py +0 -0
  106. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/socket.py +0 -0
  107. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/remote/transport.py +0 -0
  108. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/__init__.py +0 -0
  109. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/base.py +0 -0
  110. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/browser_data.py +0 -0
  111. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/decorators.py +0 -0
  112. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/dom.py +0 -0
  113. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/extension.py +0 -0
  114. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/factories.py +0 -0
  115. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/groups.py +0 -0
  116. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/navigation.py +0 -0
  117. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/perf.py +0 -0
  118. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/routing.py +0 -0
  119. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/session.py +0 -0
  120. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/tabs.py +0 -0
  121. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/windows.py +0 -0
  122. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/sdk/workflow_decorators.py +0 -0
  123. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/search/__init__.py +0 -0
  124. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/search/engines.py +0 -0
  125. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/__init__.py +0 -0
  126. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/auth.py +0 -0
  127. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/challenge.py +0 -0
  128. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/control.py +0 -0
  129. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/logging.py +0 -0
  130. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/proxy.py +0 -0
  131. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/runtime.py +0 -0
  132. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/serve/security.py +0 -0
  133. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/transport/__init__.py +0 -0
  134. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/transport/binary.py +0 -0
  135. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/transport/codecs.py +0 -0
  136. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/browser_cli/version_manager.py +0 -0
  137. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/examples/demo.py +0 -0
  138. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/examples/demo.sh +0 -0
  139. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/content.js +0 -0
  140. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icon.svg +0 -0
  141. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-128.png +0 -0
  142. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-16.png +0 -0
  143. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-32.png +0 -0
  144. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/icons/icon-48.png +0 -0
  145. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/browser-api.ts +0 -0
  146. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/CommandGroup.ts +0 -0
  147. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/CommandRegistry.ts +0 -0
  148. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/JobManager.ts +0 -0
  149. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/classes/NativeConnection.ts +0 -0
  150. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/autosave.ts +0 -0
  151. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/browser-data.ts +0 -0
  152. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/dom.ts +0 -0
  153. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/extension.ts +0 -0
  154. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/groups.ts +0 -0
  155. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/injected.ts +0 -0
  156. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/navigation.ts +0 -0
  157. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/perf.ts +0 -0
  158. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/session-snapshot.ts +0 -0
  159. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/session.ts +0 -0
  160. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/tabs-query.ts +0 -0
  161. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/tabs.ts +0 -0
  162. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/commands/windows.ts +0 -0
  163. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/dispatch.ts +0 -0
  164. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/dom-ops.ts +0 -0
  165. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/extract.ts +0 -0
  166. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/code.ts +0 -0
  167. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/index.ts +0 -0
  168. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/renderer.ts +0 -0
  169. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/root.ts +0 -0
  170. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/markdown/utils.ts +0 -0
  171. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content/page-info.ts +0 -0
  172. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/content-dispatch.ts +0 -0
  173. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/errors.ts +0 -0
  174. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/group-helpers.ts +0 -0
  175. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/index.ts +0 -0
  176. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/scripting.ts +0 -0
  177. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/storage.ts +0 -0
  178. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/tab-groups.ts +0 -0
  179. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/tab-helpers.ts +0 -0
  180. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/core/throttle.ts +0 -0
  181. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/index.ts +0 -0
  182. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/command-args.ts +0 -0
  183. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/index.ts +0 -0
  184. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/jobs.ts +0 -0
  185. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/json.ts +0 -0
  186. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/messages.ts +0 -0
  187. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/session.ts +0 -0
  188. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/tabs.ts +0 -0
  189. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/src/types/webextension.ts +0 -0
  190. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/autosave.test.ts +0 -0
  191. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/browser-api.test.ts +0 -0
  192. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/chrome-mock.ts +0 -0
  193. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/clients.test.ts +0 -0
  194. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/jobs.test.ts +0 -0
  195. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/navigation.test.ts +0 -0
  196. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/extension/test/paging.test.ts +0 -0
  197. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/justfile +0 -0
  198. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/package.json +0 -0
  199. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/gen_extension_key.py +0 -0
  200. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/package_extension.py +0 -0
  201. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/package_verified_crx.sh +0 -0
  202. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/scripts/setup_verified_crx_key.sh +0 -0
  203. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/shell.nix +0 -0
  204. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/conftest.py +0 -0
  205. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_api.py +0 -0
  206. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_auth.py +0 -0
  207. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_cli.py +0 -0
  208. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_commands_cli.py +0 -0
  209. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_dom.py +0 -0
  210. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_extension_error_page_handling.py +0 -0
  211. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_extension_packaging.py +0 -0
  212. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_extract.py +0 -0
  213. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_framing.py +0 -0
  214. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_groups.py +0 -0
  215. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_native_host.py +0 -0
  216. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_nav.py +0 -0
  217. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_page.py +0 -0
  218. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_perf.py +0 -0
  219. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_performance_integration.py +0 -0
  220. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_platform.py +0 -0
  221. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_refactor_boundaries.py +0 -0
  222. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_registry.py +0 -0
  223. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_remote_pool.py +0 -0
  224. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_remote_protocol_matrix.py +0 -0
  225. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_rendering.py +0 -0
  226. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_session.py +0 -0
  227. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_storage.py +0 -0
  228. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_tabs.py +0 -0
  229. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_transport.py +0 -0
  230. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tests/test_windows.py +0 -0
  231. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/tsconfig.json +0 -0
  232. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/webstore-assets/screenshots/screenshot-1.png +0 -0
  233. {real_browser_cli-0.16.2 → real_browser_cli-0.16.3}/webstore-assets/screenshots/screenshot-2.png +0 -0
  234. {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.2
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
- #### Security model
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
- - **`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`, or `--allow-all` (full control, including `dom.eval`/`storage.*`). `--no-auth` is rejected on non-loopback hosts.
436
- - **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`). 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 …` (works locally and over `--remote`); `auth keys` shows each key's policy.
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 beyond loopback, put it behind a TLS-terminating reverse proxy — never send the token over an untrusted network unencrypted.
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
- #### Security model
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
- - **`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`, or `--allow-all` (full control, including `dom.eval`/`storage.*`). `--no-auth` is rejected on non-loopback hosts.
342
- - **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`). 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 …` (works locally and over `--remote`); `auth keys` shows each key's policy.
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 beyond loopback, put it behind a TLS-terminating reverse proxy — never send the token over an untrusted network unencrypted.
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" and not self._require_auth():
52
- return
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
- if path == "/command":
70
- if not self._require_auth():
71
- return
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
- if node.tag not in self._VOID_TAGS:
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"![{_escape_markdown(alt)}]({src})" if alt else f"![]({src})"
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
- markdown = _block_to_markdown(parser.root)
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)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "browser-cli",
4
- "version": "0.16.0",
4
+ "version": "0.16.3",
5
5
  "description": "Control your browser from the terminal or Python SDK",
6
6
  "browser_specific_settings": {
7
7
  "gecko": {
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm whoami *)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ test-dist/
4
+ *.tsbuildinfo