coze-bridge 0.2.1-alpha.2 → 0.2.1-alpha.3

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.
package/README.md CHANGED
@@ -1,98 +1,218 @@
1
1
  # coze-bridge
2
2
 
3
- 本机后台 daemon, 把云端 Agent Service 跟本地 AI agent (Claude Code / Codex / OpenClaw) 通过扩展 ACP 协议桥接起来。
3
+ `coze-bridge` is the local bridge between the Coze cloud Agent Service and the AI
4
+ agent runtimes on your machine, including Claude Code, Codex CLI, and OpenClaw.
4
5
 
5
- ## 前置依赖
6
+ This npm package is a thin wrapper. It does not contain the bridge core. On
7
+ first run it installs and runs the native `coze-bridge` Go binary that matches
8
+ this package version, then passes every argument through unchanged.
6
9
 
7
- - **Node ≥ 20**
8
- - 本机至少装好 `claude` / `codex` / `openclaw` 中的一个
10
+ ## Install
9
11
 
10
- ## 用法
12
+ Use `npx` when you want the Coze UI command to work without a separate install
13
+ step:
11
14
 
12
- ```bash
13
- # 启动 / 复用 daemon, 投递 pair (云端 UI 复制粘贴一行, 唯一保留无 verb 的入口)
15
+ ```sh
14
16
  npx -y coze-bridge@latest --pat-token=<sat_xxx> --pair-code=<xxx>
17
+ ```
15
18
 
16
- # daemon 状态
17
- coze-bridge status
19
+ Use a global npm install when you want `coze-bridge` on PATH:
18
20
 
19
- # 停 daemon
20
- coze-bridge stop
21
+ ```sh
22
+ npm install -g coze-bridge@latest
23
+ coze-bridge --pat-token=<sat_xxx> --pair-code=<xxx>
24
+ ```
21
25
 
22
- # 重新探测本地 agent 路径 / 版本
23
- coze-bridge refresh
26
+ Use the native installers when Node/npm is not available:
24
27
 
25
- # 清理: 停 daemon + 卸自启 + 删 ~/.coze/bridge (保留 ~/.coze/agents 工作区)
26
- coze-bridge purge
28
+ ```sh
29
+ # macOS/Linux
30
+ curl -fsSL https://coze-bridge.tos-cn-beijing.volces.com/install/coze_bridge_install.sh | sh
27
31
 
28
- # / 卸 OS supervisor 自启 (login 时自动拉起 daemon)
29
- coze-bridge service install
30
- coze-bridge service uninstall
32
+ # Windows PowerShell
33
+ irm https://coze-bridge.tos-cn-beijing.volces.com/install/coze_bridge_install.ps1 | iex
31
34
  ```
32
35
 
33
- > flag (`--status` / `--stop` / `--refresh` / `--install-service` / `--uninstall`) 仍能用, 但 stderr 会打 deprecation warn, 后续 major bump 会删。建议切到新子命令形态。
34
- >
35
- > 注意 `--uninstall` 在 0.1.67 起映射到新 `purge`: 删除范围从 `~/.coze` 收窄到 `~/.coze/bridge`, 不再误删 `~/.coze/agents/<id>/workspace` 里用户写过的 CLAUDE.md / SOUL.md。完全重置仍可手动 `rm -rf ~/.coze`。
36
+ Install a specific native version or channel with the native installers:
36
37
 
37
- > **保活能力按平台不同**:
38
- > - **macOS / Linux**: 配对成功即由 launchd / systemd 接管 daemon —— 开机自启 + **进程崩溃/被杀也会自动拉起**(KeepAlive / `Restart=always`)。
39
- > - **Windows**: Task Scheduler 仅 **登录时自启**(ONLOGON), **不含崩溃自动重启**(daemon 崩了需重新登录或重新跑一次配对命令恢复)。崩溃自启是已知待办(需 PowerShell 改 Task XML 加 `RestartOnFailure`)。
38
+ ```sh
39
+ curl -fsSL https://coze-bridge.tos-cn-beijing.volces.com/install/coze_bridge_install.sh \
40
+ | env COZE_BRIDGE_VERSION=v0.3.0 sh
40
41
 
