From 15a72bce0cce5ceca33982e1d80940b437214974 Mon Sep 17 00:00:00 2001 From: liutingqiu Date: Fri, 23 Oct 2026 23:34:41 +0800 Subject: [PATCH] feat: add cross-platform bridge support for macOS and Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add scripts/install-edge-bridge.mjs; Windows delegates to the existing PowerShell installer - publish linux-x64, linux-arm64, osx-x64 and osx-arm64 hosts in releases - run native-host smoke and a real installer pass on Ubuntu and macOS CI - expand Chrome/Edge/Chromium discovery paths on every platform - prepare version 0.7.0 and update docs, changelog and social card 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/ci.yml | 62 ++- .github/workflows/release.yml | 133 ++++-- .gitignore | 2 + CHANGELOG.md | 33 +- CONTRIBUTING.md | 6 +- README.md | 58 ++- README.zh-CN.md | 50 ++- SECURITY.md | 3 +- assets/social-card.html | 2 +- assets/social-card.png | Bin 54577 -> 54604 bytes docs/RELEASING.md | 45 +- edge-bridge/README.md | 85 +++- edge-bridge/extension/manifest.json | 2 +- edge-bridge/host/Program.cs | 15 +- mcp/raw-browser-mcp.mjs | 2 +- package-lock.json | 4 +- package.json | 4 +- scripts/check-versions.mjs | 4 +- scripts/install-edge-bridge.mjs | 564 ++++++++++++++++++++++++++ scripts/raw-bridge.mjs | 2 +- scripts/smoke-bridge.mjs | 25 +- src/browser-cli.ts | 6 +- src/browser.ts | 21 + test/installer.test.mjs | 98 +++++ 25 files changed, 1112 insertions(+), 116 deletions(-) create mode 100644 scripts/install-edge-bridge.mjs create mode 100644 test/installer.test.mjs diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e3e5098..61a573a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -36,7 +36,7 @@ body: attributes: label: raw version description: The `raw status` output or `package.json` version. - placeholder: "0.5.1" + placeholder: "0.7.0" validations: required: true - type: input diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 535a7f9..9efb9d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: ci -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] permissions: contents: read @@ -70,6 +70,66 @@ jobs: - run: dotnet build edge-bridge/host/RawEdgeBridge.csproj -c Release --nologo - run: node scripts/smoke-bridge.mjs + bridge-host-posix: + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24.x + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: publish the single-file host + shell: bash + run: | + RID=$(node -p "process.platform === 'darwin' ? (process.arch === 'arm64' ? 'osx-arm64' : 'osx-x64') : 'linux-x64'") + dotnet publish edge-bridge/host/RawEdgeBridge.csproj -c Release -r "$RID" \ + --self-contained true -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:EnableCompressionInSingleFile=true -o host-out --nologo + chmod +x host-out/raw-edge-bridge + test -x host-out/raw-edge-bridge + - name: smoke the native host + shell: bash + env: + RAW_BRIDGE_HOST: ${{ github.workspace }}/host-out/raw-edge-bridge + run: node scripts/smoke-bridge.mjs + - name: install into a disposable home + shell: bash + run: | + node scripts/install-edge-bridge.mjs --browser both \ + --host-path "$PWD/host-out/raw-edge-bridge" --home "$RUNNER_TEMP/raw-home" + node - <<'NODE' + const fs = require("node:fs"); + const path = require("node:path"); + const home = path.join(process.env.RUNNER_TEMP, "raw-home"); + const raw = path.join(home, ".raw"); + for (const file of ["edge-bridge.json", "chrome-bridge.json"]) { + if (!fs.existsSync(path.join(raw, file))) throw new Error(`missing ${file}`); + } + const manifestRoot = path.join(raw, "edge-bridge"); + for (const file of ["com.raw.edge_bridge.edge.json", "com.raw.edge_bridge.chrome.json"]) { + const manifest = JSON.parse(fs.readFileSync(path.join(manifestRoot, file), "utf8")); + if (manifest.type !== "stdio" || !manifest.allowed_origins[0].startsWith("chrome-extension://")) { + throw new Error(`bad manifest ${file}`); + } + } + const browserDir = process.platform === "darwin" + ? path.join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts") + : path.join(home, ".config", "google-chrome", "NativeMessagingHosts"); + if (!fs.existsSync(path.join(browserDir, "com.raw.edge_bridge.json"))) { + throw new Error("chrome native-messaging manifest was not registered"); + } + const host = path.join(raw, "edge-bridge", "current", "raw-edge-bridge"); + if (!(fs.statSync(host).mode & 0o111)) throw new Error("installed host is not executable"); + NODE + dependency-audit: timeout-minutes: 5 runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05e3e94..26d7cc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,8 +12,8 @@ concurrency: cancel-in-progress: false jobs: - release: - runs-on: windows-latest + verify: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,54 +22,133 @@ jobs: node-version: 24.x cache: npm - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - run: npm ci - run: node scripts/check-versions.mjs --tag ${{ github.ref_name }} - run: npm run typecheck - run: npm test - run: npm run build + - run: node scripts/smoke-browser.mjs + + host-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x - - name: publish the self-contained bridge host + - name: publish the self-contained Windows host shell: pwsh run: | dotnet publish edge-bridge/host/RawEdgeBridge.csproj -c Release -r win-x64 ` --self-contained true -p:PublishSingleFile=true ` -p:IncludeNativeLibrariesForSelfExtract=true ` -p:EnableCompressionInSingleFile=true -o host-out --nologo - - - name: pack the host and checksums - shell: pwsh - run: | New-Item -ItemType Directory -Path host-pack -Force | Out-Null Copy-Item host-out/raw-edge-bridge.exe host-pack/raw-edge-bridge.exe -Force Compress-Archive -Path host-pack/raw-edge-bridge.exe -DestinationPath raw-bridge-host-win-x64.zip -Force - $hash = (Get-FileHash raw-bridge-host-win-x64.zip -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash raw-bridge-host-win-x64.zip" | Set-Content SHA256SUMS.txt -Encoding ascii - - name: pack the npm tarball - run: npm pack + - uses: actions/upload-artifact@v4 + with: + name: host-win-x64 + path: raw-bridge-host-win-x64.zip - - name: add a stable tarball name - shell: pwsh + host-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: publish the self-contained Linux hosts + shell: bash + run: | + for rid in linux-x64 linux-arm64; do + dotnet publish edge-bridge/host/RawEdgeBridge.csproj -c Release -r "$rid" \ + --self-contained true -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:EnableCompressionInSingleFile=true -o "host-out/$rid" --nologo + mkdir -p "host-pack/$rid" + cp "host-out/$rid/raw-edge-bridge" "host-pack/$rid/" + chmod +x "host-pack/$rid/raw-edge-bridge" + tar -czf "raw-bridge-host-$rid.tar.gz" -C "host-pack/$rid" raw-edge-bridge + test -s "raw-bridge-host-$rid.tar.gz" + done + + - uses: actions/upload-artifact@v4 + with: + name: host-linux + path: raw-bridge-host-linux-*.tar.gz + + host-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: publish the self-contained macOS hosts + shell: bash run: | - $tarball = (Get-ChildItem *.tgz | Select-Object -First 1).Name - Copy-Item $tarball raw-browser.tgz -Force + for rid in osx-x64 osx-arm64; do + dotnet publish edge-bridge/host/RawEdgeBridge.csproj -c Release -r "$rid" \ + --self-contained true -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + -p:EnableCompressionInSingleFile=true -o "host-out/$rid" --nologo + mkdir -p "host-pack/$rid" + cp "host-out/$rid/raw-edge-bridge" "host-pack/$rid/" + chmod +x "host-pack/$rid/raw-edge-bridge" + tar -czf "raw-bridge-host-$rid.tar.gz" -C "host-pack/$rid" raw-edge-bridge + test -s "raw-bridge-host-$rid.tar.gz" + done + + - uses: actions/upload-artifact@v4 + with: + name: host-macos + path: raw-bridge-host-osx-*.tar.gz + + release: + needs: [verify, host-windows, host-linux, host-macos] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: npm + + - run: npm ci + + - uses: actions/download-artifact@v4 + with: + path: release-assets + merge-multiple: true + + - name: checksums for every host archive + run: | + cd release-assets + sha256sum raw-bridge-host-* > SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: pack the npm tarball + run: | + npm pack + tarball=$(ls raw-browser-*.tgz | head -1) + cp "$tarball" raw-browser.tgz - name: create the release and upload assets - shell: pwsh env: GH_TOKEN: ${{ github.token }} run: | - $tag = "${{ github.ref_name }}" - gh release view $tag 2>$null - if ($LASTEXITCODE -ne 0) { - gh release create $tag --title "raw-browser $tag" --generate-notes - } - $tarball = (Get-ChildItem *.tgz | Select-Object -First 1).Name - gh release upload $tag raw-bridge-host-win-x64.zip SHA256SUMS.txt $tarball raw-browser.tgz --clobber + tag="${{ github.ref_name }}" + gh release view "$tag" >/dev/null 2>&1 || gh release create "$tag" --title "raw-browser $tag" --generate-notes + gh release upload "$tag" release-assets/*.zip release-assets/*.tar.gz release-assets/SHA256SUMS.txt raw-browser.tgz --clobber npm: needs: release diff --git a/.gitignore b/.gitignore index 49be3e0..0df62d4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ demo/.frames/ host-out/ host-pack/ raw-bridge-host-win-x64.zip +raw-bridge-host-*.zip +raw-bridge-host-*.tar.gz SHA256SUMS.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index de8259b..c0a74b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to `raw` are documented here. The format follows Keep a Changelog and versions follow Semantic Versioning. +## [0.7.0] - 2026-10-23 + +### Added + +- The extension bridge now installs on macOS and Linux. A cross-platform + `scripts/install-edge-bridge.mjs` handles the native-messaging host, + `~/.raw/edge-bridge.json` / `~/.raw/chrome-bridge.json`, and the browser + manifests under `~/Library/Application Support` (macOS) or `~/.config` + (Linux). On Windows it delegates to the existing PowerShell installer, so + one command works everywhere. +- Release builds now publish self-contained hosts for `linux-x64`, + `linux-arm64`, `osx-x64` and `osx-arm64` alongside `win-x64`, plus a single + `SHA256SUMS.txt` covering every archive. +- CI runs the native-host smoke test on Ubuntu, macOS and Windows, plus a real + installer pass on Ubuntu and macOS. + +### Changed + +- Browser discovery also finds Chrome Beta/Canary, Chromium and Edge + Beta/Dev/Canary, plus Linux packages under `/opt` and snap Chromium, so + macOS/Linux users are less likely to need `RAW_BROWSER_PATH`. +- README, README.zh-CN and `edge-bridge/README.md` document the cross-platform + install, the per-OS native-messaging directories, and the WSL/snap caveats. + +### Fixed + +- The native host treats process enumeration as best-effort, so a restricted + `Process.GetProcessesByName` call on macOS or Linux cannot prevent startup. + ## [0.6.2] - 2026-10-23 ### Added @@ -203,7 +232,9 @@ follow Semantic Versioning. - Strict TypeScript checking, a locked development toolchain, and full CLI protocol, setup, and permission smoke tests now gate publication. - Browser smoke testing uses a disposable profile and covers forms, iframes, downloads, screenshots, isolated spaces, cleanup, and CPU timeouts. -[Unreleased]: https://github.com/liutingqiu/raw-browser/compare/v0.6.1...HEAD +[Unreleased]: https://github.com/liutingqiu/raw-browser/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/liutingqiu/raw-browser/releases/tag/v0.7.0 +[0.6.2]: https://github.com/liutingqiu/raw-browser/releases/tag/v0.6.2 [0.6.1]: https://github.com/liutingqiu/raw-browser/releases/tag/v0.6.1 [0.6.0]: https://github.com/liutingqiu/raw-browser/releases/tag/v0.6.0 [0.5.1]: https://github.com/liutingqiu/raw-browser/releases/tag/v0.5.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aea8209..a6f8036 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,9 +20,9 @@ Run `npm run smoke:browser` when changing browser automation. It opens the dedicated local browser profile and verifies same-origin iframe interaction, downloads, screenshots and CPU-timeout termination. -Run `npm run smoke:bridge` on Windows (with the .NET 9 SDK) when changing the -native host or the extension. It builds the host and exercises the bridge -protocol. +Run `npm run smoke:bridge` on Windows, macOS or Linux (with the .NET 9 SDK) +when changing the native host or the extension. It builds the host and +exercises the bridge protocol. Add focused `node:test` coverage for protocol serialization, context boundaries, permissions, and filesystem behavior. Keep generated files out of commits; diff --git a/README.md b/README.md index 03df82f..d2b8bd3 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ raw status Pinned or offline installs can use the `raw-browser.tgz` tarball attached to each [release](https://github.com/liutingqiu/raw-browser/releases/latest). The extension bridge installer ships in both the npm package and a clone; a global -install runs it with `$(npm root -g)\raw-browser\scripts\install-edge-bridge.ps1`. +install runs it with +`node "$(npm root -g)/raw-browser/scripts/install-edge-bridge.mjs"`. ## Why raw-browser @@ -145,23 +146,43 @@ returned only when a script explicitly calls a cookie API. ## Drive your signed-in browser -The bridge installer is currently Windows PowerShell. Direct CDP mode works on -Windows, macOS and Linux. +The bridge installer supports Windows, macOS and Linux. Direct CDP mode also +works on all three. + +```bash +# global npm install (macOS / Linux) +node "$(npm root -g)/raw-browser/scripts/install-edge-bridge.mjs" --browser both + +# source checkout (any platform) +node scripts/install-edge-bridge.mjs --browser both +``` + +On Windows the same Node command delegates to the PowerShell installer. You can +also run that script directly: ```powershell -# global npm install powershell -ExecutionPolicy Bypass -File "$(npm root -g)\raw-browser\scripts\install-edge-bridge.ps1" -Browser both - -# source checkout -powershell -ExecutionPolicy Bypass -File scripts/install-edge-bridge.ps1 -Browser both ``` The installer publishes a self-contained native host, writes independent tokens to `~/.raw/edge-bridge.json` and `~/.raw/chrome-bridge.json`, and registers the -selected native-messaging hosts. If the .NET 9 SDK is missing, it downloads the -matching prebuilt host from [Releases](https://github.com/liutingqiu/raw-browser/releases) -and verifies `SHA256SUMS.txt` when available. Use `-HostPath ` to install a -host you built yourself, or `-BuildFromSource` to force a local build. +selected native-messaging hosts: HKCU on Windows, +`NativeMessagingHosts` under `~/Library/Application Support` on macOS, and under +`~/.config` on Linux. If the .NET 9 SDK is missing, it downloads the matching +prebuilt host from [Releases](https://github.com/liutingqiu/raw-browser/releases) +and verifies `SHA256SUMS.txt` when available. Use `--host-path` to install a +host you built yourself, or `--build-from-source` to force a local build. + +The host always lives at `~/.raw/edge-bridge/current/` so the native-messaging +manifest does not move between upgrades; the installer replaces the executable +in place and keeps the previous pair for rollback. + +The browser and the host must be in the same operating system. A Windows Chrome +cannot use a host installed in WSL: use the Windows installer for the Windows +browser, or install a Linux Chrome/Edge inside WSL (with WSLg) and run this +installer there. Snap- and Flatpak-confined browsers do not read the +user-level native-messaging directory the installer writes; use a deb/rpm or +app-bundle install for the bridge. Then load the unpacked extension once per browser: @@ -180,8 +201,8 @@ or `{ "mode": "chrome-extension" }`. `raw status` prints the selected bridge, port and extension version. If the bridge does not connect after an upgrade, restart the browser once: it caches the native-messaging host path for the life of the process. Upgrading from an older installer? Restart the browser, then run -the installer once with `-CleanLegacyHosts` to remove the old `host-*` -directories. +the installer once with `--clean-legacy-hosts` (`-CleanLegacyHosts` in +PowerShell) to remove the old `host-*` directories. ## CLI @@ -265,19 +286,20 @@ See [SECURITY.md](SECURITY.md). ```bash npm run typecheck # strict TypeScript -npm test # 51 unit tests +npm test # 55 unit tests npm run build npm run smoke:browser npm run check # all of the above plus the browser smoke test ``` -CI runs the suite on Ubuntu, Windows and macOS across Node 22, 24 and 26, plus a -Windows bridge-host smoke test and a dependency audit. +CI runs the suite on Ubuntu, Windows and macOS across Node 22, 24 and 26, plus +native-host smoke tests on Windows, macOS and Linux and a dependency audit. ## Known limitations -1. **The bridge installer is Windows-only today.** Direct CDP mode is - cross-platform. +1. **Native messaging needs a desktop browser in the same OS.** The installer + covers Chrome and Edge stable on Windows, macOS and Linux; snap/Flatpak + confined browsers and WSL-to-Windows browser setups are not registered. 2. **One debugger per tab.** Chromium allows a single CDP debugger client per tab; concurrent clients detach each other. 3. **No `--space` in bridge mode.** Extensions cannot create an equivalent of diff --git a/README.zh-CN.md b/README.zh-CN.md index bf4b049..c668ca4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -45,7 +45,7 @@ raw status 需要固定版本或离线安装时,可以用每个 [Release](https://github.com/liutingqiu/raw-browser/releases/latest) 附带的 `raw-browser.tgz`。扩展桥安装脚本在 npm 包和克隆仓库里都有;全局安装时用 -`$(npm root -g)\raw-browser\scripts\install-edge-bridge.ps1` 运行。 +`node "$(npm root -g)/raw-browser/scripts/install-edge-bridge.mjs"` 运行。 ## 为什么是 raw-browser @@ -137,22 +137,39 @@ Cookie API 时才会返回 Cookie 值。 ## 驱动已登录的浏览器 -桥接安装脚本目前是 Windows PowerShell;直连 CDP 模式支持 Windows / macOS / Linux。 +桥接安装器支持 Windows / macOS / Linux;直连 CDP 模式同样三平台可用。 + +```bash +# 全局 npm 安装(macOS / Linux) +node "$(npm root -g)/raw-browser/scripts/install-edge-bridge.mjs" --browser both + +# 源码克隆(任意平台) +node scripts/install-edge-bridge.mjs --browser both +``` + +Windows 上同一条 Node 命令会转交给 PowerShell 安装器;也可以直接运行: ```powershell -# 全局 npm 安装 powershell -ExecutionPolicy Bypass -File "$(npm root -g)\raw-browser\scripts\install-edge-bridge.ps1" -Browser both - -# 源码克隆 -powershell -ExecutionPolicy Bypass -File scripts/install-edge-bridge.ps1 -Browser both ``` -安装脚本会发布自包含的原生宿主,生成独立的 +安装器会发布自包含的原生宿主,生成独立的 `~/.raw/edge-bridge.json` 与 `~/.raw/chrome-bridge.json`,并注册所选浏览器的 -Native Messaging 宿主。没有 .NET 9 SDK 时,它会从 +Native Messaging 宿主:Windows 写 HKCU,macOS 写到 +`~/Library/Application Support` 下的 `NativeMessagingHosts`,Linux 写到 +`~/.config` 下。没有 .NET 9 SDK 时,它会从 [Releases](https://github.com/liutingqiu/raw-browser/releases) 下载匹配版本的 -预编译宿主,并在有 `SHA256SUMS.txt` 时校验。也可以用 `-HostPath ` -安装自己编译的宿主,或用 `-BuildFromSource` 强制本地编译。 +预编译宿主,并在有 `SHA256SUMS.txt` 时校验。也可以用 `--host-path` +安装自己编译的宿主,或用 `--build-from-source` 强制本地编译。 + +宿主始终安装在 `~/.raw/edge-bridge/current/`,升级时原地替换,上一版留在 +`previous/` 便于回滚,Native Messaging 清单不会因升级改路径。 + +浏览器和宿主必须在同一个操作系统里。Windows 的 Chrome 不能使用装在 WSL +里的宿主:要驱动 Windows 浏览器就用 Windows 安装器;要在 WSL 里用桥接,就 +在 WSL 内安装 Linux 版 Chrome / Edge(需要 WSLg),再运行这个安装器。Snap 和 +Flatpak 沙箱里的浏览器不会读取安装器写入的用户级 Native Messaging 目录, +桥接请使用 deb/rpm 或 app bundle 安装的浏览器。 然后在每个要驱动的浏览器里加载一次解压扩展: @@ -170,7 +187,8 @@ Native Messaging 宿主。没有 .NET 9 SDK 时,它会从 或 `{ "mode": "chrome-extension" }`。`raw status` 会显示当前桥、端口和扩展版本。 如果升级后桥接没有连上,重启一次浏览器:它会在进程生命周期内缓存 Native Messaging 宿主路径。从旧版安装脚本升级的用户,重启浏览器后再运行一次 -安装脚本并加上 `-CleanLegacyHosts`,即可删除旧的 `host-*` 目录。 +安装脚本并加上 `--clean-legacy-hosts`(PowerShell 里是 `-CleanLegacyHosts`), +即可删除旧的 `host-*` 目录。 ## CLI @@ -251,18 +269,20 @@ Native Messaging 宿主路径。从旧版安装脚本升级的用户,重启浏 ```bash npm run typecheck # 严格 TypeScript 检查 -npm test # 51 个单元测试 +npm test # 55 个单元测试 npm run build npm run smoke:browser npm run check # 以上全部 + 浏览器冒烟测试 ``` -CI 在 Ubuntu / Windows / macOS × Node 22/24/26 上跑完整套件,另有 Windows -桥接宿主冒烟测试和依赖审计。 +CI 在 Ubuntu / Windows / macOS × Node 22/24/26 上跑完整套件,另有 +Windows / macOS / Linux 的桥接宿主冒烟测试和依赖审计。 ## 已知限制 -1. **桥接安装脚本目前仅限 Windows。** 直连 CDP 模式跨平台。 +1. **Native Messaging 需要同一操作系统里的桌面浏览器。** 安装器覆盖 + Windows / macOS / Linux 上的 Chrome 与 Edge 稳定版;Snap/Flatpak 沙箱浏览器 + 和“WSL 宿主 + Windows 浏览器”的组合不会自动注册。 2. **单标签页单调试器。** Chromium 同一标签页只允许一个 CDP 调试客户端; 并发挂载会互相 detach。 3. **桥接模式不支持 `--space`。** 扩展无法创建 diff --git a/SECURITY.md b/SECURITY.md index 0c4e497..e0ccc1f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,8 @@ profile. The calling agent's prompt and permission mode are the authority for wh The bridge is defended at the transport boundary: listeners bind only to `127.0.0.1`, every HTTP and WebSocket request requires a per-browser random token, native messaging is restricted to the pinned extension id, and a host only attaches to a debug port it owns. Release host binaries are published -with `SHA256SUMS.txt`; `scripts/install-edge-bridge.ps1` verifies the checksum when it downloads one. +with `SHA256SUMS.txt`; `scripts/install-edge-bridge.mjs` and `scripts/install-edge-bridge.ps1` +verify the checksum when they download one. Cookies, tokens and page contents are secrets; do not log or forward them to external services. Configuration, downloads, bridge tokens and the dedicated browser profile are stored locally under diff --git a/assets/social-card.html b/assets/social-card.html index 405e9bc..e71451f 100644 --- a/assets/social-card.html +++ b/assets/social-card.html @@ -200,7 +200,7 @@

Your browser.

MIT Node 22+ - v0.6.1 + v0.7.0 MCP 16 tools
diff --git a/assets/social-card.png b/assets/social-card.png index a4a68525741f16e6b989d765b057baeb9e667b5a..dafe1da1d9fab34b86b41b60a3657f80cbac4b84 100644 GIT binary patch literal 54604 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