coze-bridge 0.2.1-alpha.2 → 0.2.1-alpha.4
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 +185 -65
- package/bin/coze-bridge.js +16 -0
- package/lib/archive.js +134 -0
- package/lib/download.js +172 -0
- package/lib/errors.js +36 -0
- package/lib/install.js +129 -0
- package/lib/installstate.js +145 -0
- package/lib/launcher.js +136 -0
- package/lib/locate.js +176 -0
- package/lib/lock.js +147 -0
- package/lib/logger.js +50 -0
- package/lib/main.js +293 -0
- package/lib/manifest.js +161 -0
- package/lib/platform.js +109 -0
- package/lib/version.js +117 -0
- package/package.json +15 -50
- package/LICENSE +0 -21
- package/dist/index.js +0 -267
package/README.md
CHANGED
|
@@ -1,98 +1,218 @@
|
|
|
1
1
|
# coze-bridge
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
17
|
-
coze-bridge status
|
|
19
|
+
Use a global npm install when you want `coze-bridge` on PATH:
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
coze-bridge
|
|
21
|
+
```sh
|
|
22
|
+
npm install -g coze-bridge@latest
|
|
23
|
+
coze-bridge --pat-token=<sat_xxx> --pair-code=<xxx>
|
|
24
|
+
```
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
coze-bridge refresh
|
|
26
|
+
Use the native installers when Node/npm is not available:
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
#
|
|
29
|
-
coze-bridge
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
+
## Managed Layout
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Managed installs created by `npx`, global npm, curl, or PowerShell use one
|
|
53
|
+
native runtime layout:
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/download.js
ADDED
|
@@ -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
|
+
};
|