41
- ### 配对参数
42
+ curl -fsSL https://coze-bridge.tos-cn-beijing.volces.com/install/coze_bridge_install.sh \
43
+ | env COZE_BRIDGE_CHANNEL=alpha sh
44
+ ```
42
45
 
43
- | Flag | 说明 |
44
- |---|---|
45
- | `--pat-token <sat_xxx>` | coze PAT token, 从 coze 平台拿 |
46
- | `--pair-code <xxx>` | 配对码, 从云端 UI 拿 |
46
+ `COZE_BRIDGE_VERSION` accepts `0.3.0` or `v0.3.0` and takes precedence over
47
+ `COZE_BRIDGE_CHANNEL`. Supported channels are `stable`, `alpha`, `beta`, `rc`,
48
+ and `next`.
47
49
 
48
- 支持 `--name VALUE` 与 `--name=VALUE` 两种写法。
50
+ ## Managed Layout
49
51
 
50
- ### 自动 connect 某个 agent(可选)
52
+ Managed installs created by `npx`, global npm, curl, or PowerShell use one
53
+ native runtime layout:
51
54
 
52
- | Flag | 说明 |
53
- |---|---|
54
- | `--agent-id <id>` | pair 成功后自动 connect 该 agent: 本地已有走 reconnect/revive, 没有 (需云端协议升级) 拉详情新建 |
55
+ ```text
56
+ ~/.coze/bridge/
57
+ bin/coze-bridge
58
+ releases/<version>/coze-bridge
59
+ install-state.json
60
+ pat-token
61
+ bridge.token
62
+ logs/install-update.log
55
63
 
56
- ```bash
57
- npx -y coze-bridge --pat-token=sat_xxx --pair-code=xxx --agent-id=<your-agent-id>
64
+ ~/.local/bin/coze-bridge -> ~/.coze/bridge/bin/coze-bridge
58
65
  ```
59
66
 
60
- ### 环境路由(可选, 联调时用)
67
+ On Windows the stable launcher is
68
+ `%USERPROFILE%\.coze\bridge\bin\coze-bridge.cmd`, and the installer adds
69
+ `%USERPROFILE%\.coze\bridge\bin` to the User PATH.
61
70
 
62
- | Flag | 说明 |
63
- |---|---|
64
- | `--env <value>` | `ppe_*` `x-use-ppe: 1` + `x-tt-env`; `boe_*` 仅 `x-tt-env` |
65
- | `--handshake-url <url>` | 覆盖 HTTP base, 默认 `https://www.coze.cn` |
66
- | `--frontier-url <url>` | 覆盖 Frontier WS URL, 默认 `wss://frontier.coze.cn` |
71
+ The stable launcher forwards to the versioned binary in `releases/<version>/`.
72
+ Its target is the `nativeBin` recorded in `install-state.json` at install or
73
+ update time, written into the launcher script itself; the installer and updater
74
+ rewrite this one file on every version switch and rollback. PATH entries should
75
+ point to the stable launcher, not directly to a versioned binary.
67
76
 
68
- ```bash
69
- npx -y coze-bridge --pat-token=sat_xxx --pair-code=xxx --env=ppe_xxx
70
- ```
77
+ ## What The Wrapper Does
71
78
 
72
- ## 行为说明
79
+ 1. Detects the current OS/arch.
80
+ 2. Reuses an existing managed native binary when it already satisfies this npm
81
+ package version, or when the native updater has moved the same channel to a
82
+ newer version. It never downgrades a managed binary.
83
+ 3. Otherwise reads the version manifest for this npm package version, downloads
84
+ and verifies the platform artifact with size and sha256 checks, installs it
85
+ under `~/.coze/bridge/releases/<version>/`, and writes the stable launcher,
86
+ PATH entry, and `install-state.json`.
87
+ 4. Executes the native binary with the original arguments.
73
88
 
74
- - **daemon 启动自动复活 agents**: `~/.coze/agents/` 里所有未被云端主动 disconnect 的 agent, daemon 启动后会自动 spawn 子进程 + 加入路由, 不需要你一个个 reconnect
75
- - **device 10s 批量心跳**: pair 成功后每 10s 上行 1 `_agent/health`, 装该 device 下所有 eligible agent (`agents:[{agentId,status},...]`), 新 agent 上线毫秒级出首帧
76
- - **deviceId 不持久化**: 每次 pair 由云端 handshake 接口分配, 切环境 / 重启 daemon 都用新值
77
- - **cloud agent 跟本机 framework 解耦**: `_agent/create` 带 `_meta.cozeDeployType=cloud` Bridge 直接 ack spawn agent 在云端跑, 本机没装对应 framework 也行
89
+ The wrapper takes the cross-language `~/.coze/bridge/install.lock` directory lock
90
+ around writes. It never runs the updater state machine, stops or starts the
91
+ daemon, restores `connect`, or rolls back. Those actions live in the native Go
92
+ runtime. If the wrapper finds a pending update, it only delegates to an
93
+ executable native entry.
78
94
 
79
- ## 工作目录
95
+ ## Update
80
96
 
97
+ Run updates from a managed install:
98
+
99
+ ```sh
100
+ coze-bridge update
81
101
  ```
82
- ~/.coze/
83
- ├── bridge/ # daemon 文件
84
- │ ├── lib/index.js # 固化的 dist (npx 首次执行复制过来)
85
- │ ├── bridge.pid # PID
86
- │ ├── bridge.port # IPC HTTP 端口
87
- │ ├── bridge.token # IPC token (mode 0600)
88
- │ ├── bridge.log
89
- │ └── config.json # frameworks_cache + libVersion (不存 deviceId, 每次 pair 拿)
90
- └── agents/<agentId>/ # 每个 agent 一个目录, daemon 重启自动复活
91
- ├── config.json # mode 0600 (含 cozeIdentity / model / modelToken / sessions / ...)
92
- ├── workspace/ # 子进程 cwd, CLAUDE.md / AGENTS.md / SOUL.md 在这
93
- └── logs/
102
+
103
+ The native updater reads the installed channel from `install-state.json`, checks
104
+ the channel manifest, downloads and verifies the target artifact, switches the
105
+ stable launcher to the new version, and restores `connect` when the daemon was
106
+ running before the update. If `~/.coze/bridge/pat-token` is missing, the binary
107
+ update can still succeed, but you must run pair/connect again.
108
+
109
+ Manual binaries that were downloaded and placed on PATH by hand are not managed
110
+ installs. `coze-bridge update` intentionally fails for those binaries with
111
+ `COZE_BRIDGE_UPDATE_ERROR` and points you back to npm, npx, curl, or PowerShell.
112
+
113
+ ## Manual Download
114
+
115
+ Manual download is a fallback for running one binary directly. It does not
116
+ create `install-state.json`, the stable launcher, or PATH entries, and it does
117
+ not enable `coze-bridge update`.
118
+
119
+ ```sh
120
+ curl -fsSLO https://coze-bridge.tos-cn-beijing.volces.com/releases/v0.3.0/manifest.json
121
+ curl -fsSLO https://coze-bridge.tos-cn-beijing.volces.com/releases/v0.3.0/coze-bridge_v0.3.0_linux_amd64.tar.gz
122
+ sha256sum coze-bridge_v0.3.0_linux_amd64.tar.gz
123
+ tar -xzf coze-bridge_v0.3.0_linux_amd64.tar.gz
124
+ ./coze-bridge version
94
125
  ```
95
126
 
96
- ## License
127
+ Compare the sha256 with the matching artifact entry in `manifest.json` before
128
+ running the binary. Windows artifacts are zip files containing
129
+ `coze-bridge.exe`.
130
+
131
+ ## PATH Notes
132
+
133
+ On macOS/Linux the installer creates `~/.local/bin/coze-bridge` as a symlink to
134
+ the stable launcher when possible, or as a tiny launcher script when symlinks are
135
+ not available. Add `~/.local/bin` to PATH if your shell cannot find
136
+ `coze-bridge`.
137
+
138
+ On Windows, open a new shell after the PowerShell installer changes the User
139
+ PATH. You can always run the managed launcher directly:
140
+
141
+ ```powershell
142
+ & "$env:USERPROFILE\.coze\bridge\bin\coze-bridge.cmd" version
143
+ ```
97
144
 
98
- MIT
145
+ If an older global npm or TypeScript install appears earlier on PATH, install
146
+ can still succeed but will print a shadowing warning. Update PATH ordering so
147
+ the managed launcher is found first, or run the managed launcher by full path.
148
+
149
+ ## Service Install
150
+
151
+ Managed installs make `service install` point launchd, systemd, or Task
152
+ Scheduler at the stable launcher. After `coze-bridge update`, the service keeps
153
+ the same command and the launcher forwards to the updated version.
154
+
155
+ When you run `service install` from a manual binary, the service is bound to that
156
+ exact binary path. It will not move automatically with `coze-bridge update`;
157
+ install through npm, npx, curl, or PowerShell first if you want managed update
158
+ behavior.
159
+
160
+ ## Migration From The Old TypeScript Package
161
+
162
+ The old TypeScript package used the same package name and stored local state
163
+ under `~/.coze/bridge`. The Go package keeps that state directory and preserves
164
+ agent workspaces under `~/.coze/agents/<agentId>/workspace`.
165
+
166
+ Common migration paths:
167
+
168
+ - Running the Coze UI command again with `npx -y coze-bridge@latest ...` lands
169
+ on this wrapper, installs the native runtime, and then starts native pair or
170
+ connect.
171
+ - Running `npm install -g coze-bridge@latest` replaces the old global npm entry
172
+ with this wrapper. The first `coze-bridge ...` command installs and runs the
173
+ native binary.
174
+ - If an old TypeScript update command runs
175
+ `npx -y --ignore-existing coze-bridge@latest connect`, that first hop also
176
+ lands on this wrapper and enters the native runtime.
177
+
178
+ The native runtime stops or reuses any old local daemon before starting the Go
179
+ daemon. It reads compatible configuration fields but does not treat unknown old
180
+ `config.json` fields as new behavior.
181
+
182
+ ## Environment
183
+
184
+ - `COZE_BRIDGE_NATIVE_BIN`: absolute path to a native binary to run directly.
185
+ Invalid paths are reported and skipped.
186
+ - `COZE_BRIDGE_DOWNLOAD_BASE`: override the release download root. The value
187
+ must be an HTTPS URL.
188
+ - `COZE_BRIDGE_VERSION`: native installer-only override for an exact version.
189
+ - `COZE_BRIDGE_CHANNEL`: native installer-only channel override when no exact
190
+ version is set.
191
+
192
+ ## Troubleshooting
193
+
194
+ - `COZE_BRIDGE_INSTALL_ERROR stage=manifest`: the manifest is missing, invalid,
195
+ for an unsupported schema, or not consistent with the requested version or
196
+ channel.
197
+ - `COZE_BRIDGE_INSTALL_ERROR stage=checksum`: the downloaded manifest or
198
+ artifact did not match the expected sha256 or size. The current install is
199
+ left untouched.
200
+ - `COZE_BRIDGE_INSTALL_ERROR stage=lock`: another installer or updater owns
201
+ `~/.coze/bridge/install.lock`. Retry after that process exits.
202
+ - `COZE_BRIDGE_UPDATE_ERROR stage=preflight`: the current binary is not a
203
+ managed install, or `install-state.json`/stable launcher/native binary is
204
+ missing or inconsistent.
205
+ - Pending update errors are recovered by the native runtime. Re-run
206
+ `coze-bridge update` or `coze-bridge connect` from a managed launcher.
207
+
208
+ Install and update logs are written to
209
+ `~/.coze/bridge/logs/install-update.log`. They include stages, versions,
210
+ manifest URLs, artifact URLs, checksums, and native binary paths. They must not
211
+ contain PAT tokens, bridge tokens, or pair codes.
212
+
213
+ ## Package Boundaries
214
+
215
+ - No `preinstall`, `install`, or `postinstall` lifecycle scripts: `npm install`
216
+ never downloads a native artifact or writes `~/.coze/bridge`.
217
+ - The npm package ships only `bin/`, `lib/`, and this README. It does not ship
218
+ native artifacts or the legacy TypeScript runtime.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // coze-bridge npm wrapper entry point. It installs (when needed) and execs the
5
+ // native coze-bridge binary, passing all arguments through unchanged. The
6
+ // wrapper never carries Go core logic and never runs the updater state machine.
7
+ const { run } = require('../lib/main');
8
+
9
+ run(process.argv.slice(2))
10
+ .then((code) => {
11
+ process.exitCode = typeof code === 'number' ? code : 0;
12
+ })
13
+ .catch((err) => {
14
+ process.stderr.write(`COZE_BRIDGE_INSTALL_ERROR stage=wrapper reason=${err && err.message ? err.message : err}\n`);
15
+ process.exitCode = 1;
16
+ });
package/lib/archive.js ADDED
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const zlib = require('zlib');
6
+
7
+ // extractBinary extracts the single coze-bridge[.exe] entry from an artifact
8
+ // archive into destPath, setting an executable mode on POSIX targets.
9
+ function extractBinary(archivePath, format, destPath, osName) {
10
+ const data = fs.readFileSync(archivePath);
11
+ let bin;
12
+ if (format === 'tar.gz') {
13
+ bin = extractFromTarGz(data, osName);
14
+ } else if (format === 'zip') {
15
+ bin = extractFromZip(data, osName);
16
+ } else {
17
+ throw new Error(`unsupported archive format ${format}`);
18
+ }
19
+ if (!bin) {
20
+ throw new Error('archive did not contain a coze-bridge binary');
21
+ }
22
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
23
+ const mode = osName === 'windows' ? 0o644 : 0o755;
24
+ fs.writeFileSync(destPath, bin, { mode });
25
+ if (osName !== 'windows') {
26
+ fs.chmodSync(destPath, 0o755);
27
+ }
28
+ }
29
+
30
+ function wantName(osName) {
31
+ return osName === 'windows' ? 'coze-bridge.exe' : 'coze-bridge';
32
+ }
33
+
34
+ function extractFromTarGz(gzData, osName) {
35
+ const tarData = zlib.gunzipSync(gzData);
36
+ const want = wantName(osName);
37
+ let offset = 0;
38
+ while (offset + 512 <= tarData.length) {
39
+ const header = tarData.subarray(offset, offset + 512);
40
+ if (isZeroBlock(header)) {
41
+ break;
42
+ }
43
+ const name = readTarString(header, 0, 100);
44
+ const sizeStr = readTarString(header, 124, 12).trim();
45
+ const size = parseInt(sizeStr, 8) || 0;
46
+ const typeFlag = String.fromCharCode(header[156]);
47
+ offset += 512;
48
+ const fileData = tarData.subarray(offset, offset + size);
49
+ offset += Math.ceil(size / 512) * 512;
50
+ if ((typeFlag === '0' || typeFlag === '\0') && path.basename(name) === want) {
51
+ return Buffer.from(fileData);
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function isZeroBlock(buf) {
58
+ for (let i = 0; i < buf.length; i++) {
59
+ if (buf[i] !== 0) return false;
60
+ }
61
+ return true;
62
+ }
63
+
64
+ function readTarString(buf, start, len) {
65
+ const slice = buf.subarray(start, start + len);
66
+ let end = slice.indexOf(0);
67
+ if (end < 0) end = slice.length;
68
+ return slice.subarray(0, end).toString('utf8');
69
+ }
70
+
71
+ // extractFromZip reads the End Of Central Directory record, then walks central
72
+ // directory entries to find and inflate the wanted binary.
73
+ function extractFromZip(data, osName) {
74
+ const want = wantName(osName);
75
+ const eocd = findEOCD(data);
76
+ if (eocd < 0) {
77
+ throw new Error('zip end-of-central-directory not found');
78
+ }
79
+ const cdEntries = data.readUInt16LE(eocd + 10);
80
+ let cdOffset = data.readUInt32LE(eocd + 16);
81
+ for (let i = 0; i < cdEntries; i++) {
82
+ if (data.readUInt32LE(cdOffset) !== 0x02014b50) {
83
+ throw new Error('bad zip central directory signature');
84
+ }
85
+ const method = data.readUInt16LE(cdOffset + 10);
86
+ const compSize = data.readUInt32LE(cdOffset + 20);
87
+ const nameLen = data.readUInt16LE(cdOffset + 28);
88
+ const extraLen = data.readUInt16LE(cdOffset + 30);
89
+ const commentLen = data.readUInt16LE(cdOffset + 32);
90
+ const localOffset = data.readUInt32LE(cdOffset + 42);
91
+ const name = data.subarray(cdOffset + 46, cdOffset + 46 + nameLen).toString('utf8');
92
+ cdOffset += 46 + nameLen + extraLen + commentLen;
93
+ if (path.basename(name) !== want) {
94
+ continue;
95
+ }
96
+ return inflateLocalEntry(data, localOffset, method, compSize);
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function inflateLocalEntry(data, localOffset, method, compSize) {
102
+ if (data.readUInt32LE(localOffset) !== 0x04034b50) {
103
+ throw new Error('bad zip local header signature');
104
+ }
105
+ const nameLen = data.readUInt16LE(localOffset + 26);
106
+ const extraLen = data.readUInt16LE(localOffset + 28);
107
+ const dataStart = localOffset + 30 + nameLen + extraLen;
108
+ const raw = data.subarray(dataStart, dataStart + compSize);
109
+ if (method === 0) {
110
+ return Buffer.from(raw);
111
+ }
112
+ if (method === 8) {
113
+ return zlib.inflateRawSync(raw);
114
+ }
115
+ throw new Error(`unsupported zip compression method ${method}`);
116
+ }
117
+
118
+ function findEOCD(data) {
119
+ const minLen = 22;
120
+ if (data.length < minLen) return -1;
121
+ const maxBack = Math.min(data.length, minLen + 0xffff);
122
+ for (let i = data.length - minLen; i >= data.length - maxBack; i--) {
123
+ if (data.readUInt32LE(i) === 0x06054b50) {
124
+ return i;
125
+ }
126
+ }
127
+ return -1;
128
+ }
129
+
130
+ module.exports = {
131
+ extractBinary,
132
+ extractFromTarGz,
133
+ extractFromZip,
134
+ };
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // buildVersionManifestURL returns <base>/releases/<version>/manifest.json.
9
+ function buildVersionManifestURL(downloadBase, version) {
10
+ return joinHTTPS(downloadBase, ['releases', version, 'manifest.json']);
11
+ }
12
+
13
+ // buildChannelManifestURL returns <base>/channels/<channel>/manifest.json.
14
+ function buildChannelManifestURL(downloadBase, channel) {
15
+ return joinHTTPS(downloadBase, ['channels', channel, 'manifest.json']);
16
+ }
17
+
18
+ function joinHTTPS(base, elems) {
19
+ const parsed = new URL(String(base == null ? '' : base));
20
+ if (parsed.protocol !== 'https:') {
21
+ throw new Error(`download base must use https, got ${JSON.stringify(parsed.protocol.replace(':', ''))}`);
22
+ }
23
+ if (!parsed.host) {
24
+ throw new Error('download base host is required');
25
+ }
26
+ const parts = [];
27
+ const basePath = parsed.pathname.replace(/^\/+|\/+$/g, '');
28
+ if (basePath) parts.push(basePath);
29
+ for (const e of elems) parts.push(String(e).replace(/^\/+|\/+$/g, ''));
30
+ parsed.pathname = `/${parts.join('/')}`;
31
+ parsed.search = '';
32
+ parsed.hash = '';
33
+ return parsed.toString();
34
+ }
35
+
36
+ // defaultHTTPGet streams an https GET, following redirects to https only.
37
+ function defaultHTTPGet(url, onResponse, depth) {
38
+ const hops = depth || 0;
39
+ if (hops > 5) {
40
+ onResponse(new Error('too many redirects'));
41
+ return;
42
+ }
43
+ const req = https.get(url, (res) => {
44
+ const status = res.statusCode || 0;
45
+ if (status >= 300 && status < 400 && res.headers.location) {
46
+ res.resume();
47
+ let next;
48
+ try {
49
+ next = new URL(res.headers.location, url).toString();
50
+ } catch (err) {
51
+ onResponse(new Error(`invalid redirect location: ${res.headers.location}`));
52
+ return;
53
+ }
54
+ if (!next.startsWith('https:')) {
55
+ onResponse(new Error(`refusing non-https redirect to ${next}`));
56
+ return;
57
+ }
58
+ defaultHTTPGet(next, onResponse, hops + 1);
59
+ return;
60
+ }
61
+ if (status !== 200) {
62
+ res.resume();
63
+ onResponse(new Error(`unexpected status ${status} for ${url}`));
64
+ return;
65
+ }
66
+ onResponse(null, res);
67
+ });
68
+ req.on('error', (err) => onResponse(err));
69
+ }
70
+
71
+ // fetchBuffer downloads url into memory, enforcing a max byte cap.
72
+ function fetchBuffer(url, opts) {
73
+ const httpGet = (opts && opts.httpGet) || defaultHTTPGet;
74
+ const maxBytes = (opts && opts.maxBytes) || 16 * 1024 * 1024;
75
+ return new Promise((resolve, reject) => {
76
+ httpGet(url, (err, res) => {
77
+ if (err) {
78
+ reject(err);
79
+ return;
80
+ }
81
+ const chunks = [];
82
+ let total = 0;
83
+ res.on('data', (chunk) => {
84
+ total += chunk.length;
85
+ if (total > maxBytes) {
86
+ res.destroy();
87
+ reject(new Error(`response from ${url} exceeds ${maxBytes} bytes`));
88
+ return;
89
+ }
90
+ chunks.push(chunk);
91
+ });
92
+ res.on('end', () => resolve(Buffer.concat(chunks)));
93
+ res.on('error', reject);
94
+ });
95
+ });
96
+ }
97
+
98
+ // fetchJSON downloads and parses a JSON document, returning {buffer, json}.
99
+ async function fetchJSON(url, opts) {
100
+ const buffer = await fetchBuffer(url, opts);
101
+ let json;
102
+ try {
103
+ json = JSON.parse(buffer.toString('utf8'));
104
+ } catch (err) {
105
+ throw new Error(`failed to parse JSON from ${url}: ${err.message}`);
106
+ }
107
+ return { buffer, json };
108
+ }
109
+
110
+ // sha256Hex returns the lowercase hex sha256 digest of a buffer.
111
+ function sha256Hex(buffer) {
112
+ return crypto.createHash('sha256').update(buffer).digest('hex');
113
+ }
114
+
115
+ // downloadArtifact streams url to a temp file under destDir, verifying size
116
+ // then sha256; it removes the temp file on any failure.
117
+ function downloadArtifact(url, destDir, expected, opts) {
118
+ const httpGet = (opts && opts.httpGet) || defaultHTTPGet;
119
+ return new Promise((resolve, reject) => {
120
+ fs.mkdirSync(destDir, { recursive: true });
121
+ const tmp = path.join(destDir, `artifact.${process.pid}.${Date.now()}.tmp`);
122
+ httpGet(url, (err, res) => {
123
+ if (err) {
124
+ reject(err);
125
+ return;
126
+ }
127
+ const hash = crypto.createHash('sha256');
128
+ let total = 0;
129
+ const out = fs.createWriteStream(tmp);
130
+ const fail = (e) => {
131
+ out.destroy();
132
+ res.destroy();
133
+ try {
134
+ fs.rmSync(tmp, { force: true });
135
+ } catch (_) {
136
+ // best effort cleanup
137
+ }
138
+ reject(e);
139
+ };
140
+ res.on('data', (chunk) => {
141
+ total += chunk.length;
142
+ hash.update(chunk);
143
+ });
144
+ res.on('error', fail);
145
+ out.on('error', fail);
146
+ res.pipe(out);
147
+ out.on('finish', () => {
148
+ if (total !== expected.size) {
149
+ fail(new Error(`downloaded ${total} bytes, manifest size ${expected.size}`));
150
+ return;
151
+ }
152
+ const digest = hash.digest('hex');
153
+ if (digest !== String(expected.sha256).toLowerCase()) {
154
+ fail(new Error(`sha256 mismatch: got ${digest}, want ${expected.sha256}`));
155
+ return;
156
+ }
157
+ resolve(tmp);
158
+ });
159
+ });
160
+ });
161
+ }
162
+
163
+ module.exports = {
164
+ buildVersionManifestURL,
165
+ buildChannelManifestURL,
166
+ joinHTTPS,
167
+ defaultHTTPGet,
168
+ fetchBuffer,
169
+ fetchJSON,
170
+ sha256Hex,
171
+ downloadArtifact,
172
+ };
package/lib/errors.js ADDED
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ // Stable error prefixes shared with the Go native runtime and installers.
4
+ const INSTALL_ERROR = 'COZE_BRIDGE_INSTALL_ERROR';
5
+ const UPDATE_ERROR = 'COZE_BRIDGE_UPDATE_ERROR';
6
+
7
+ // BridgeError carries a failure stage so callers can print a stable prefix.
8
+ class BridgeError extends Error {
9
+ constructor(prefix, stage, message) {
10
+ super(message);
11
+ this.name = 'BridgeError';
12
+ this.prefix = prefix;
13
+ this.stage = stage;
14
+ }
15
+
16
+ // line returns the canonical single-line diagnostic for stderr and logs.
17
+ line() {
18
+ return `${this.prefix} stage=${this.stage} reason=${this.message}`;
19
+ }
20
+ }
21
+
22
+ function installError(stage, message) {
23
+ return new BridgeError(INSTALL_ERROR, stage, message);
24
+ }
25
+
26
+ function updateError(stage, message) {
27
+ return new BridgeError(UPDATE_ERROR, stage, message);
28
+ }
29
+
30
+ module.exports = {
31
+ INSTALL_ERROR,
32
+ UPDATE_ERROR,
33
+ BridgeError,
34
+ installError,
35
+ updateError,
36
+ };