@landh93/web-codex-client 0.1.0-rc.1 → 0.1.0-rc.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/NPM_RELEASE.md +9 -6
- package/README.md +7 -37
- package/dist/cli.js +436 -164
- package/package.json +2 -2
package/NPM_RELEASE.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# npm 客户端发布与安装
|
|
2
2
|
|
|
3
|
-
包名:`@landh93/web-codex-client
|
|
3
|
+
包名:`@landh93/web-codex-client`。当前候选版为 `0.1.0-rc.3`,使用 `next` 标签。仅支持 Linux / Node.js 24;这是独立安装候选版,不代表公网、多人或真实模型执行已完成生产验收。`remote-wss-v1` 明确允许远端 WSS,继续使用固定 Noise 加密、TLS 校验及 Token 自动认证。项目尚未选定开源许可证,包暂保留 `UNLICENSED`;第三方代码许可见包内 `sdk/THIRD_PARTY_NOTICES.md`。
|
|
4
4
|
|
|
5
5
|
## 用户安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g @landh93/web-codex-client@next
|
|
9
9
|
web-codex-client configure
|
|
10
|
-
web-codex-client doctor
|
|
11
10
|
web-codex-client run
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
无需安装 pnpm、Rust 或构建工具。Codex 及其私有 Unix socket 必须在这台机器可用;本包不会安装或重启 Codex
|
|
13
|
+
无需安装 pnpm、Rust 或构建工具。Codex 及其私有 Unix socket 必须在这台机器可用;本包不会安装或重启 Codex。交互配置只输入服务端 HTTPS/WSS 地址和连接 Token(隐藏输入)。Codex socket 自动发现;需要自定义时使用 `--socket`。网页输入同一个连接 Token,**不是 npm 发布 token**。
|
|
15
14
|
|
|
16
15
|
配置默认位于 `${XDG_CONFIG_HOME:-$HOME/.config}/web-codex-client/config.json`,可用 `--config FILE` 或 `WEB_CODEX_CLIENT_CONFIG` 指定。状态目录和 token 文件分别为 0700/0600。重新配置保留身份和授权;运行中的 Client 需先停止再配置。升级包不改动用户配置;配置不会写入 npm 安装目录。
|
|
17
16
|
|
|
@@ -23,9 +22,13 @@ web-codex-client configure --relay wss://relay.example.com --token-stdin < /path
|
|
|
23
22
|
|
|
24
23
|
`doctor` 检查配置及 token 权限、WASM 完整性、WSS 设备认证和 Codex 初始化,不读取历史或调用模型。已有本地 Client 在线时复用其状态,不抢占连接;没有运行时创建一次短暂的设备认证连接,**同一设备 token 不要在其他机器同时使用**。报告默认是配置目录中的 `doctor-report.json`,不含 token、私钥、历史或上游原始报错。支持 `--quiet`、`--report PRIVATE_PATH`。失败阶段落盘后退出非零。
|
|
25
24
|
|
|
26
|
-
使用公开 CA 证书时通常无需额外设置;私有 CA 可在启动 Node 前设置 `NODE_EXTRA_CA_CERTS=/path/to/ca.pem`。禁止 `NODE_TLS_REJECT_UNAUTHORIZED=0`。Nginx 要正确代理 `/ws/v1/client`,
|
|
25
|
+
使用公开 CA 证书时通常无需额外设置;私有 CA 可在启动 Node 前设置 `NODE_EXTRA_CA_CERTS=/path/to/ca.pem`。禁止 `NODE_TLS_REJECT_UNAUTHORIZED=0`。Nginx 要正确代理 `/ws/v1/client`,Token 的 SHA-256 派生凭据经 Authorization 头发送,原始 Token 不发送给中继。
|
|
27
26
|
|
|
28
|
-
Client
|
|
27
|
+
Client 运行后,在网页输入同一个 Token 即可:自动选择设备、建立端到端加密连接、加载项目与会话。打开会话即可输入消息;写租约自动取得和续期,只有另一个浏览器占用时才需要处理冲突。无需 `pair-open`、`pair`、指纹核对或“申请控制权”。网页刷新后在同一标签页自动重连;Token 只保存在该标签页的 sessionStorage,退出登录清除。
|
|
28
|
+
|
|
29
|
+
默认授权本机 Codex 项目目录与活动/归档会话元数据识别出的已有项目读写,保留目录边界和 Codex 执行审批。授权在每次连接时重新读取,不持久保存临时浏览器身份。不同 token 对应不同设备,不能凭一个设备 Token 查看其他设备。
|
|
30
|
+
|
|
31
|
+
此版本需要同时更新 Server 网关、网页和 Client(SDK rc.4)。旧 Server 不支持新的 Token 登录;应先部署 Server 再升级并重启 Client。原有设备 Token 可继续使用,无需重新注册。新部署的 Server 可通过 `CONNECTION_TOKEN_FILE=/private/path/token` 直接配置同一个 Token,无需先创建管理员账号;文件权限为 0600,Token 至少 32 个无空白字符。管理员口令和 `pairingMode:"manual"` 仅保留为兼容入口,不属于默认连接流程。
|
|
29
32
|
|
|
30
33
|
后台常驻可使用 systemd 用户服务,将 `ExecStart` 设为实际安装的 `web-codex-client run --config /absolute/config.json`。npm 安装不会自动启用服务或打开配对窗口。
|
|
31
34
|
|
|
@@ -65,7 +68,7 @@ npm whoami --registry=https://registry.npmjs.org/ --userconfig="$HOME/.config/we
|
|
|
65
68
|
随后发布已通过验证的**具体 tgz**,不重新构建另一个包:
|
|
66
69
|
|
|
67
70
|
```bash
|
|
68
|
-
npm publish .test-output/landh93-web-codex-client-0.1.0-rc.
|
|
71
|
+
npm publish .test-output/landh93-web-codex-client-0.1.0-rc.3.tgz \
|
|
69
72
|
--access public --tag next --ignore-scripts \
|
|
70
73
|
--registry=https://registry.npmjs.org/ \
|
|
71
74
|
--userconfig="$HOME/.config/web-codex-npm/npmrc"
|
package/README.md
CHANGED
|
@@ -13,13 +13,12 @@
|
|
|
13
13
|
```bash
|
|
14
14
|
npm install -g @landh93/web-codex-client@next
|
|
15
15
|
web-codex-client configure
|
|
16
|
-
web-codex-client doctor
|
|
17
16
|
web-codex-client run
|
|
18
17
|
```
|
|
19
18
|
|
|
20
|
-
`configure`
|
|
19
|
+
`configure` 只需服务端 HTTPS/WSS 地址和连接 Token。启动后,在网页输入同一个 Token,就能查看项目、session 并发送对话;无需配对命令、指纹核对或申请控制权。Codex socket 自动发现,`doctor` 仅用于故障诊断。
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
已有 Codex 项目自动允许读写,执行审批和目录边界仍保留。配置、Token 和身份与 npm 安装目录分开存放,升级保留。Client rc.3 需要新版 Server 与 SDK rc.4;请先更新 Server,再升级 Client。完整安装和发布说明见 [NPM_RELEASE.md](./NPM_RELEASE.md)。
|
|
23
22
|
|
|
24
23
|
## 安装、构建、自测
|
|
25
24
|
|
|
@@ -42,44 +41,15 @@ NODE_EXTRA_CA_CERTS=/tmp/web-codex-tls/cert.pem pnpm test:cli
|
|
|
42
41
|
|
|
43
42
|
Chromium 已装在其他位置时可设置 `CHROMIUM_EXECUTABLE=/absolute/path/to/chrome`。本次实测使用 `/tmp/web-codex-browsers/chromium-1187/chrome-linux/chrome`。浏览器测试基于真实完成的四个阶段显示百分比、耗时、速率和 ETA;只使用合成数据。
|
|
44
43
|
|
|
45
|
-
已发布 SDK:`artifacts/web-codex-bridge-sdk-0.1.0-rc.
|
|
44
|
+
已发布 SDK:`artifacts/web-codex-bridge-sdk-0.1.0-rc.4.tgz`。Server 复制该文件到自己的 `vendor/` 后安装并固定锁文件,核对同名 `.sha256`。不要覆盖已发布文件,也不要通过 link: 指向本目录源码。构建后 `pnpm pack:sdk` 会拒绝覆盖已有版本;修改 SDK 需先调整 `scripts/build.mjs` 中的版本。
|
|
46
45
|
|
|
47
|
-
##
|
|
46
|
+
## 连接与授权
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
默认使用 Token 自动认证。Client 与网页分别持有相同 Token,通过 Noise 握手哈希和角色绑定的 HMAC 验证对端;中继只接收 Token 哈希凭据,不处理会话明文。Token 持有者可访问该设备已经在 Codex 中打开过的项目并读写。
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
mkdir -p ~/.config/web-codex-client
|
|
53
|
-
chmod 700 ~/.config/web-codex-client
|
|
54
|
-
cp deploy/config.example.json ~/.config/web-codex-client/config.json
|
|
55
|
-
# 修改 relayUrl、upstream.socket;把 Server 给出的 token 写入 device-token 文件。
|
|
56
|
-
chmod 600 ~/.config/web-codex-client/device-token
|
|
57
|
-
node dist/cli.js init --state ~/.config/web-codex-client/state
|
|
58
|
-
node dist/cli.js run --config ~/.config/web-codex-client/config.json
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
中继仅允许 WSS;不会禁用 TLS 验证。自签名开发证书使用 `NODE_EXTRA_CA_CERTS`。长期 token 不放 URL 或命令参数。设备身份私钥存于 0700 state 目录中的 0600 identity.json;这是磁盘明文身份文件,需要 OS/磁盘保护。授权表只含公钥、规范化工作区路径和读写权限,不含对话。
|
|
62
|
-
|
|
63
|
-
另开本机终端进行配对:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
node dist/cli.js status --state ~/.config/web-codex-client/state
|
|
67
|
-
node dist/cli.js pair-open --state ~/.config/web-codex-client/state
|
|
68
|
-
node dist/cli.js pair-list --state ~/.config/web-codex-client/state
|
|
69
|
-
# 浏览器完成握手后,与本地 pair-list 核对完整的 64 位指纹。
|
|
70
|
-
# 在可信浏览器点击确认,再在本机明确授予目录权限:
|
|
71
|
-
node dist/cli.js pair-approve --state ~/.config/web-codex-client/state \
|
|
72
|
-
--fingerprint FULL_64_CHARACTER_FINGERPRINT --root /absolute/project
|
|
73
|
-
# 需要通过 Codex 执行任务时,审批命令额外加 --write。
|
|
74
|
-
node dist/cli.js revoke --state ~/.config/web-codex-client/state \
|
|
75
|
-
--peer BROWSER_PUBLIC_KEY_FROM_PAIR_LIST
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
配对窗口 120 秒,本机确认还要求浏览器已确认相同指纹。默认只读;`--write` 才允许执行和租约。撤销立即关闭该身份的会话并拒绝后续业务。浏览器身份默认仅存内存,刷新后重新配对;同一页面断线重连可以继续使用内存身份和固定公钥。
|
|
79
|
-
|
|
80
|
-
`status` 分别返回 relayOnline、handshakes、authorizedSessions、codexAvailable。Codex 连接按第一次已授权请求惰性建立,因此仅中继上线时 codexAvailable 仍为 false。上游断线后下一次请求重连,旧 epoch/订阅失效。后台服务无有限总工作量,不显示伪造百分比。
|
|
50
|
+
只允许 WSS 并验证证书;私有 CA 使用 `NODE_EXTRA_CA_CERTS`。浏览器在当前标签页保存 Token 以支持刷新自动重连,退出登录清除。已识别项目每次连接重新扫描,根目录、用户主目录和 Client 私有状态的父目录不会自动授权。
|
|
81
51
|
|
|
82
|
-
`deploy/web-codex-client.service`
|
|
52
|
+
旧的人工配对仅在配置显式设置 `pairingMode:"manual"` 时启用,不再是默认流程。每个 stateDir 只运行一个 Client;`status` 和 `doctor` 可诊断连接,`deploy/web-codex-client.service` 提供后台运行模板。
|
|
83
53
|
|
|
84
54
|
## Codex 接入与只读诊断
|
|
85
55
|
|
package/dist/cli.js
CHANGED
|
@@ -83,7 +83,7 @@ var CodexRpc = class _CodexRpc extends EventEmitter {
|
|
|
83
83
|
this.ws.once("open", resolve5);
|
|
84
84
|
this.ws.once("error", reject);
|
|
85
85
|
});
|
|
86
|
-
this.ws.on("message", (
|
|
86
|
+
this.ws.on("message", (data2) => this.parse(data2.toString()));
|
|
87
87
|
this.ws.on("error", () => this.fail());
|
|
88
88
|
this.ws.on("close", () => this.fail());
|
|
89
89
|
return;
|
|
@@ -124,10 +124,10 @@ var CodexRpc = class _CodexRpc extends EventEmitter {
|
|
|
124
124
|
this.emit(m.id === void 0 ? "notification" : "serverRequest", m);
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
|
-
const
|
|
128
|
-
const p = this.pending.get(
|
|
127
|
+
const key2 = String(m.id);
|
|
128
|
+
const p = this.pending.get(key2);
|
|
129
129
|
if (!p) return;
|
|
130
|
-
this.pending.delete(
|
|
130
|
+
this.pending.delete(key2);
|
|
131
131
|
clearTimeout(p.timer);
|
|
132
132
|
if (m.error) p.reject(new RpcError(m.error.code, m.error.message));
|
|
133
133
|
else p.resolve(m.result);
|
|
@@ -137,14 +137,14 @@ var CodexRpc = class _CodexRpc extends EventEmitter {
|
|
|
137
137
|
}
|
|
138
138
|
send(value) {
|
|
139
139
|
if (this.ended) throw new BridgeError("UPSTREAM_UNAVAILABLE");
|
|
140
|
-
const
|
|
140
|
+
const data2 = JSON.stringify(value);
|
|
141
141
|
if (this.ws) {
|
|
142
142
|
if (this.ws.bufferedAmount > 1024 * 1024) throw new BridgeError("BUSY");
|
|
143
|
-
this.ws.send(
|
|
143
|
+
this.ws.send(data2);
|
|
144
144
|
} else {
|
|
145
145
|
if (!this.child || this.child.stdin.writableLength > 1024 * 1024)
|
|
146
146
|
throw new BridgeError("BUSY");
|
|
147
|
-
this.child.stdin.write(
|
|
147
|
+
this.child.stdin.write(data2 + "\n");
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
call(method, params) {
|
|
@@ -603,8 +603,8 @@ async function __wbg_load(module, imports) {
|
|
|
603
603
|
}
|
|
604
604
|
}
|
|
605
605
|
}
|
|
606
|
-
const
|
|
607
|
-
return await WebAssembly.instantiate(
|
|
606
|
+
const bytes2 = await module.arrayBuffer();
|
|
607
|
+
return await WebAssembly.instantiate(bytes2, imports);
|
|
608
608
|
} else {
|
|
609
609
|
const instance = await WebAssembly.instantiate(module, imports);
|
|
610
610
|
if (instance instanceof WebAssembly.Instance) {
|
|
@@ -750,10 +750,10 @@ function aad(f) {
|
|
|
750
750
|
var WASM_SHA256 = "1ac97442f8b0fd8ef20b31d46d2e4cbc30b6d877346f0cf2438eb08026330490";
|
|
751
751
|
|
|
752
752
|
// sdk/src/noise-provider.ts
|
|
753
|
-
async function initializeNoise(
|
|
754
|
-
const digest = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(
|
|
753
|
+
async function initializeNoise(bytes2) {
|
|
754
|
+
const digest = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes2))), (n) => n.toString(16).padStart(2, "0")).join("");
|
|
755
755
|
if (digest !== WASM_SHA256) throw Error("CRYPTO_ARTIFACT_MISMATCH");
|
|
756
|
-
initSync({ module:
|
|
756
|
+
initSync({ module: bytes2 });
|
|
757
757
|
return {
|
|
758
758
|
constants: { NOISE_DH_CURVE25519: 1, NOISE_ROLE_INITIATOR: 1, NOISE_ROLE_RESPONDER: 2, NOISE_ACTION_WRITE_MESSAGE: 1, NOISE_ACTION_READ_MESSAGE: 2, NOISE_ACTION_SPLIT: 3 },
|
|
759
759
|
CreateKeyPair(algorithm) {
|
|
@@ -771,9 +771,9 @@ async function initializeNoise(bytes) {
|
|
|
771
771
|
if (suite !== SUITE || ![1, 2].includes(role)) throw Error("UNSUPPORTED_SUITE");
|
|
772
772
|
}
|
|
773
773
|
inner;
|
|
774
|
-
Initialize(prologue2,
|
|
774
|
+
Initialize(prologue2, key2) {
|
|
775
775
|
if (this.inner) throw Error("HANDSHAKE_STATE");
|
|
776
|
-
this.inner = new Handshake(this.role === 1, prologue2,
|
|
776
|
+
this.inner = new Handshake(this.role === 1, prologue2, key2);
|
|
777
777
|
}
|
|
778
778
|
get state() {
|
|
779
779
|
if (!this.inner) throw Error("HANDSHAKE_STATE");
|
|
@@ -929,7 +929,8 @@ var messageSchema = z2.union([
|
|
|
929
929
|
z2.strictObject({
|
|
930
930
|
kind: z2.literal("pairing"),
|
|
931
931
|
status: z2.enum(["pending", "confirmed", "ready", "rejected"]),
|
|
932
|
-
fingerprint: z2.string().regex(/^[a-f0-9]{64}$/)
|
|
932
|
+
fingerprint: z2.string().regex(/^[a-f0-9]{64}$/),
|
|
933
|
+
proof: z2.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
933
934
|
}),
|
|
934
935
|
z2.strictObject({
|
|
935
936
|
kind: z2.literal("ack"),
|
|
@@ -944,9 +945,9 @@ function parseMessage(value) {
|
|
|
944
945
|
return m;
|
|
945
946
|
}
|
|
946
947
|
function encodeMessage(value) {
|
|
947
|
-
const
|
|
948
|
-
if (
|
|
949
|
-
return
|
|
948
|
+
const bytes2 = new TextEncoder().encode(JSON.stringify(parseMessage(value)));
|
|
949
|
+
if (bytes2.length > MAX_MESSAGE_BYTES) throw Error("RESOURCE_TOO_LARGE");
|
|
950
|
+
return bytes2;
|
|
950
951
|
}
|
|
951
952
|
var opaque = z2.object({ id: z2.string() }).passthrough();
|
|
952
953
|
var projectDto = z2.strictObject({
|
|
@@ -1168,8 +1169,8 @@ var SecureSession = class {
|
|
|
1168
1169
|
this.guard();
|
|
1169
1170
|
if (!this.ready || !this.trusted && message.kind !== "pairing")
|
|
1170
1171
|
throw Error("NOT_PAIRED");
|
|
1171
|
-
const
|
|
1172
|
-
const total = Math.ceil(
|
|
1172
|
+
const bytes2 = encodeMessage(message);
|
|
1173
|
+
const total = Math.ceil(bytes2.length / CHUNK_BYTES);
|
|
1173
1174
|
const id2 = randomId();
|
|
1174
1175
|
for (let index = 0; index < total; index++) {
|
|
1175
1176
|
this.guard();
|
|
@@ -1178,9 +1179,9 @@ var SecureSession = class {
|
|
|
1178
1179
|
id: id2,
|
|
1179
1180
|
index,
|
|
1180
1181
|
total,
|
|
1181
|
-
size:
|
|
1182
|
+
size: bytes2.length,
|
|
1182
1183
|
data: b64(
|
|
1183
|
-
|
|
1184
|
+
bytes2.subarray(index * CHUNK_BYTES, (index + 1) * CHUNK_BYTES)
|
|
1184
1185
|
)
|
|
1185
1186
|
})
|
|
1186
1187
|
);
|
|
@@ -1268,6 +1269,23 @@ var SecureSession = class {
|
|
|
1268
1269
|
}
|
|
1269
1270
|
};
|
|
1270
1271
|
|
|
1272
|
+
// sdk/src/token.ts
|
|
1273
|
+
var bytes = (value) => new TextEncoder().encode(value);
|
|
1274
|
+
async function relayCredential(token) {
|
|
1275
|
+
return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes(token))));
|
|
1276
|
+
}
|
|
1277
|
+
var data = (fingerprint, role) => bytes(JSON.stringify(["web-codex-token-v1", role, fingerprint]));
|
|
1278
|
+
async function key(token) {
|
|
1279
|
+
return crypto.subtle.importKey("raw", bytes(token), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
1280
|
+
}
|
|
1281
|
+
async function tokenProof(token, fingerprint, role) {
|
|
1282
|
+
return hex(new Uint8Array(await crypto.subtle.sign("HMAC", await key(token), data(fingerprint, role))));
|
|
1283
|
+
}
|
|
1284
|
+
async function verifyTokenProof(token, fingerprint, role, proof) {
|
|
1285
|
+
if (!proof || !/^[a-f0-9]{64}$/.test(proof)) return false;
|
|
1286
|
+
return crypto.subtle.verify("HMAC", await key(token), Uint8Array.from(proof.match(/../g), (value) => parseInt(value, 16)), data(fingerprint, role));
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1271
1289
|
// sdk/src/node.ts
|
|
1272
1290
|
var pending;
|
|
1273
1291
|
function loadNoise() {
|
|
@@ -1288,6 +1306,7 @@ var configSchema = z4.strictObject({
|
|
|
1288
1306
|
return u.protocol === "wss:" && !u.username && !u.password && !u.search && !u.hash && u.pathname === "/";
|
|
1289
1307
|
}, "Relay requires WSS without URL credentials"),
|
|
1290
1308
|
deviceTokenFile: z4.string(),
|
|
1309
|
+
pairingMode: z4.enum(["token", "manual"]).optional(),
|
|
1291
1310
|
stateDir: z4.string(),
|
|
1292
1311
|
experimentalCrypto: z4.literal(true).optional(),
|
|
1293
1312
|
cryptoPolicy: z4.enum(["local-reviewed-v1", "remote-wss-v1"]).optional(),
|
|
@@ -1336,10 +1355,10 @@ async function loadState(dir) {
|
|
|
1336
1355
|
const path = dir + "/identity.json";
|
|
1337
1356
|
let identity;
|
|
1338
1357
|
try {
|
|
1339
|
-
const
|
|
1358
|
+
const data2 = JSON.parse(await privateRead(path));
|
|
1340
1359
|
identity = {
|
|
1341
|
-
privateKey: unb64(
|
|
1342
|
-
publicKey: unb64(
|
|
1360
|
+
privateKey: unb64(data2.privateKey),
|
|
1361
|
+
publicKey: unb64(data2.publicKey)
|
|
1343
1362
|
};
|
|
1344
1363
|
if (identity.privateKey.length !== 32 || identity.publicKey.length !== 32)
|
|
1345
1364
|
throw Error("IDENTITY");
|
|
@@ -1356,15 +1375,18 @@ async function loadState(dir) {
|
|
|
1356
1375
|
list = z4.array(
|
|
1357
1376
|
z4.strictObject({
|
|
1358
1377
|
peer: z4.string(),
|
|
1359
|
-
roots: z4.array(z4.string()).min(1).max(
|
|
1360
|
-
write: z4.boolean()
|
|
1378
|
+
roots: z4.array(z4.string()).min(1).max(256),
|
|
1379
|
+
write: z4.boolean(),
|
|
1380
|
+
writeRoots: z4.array(z4.string()).max(256).optional()
|
|
1361
1381
|
})
|
|
1362
1382
|
).max(256).parse(JSON.parse(await privateRead(dir + "/grants.json")));
|
|
1363
1383
|
} catch (e) {
|
|
1364
1384
|
if (e.code !== "ENOENT") throw e;
|
|
1365
1385
|
}
|
|
1366
|
-
for (const g of list)
|
|
1386
|
+
for (const g of list) {
|
|
1367
1387
|
g.roots = await Promise.all(g.roots.map((r) => realpath(r)));
|
|
1388
|
+
if (g.writeRoots) g.writeRoots = await Promise.all(g.writeRoots.map((r) => realpath(r)));
|
|
1389
|
+
}
|
|
1368
1390
|
return { identity, grants: new Map(list.map((g) => [g.peer, g])) };
|
|
1369
1391
|
}
|
|
1370
1392
|
|
|
@@ -1374,7 +1396,7 @@ import {
|
|
|
1374
1396
|
createServer,
|
|
1375
1397
|
createConnection
|
|
1376
1398
|
} from "node:net";
|
|
1377
|
-
import { chmod, realpath as
|
|
1399
|
+
import { chmod, realpath as realpath4, unlink, lstat as lstat2, stat as stat3 } from "node:fs/promises";
|
|
1378
1400
|
import { z as z5 } from "zod";
|
|
1379
1401
|
|
|
1380
1402
|
// src/bridge.ts
|
|
@@ -1491,15 +1513,15 @@ var FileResources = class {
|
|
|
1491
1513
|
};
|
|
1492
1514
|
try {
|
|
1493
1515
|
await verify();
|
|
1494
|
-
const
|
|
1495
|
-
const { bytesRead } = await h.fd.read(
|
|
1516
|
+
const bytes2 = Buffer.alloc(Math.min(length, h.size - offset));
|
|
1517
|
+
const { bytesRead } = await h.fd.read(bytes2, 0, bytes2.length, offset);
|
|
1496
1518
|
await verify();
|
|
1497
|
-
if (bytesRead !==
|
|
1519
|
+
if (bytesRead !== bytes2.length) throw new BridgeError("RESOURCE_CHANGED");
|
|
1498
1520
|
return {
|
|
1499
1521
|
resourceId: id2,
|
|
1500
1522
|
offset,
|
|
1501
|
-
data:
|
|
1502
|
-
sha256: createHash("sha256").update(
|
|
1523
|
+
data: bytes2.toString("base64url"),
|
|
1524
|
+
sha256: createHash("sha256").update(bytes2).digest("hex"),
|
|
1503
1525
|
eof: offset + bytesRead === h.size
|
|
1504
1526
|
};
|
|
1505
1527
|
} catch (e) {
|
|
@@ -1569,9 +1591,9 @@ var Dedup = class {
|
|
|
1569
1591
|
bytes = 0;
|
|
1570
1592
|
entries = /* @__PURE__ */ new Map();
|
|
1571
1593
|
run(owner, id2, body, fn) {
|
|
1572
|
-
const
|
|
1594
|
+
const key2 = owner + ":" + id2;
|
|
1573
1595
|
const digest = createHash2("sha256").update(JSON.stringify(body)).digest("hex");
|
|
1574
|
-
const old = this.entries.get(
|
|
1596
|
+
const old = this.entries.get(key2);
|
|
1575
1597
|
if (old) {
|
|
1576
1598
|
if (old.digest !== digest) throw new BridgeError("REQUEST_ID_REUSED");
|
|
1577
1599
|
return old.promise;
|
|
@@ -1582,16 +1604,16 @@ var Dedup = class {
|
|
|
1582
1604
|
"Dedup budget exhausted; reconnect after a deliberate client restart and reconcile outcomes"
|
|
1583
1605
|
);
|
|
1584
1606
|
const promise = Promise.resolve().then(fn).then((result) => {
|
|
1585
|
-
const
|
|
1586
|
-
if (
|
|
1607
|
+
const bytes2 = Buffer.byteLength(JSON.stringify(result));
|
|
1608
|
+
if (bytes2 > 1024 * 1024 || this.bytes + bytes2 > 8 * 1024 * 1024)
|
|
1587
1609
|
throw new BridgeError(
|
|
1588
1610
|
"OUTCOME_UNKNOWN",
|
|
1589
1611
|
"Write executed; response exceeds retained dedup budget; reconcile upstream"
|
|
1590
1612
|
);
|
|
1591
|
-
this.bytes +=
|
|
1613
|
+
this.bytes += bytes2;
|
|
1592
1614
|
return result;
|
|
1593
1615
|
});
|
|
1594
|
-
this.entries.set(
|
|
1616
|
+
this.entries.set(key2, { digest, promise, at: Date.now() });
|
|
1595
1617
|
return promise;
|
|
1596
1618
|
}
|
|
1597
1619
|
};
|
|
@@ -1772,6 +1794,11 @@ var Bridge = class {
|
|
|
1772
1794
|
if (!grant) throw new BridgeError("NOT_PAIRED");
|
|
1773
1795
|
return grant;
|
|
1774
1796
|
}
|
|
1797
|
+
async writable(owner, path) {
|
|
1798
|
+
const grant = this.grant(owner);
|
|
1799
|
+
if (!grant.write) throw new BridgeError("FORBIDDEN", "\u76EE\u5F55\u5C1A\u672A\u5141\u8BB8\u5199\u5165\uFF0C\u8BF7\u5728\u672C\u673A\u8FD0\u884C web-codex-client authorize --root \u76EE\u5F55\u3002");
|
|
1800
|
+
await authorizedPath(grant.writeRoots ?? grant.roots, path);
|
|
1801
|
+
}
|
|
1775
1802
|
async project(owner, id2) {
|
|
1776
1803
|
const g = this.grant(owner);
|
|
1777
1804
|
if (id2.startsWith("cwd:")) {
|
|
@@ -1812,12 +1839,12 @@ var Bridge = class {
|
|
|
1812
1839
|
const h = this.histories.get(p.cursor);
|
|
1813
1840
|
if (!h || h.owner !== owner || h.thread !== p.threadId || h.kind !== method + ":" + (p.turnId ?? "") || h.expires < Date.now())
|
|
1814
1841
|
throw new BridgeError("RESYNC_REQUIRED");
|
|
1815
|
-
const
|
|
1816
|
-
h.offset +=
|
|
1842
|
+
const data3 = h.data.slice(h.offset, h.offset + p.limit);
|
|
1843
|
+
h.offset += data3.length;
|
|
1817
1844
|
this.histories.delete(p.cursor);
|
|
1818
1845
|
const nextCursor2 = h.offset < h.data.length ? "fallback:" + randomUUID4() : null;
|
|
1819
1846
|
if (nextCursor2) this.histories.set(nextCursor2, h);
|
|
1820
|
-
return { data:
|
|
1847
|
+
return { data: data3, nextCursor: nextCursor2, source: "bounded-fallback" };
|
|
1821
1848
|
}
|
|
1822
1849
|
try {
|
|
1823
1850
|
const r2 = await this.upstream.call(
|
|
@@ -1848,31 +1875,31 @@ var Bridge = class {
|
|
|
1848
1875
|
threadId: p.threadId,
|
|
1849
1876
|
includeTurns: true
|
|
1850
1877
|
});
|
|
1851
|
-
const
|
|
1852
|
-
if (
|
|
1878
|
+
const bytes2 = Buffer.byteLength(JSON.stringify(r));
|
|
1879
|
+
if (bytes2 > 8 * 1024 * 1024 || Array.from(this.histories.values()).reduce((n, h) => n + h.bytes, 0) + bytes2 > 8 * 1024 * 1024)
|
|
1853
1880
|
throw new BridgeError(
|
|
1854
1881
|
"RESOURCE_TOO_LARGE",
|
|
1855
1882
|
"Legacy history exceeds bounded fallback budget"
|
|
1856
1883
|
);
|
|
1857
1884
|
const turns = (r.thread.turns ?? []).slice().reverse();
|
|
1858
|
-
const
|
|
1885
|
+
const data2 = method === "turns.list" ? turns.map((t) => ({ ...t, items: [] })) : turns.filter((t) => !p.turnId || p.turnId === t.id).flatMap(
|
|
1859
1886
|
(t) => (t.items ?? []).slice().reverse().map((item) => ({ turnId: t.id, item }))
|
|
1860
1887
|
);
|
|
1861
|
-
const nextCursor =
|
|
1888
|
+
const nextCursor = data2.length > p.limit ? "fallback:" + randomUUID4() : null;
|
|
1862
1889
|
if (nextCursor) {
|
|
1863
1890
|
if (this.histories.size >= 32) throw new BridgeError("BUSY");
|
|
1864
1891
|
this.histories.set(nextCursor, {
|
|
1865
1892
|
owner,
|
|
1866
1893
|
thread: p.threadId,
|
|
1867
1894
|
kind: method + ":" + (p.turnId ?? ""),
|
|
1868
|
-
data,
|
|
1895
|
+
data: data2,
|
|
1869
1896
|
offset: p.limit,
|
|
1870
|
-
bytes,
|
|
1897
|
+
bytes: bytes2,
|
|
1871
1898
|
expires: Date.now() + 6e4
|
|
1872
1899
|
});
|
|
1873
1900
|
}
|
|
1874
1901
|
return {
|
|
1875
|
-
data:
|
|
1902
|
+
data: data2.slice(0, p.limit),
|
|
1876
1903
|
nextCursor,
|
|
1877
1904
|
source: "bounded-fallback"
|
|
1878
1905
|
};
|
|
@@ -1881,25 +1908,25 @@ var Bridge = class {
|
|
|
1881
1908
|
* Buffer at most one turn; never load a whole long conversation for this path. */
|
|
1882
1909
|
async turnItems(owner, p, previous) {
|
|
1883
1910
|
const h = previous ? { ...previous } : { owner, thread: p.threadId, kind: "items.list:" + (p.turnId ?? ""), data: [], offset: 0, bytes: 0, expires: Date.now() + 6e4 };
|
|
1884
|
-
const
|
|
1911
|
+
const data2 = [];
|
|
1885
1912
|
let pages = 0;
|
|
1886
1913
|
const visited = /* @__PURE__ */ new Set();
|
|
1887
|
-
while (
|
|
1888
|
-
const part = h.data.slice(h.offset, h.offset + p.limit -
|
|
1889
|
-
|
|
1914
|
+
while (data2.length < p.limit) {
|
|
1915
|
+
const part = h.data.slice(h.offset, h.offset + p.limit - data2.length);
|
|
1916
|
+
data2.push(...part);
|
|
1890
1917
|
h.offset += part.length;
|
|
1891
|
-
if (
|
|
1918
|
+
if (data2.length >= p.limit || h.upstream === null || pages >= 10) break;
|
|
1892
1919
|
if (h.upstream && visited.has(h.upstream)) throw new BridgeError("RESYNC_REQUIRED");
|
|
1893
1920
|
if (h.upstream) visited.add(h.upstream);
|
|
1894
1921
|
const r = await this.upstream.call("thread/turns/list", { threadId: p.threadId, cursor: h.upstream, limit: 1, sortDirection: "desc", itemsView: "full" });
|
|
1895
1922
|
pages++;
|
|
1896
|
-
const
|
|
1923
|
+
const bytes2 = Buffer.byteLength(JSON.stringify(r));
|
|
1897
1924
|
const retained = Array.from(this.histories.values()).reduce((sum, entry) => sum + (entry === previous ? 0 : entry.bytes), 0);
|
|
1898
|
-
if (
|
|
1925
|
+
if (bytes2 > 8 * 1024 * 1024 || retained + bytes2 > 8 * 1024 * 1024) throw new BridgeError("RESOURCE_TOO_LARGE");
|
|
1899
1926
|
if (r.data.some((turn) => turn.itemsView && turn.itemsView !== "full")) throw new BridgeError("UNSUPPORTED_CAPABILITY", "Upstream did not return full turn items");
|
|
1900
1927
|
h.data = r.data.filter((turn) => !p.turnId || turn.id === p.turnId).flatMap((turn) => (turn.items ?? []).slice().reverse().map((item) => ({ turnId: turn.id, item })));
|
|
1901
1928
|
h.offset = 0;
|
|
1902
|
-
h.bytes =
|
|
1929
|
+
h.bytes = bytes2;
|
|
1903
1930
|
h.upstream = r.nextCursor ?? null;
|
|
1904
1931
|
}
|
|
1905
1932
|
const more = h.offset < h.data.length || h.upstream !== null;
|
|
@@ -1907,7 +1934,7 @@ var Bridge = class {
|
|
|
1907
1934
|
if (previous) this.histories.delete(p.cursor);
|
|
1908
1935
|
const nextCursor = more ? "turn-items:" + randomUUID4() : null;
|
|
1909
1936
|
if (nextCursor) this.histories.set(nextCursor, h);
|
|
1910
|
-
return { data, nextCursor, source: "bounded-fallback" };
|
|
1937
|
+
return { data: data2, nextCursor, source: "bounded-fallback" };
|
|
1911
1938
|
}
|
|
1912
1939
|
async handle(owner, message) {
|
|
1913
1940
|
if (message.kind === "serverResponse") {
|
|
@@ -1979,7 +2006,10 @@ var Bridge = class {
|
|
|
1979
2006
|
historyFallback: "bounded",
|
|
1980
2007
|
activeTakeover: "unverified"
|
|
1981
2008
|
};
|
|
1982
|
-
if ("threadId" in p)
|
|
2009
|
+
if ("threadId" in p) {
|
|
2010
|
+
const thread2 = await this.thread(owner, p.threadId);
|
|
2011
|
+
if (writes.has(method) || method === "leases.acquire") await this.writable(owner, thread2.cwd);
|
|
2012
|
+
}
|
|
1983
2013
|
if (writes.has(method) && "threadId" in p) {
|
|
1984
2014
|
this.leases.assert(p.threadId, owner);
|
|
1985
2015
|
if ((method === "turns.start" || method === "turns.interrupt") && p.connectionEpoch !== this.subscriptions.epoch)
|
|
@@ -1999,12 +2029,12 @@ var Bridge = class {
|
|
|
1999
2029
|
const r = await this.upstream.call("project/list", p);
|
|
2000
2030
|
if (!p.cursor && r.data.length === 0 && !r.nextCursor)
|
|
2001
2031
|
throw new RpcError(-32601, "Empty project catalog; discover history roots");
|
|
2002
|
-
const
|
|
2032
|
+
const data2 = [];
|
|
2003
2033
|
for (const item of r.data) {
|
|
2004
2034
|
for (const root of item.roots ?? []) {
|
|
2005
2035
|
try {
|
|
2006
2036
|
const cwd = await authorizedPath(g.roots, root.path);
|
|
2007
|
-
|
|
2037
|
+
data2.push({
|
|
2008
2038
|
id: item.id,
|
|
2009
2039
|
name: item.name,
|
|
2010
2040
|
cwd,
|
|
@@ -2015,10 +2045,10 @@ var Bridge = class {
|
|
|
2015
2045
|
}
|
|
2016
2046
|
}
|
|
2017
2047
|
}
|
|
2018
|
-
return { data, nextCursor: r.nextCursor ?? null };
|
|
2048
|
+
return { data: data2, nextCursor: r.nextCursor ?? null };
|
|
2019
2049
|
} catch (e) {
|
|
2020
2050
|
if (!unsupported(e)) throw e;
|
|
2021
|
-
const
|
|
2051
|
+
const data2 = [];
|
|
2022
2052
|
const seen = new Set(previous?.seen);
|
|
2023
2053
|
let upstreamCursor = previous?.upstream ?? p.cursor;
|
|
2024
2054
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2029,7 +2059,7 @@ var Bridge = class {
|
|
|
2029
2059
|
}
|
|
2030
2060
|
const r = await this.upstream.call("thread/list", {
|
|
2031
2061
|
cursor: upstreamCursor,
|
|
2032
|
-
limit: (p.limit ?? 50) -
|
|
2062
|
+
limit: (p.limit ?? 50) - data2.length,
|
|
2033
2063
|
useStateDbOnly: true,
|
|
2034
2064
|
sourceKinds: sources,
|
|
2035
2065
|
sortKey: "updated_at",
|
|
@@ -2044,7 +2074,7 @@ var Bridge = class {
|
|
|
2044
2074
|
}
|
|
2045
2075
|
if (!seen.has(cwd)) {
|
|
2046
2076
|
seen.add(cwd);
|
|
2047
|
-
|
|
2077
|
+
data2.push({
|
|
2048
2078
|
id: "cwd:" + Buffer.from(cwd).toString("base64url"),
|
|
2049
2079
|
name: basename2(cwd),
|
|
2050
2080
|
cwd,
|
|
@@ -2053,7 +2083,7 @@ var Bridge = class {
|
|
|
2053
2083
|
}
|
|
2054
2084
|
}
|
|
2055
2085
|
upstreamCursor = r.nextCursor ?? void 0;
|
|
2056
|
-
if (!upstreamCursor ||
|
|
2086
|
+
if (!upstreamCursor || data2.length >= (p.limit ?? 50)) break;
|
|
2057
2087
|
}
|
|
2058
2088
|
if (seen.size > 1024 || this.projectCursors.size >= 128)
|
|
2059
2089
|
throw new BridgeError("RESOURCE_TOO_LARGE");
|
|
@@ -2066,7 +2096,7 @@ var Bridge = class {
|
|
|
2066
2096
|
seen,
|
|
2067
2097
|
expires: Date.now() + 6e4
|
|
2068
2098
|
});
|
|
2069
|
-
return { data, nextCursor };
|
|
2099
|
+
return { data: data2, nextCursor };
|
|
2070
2100
|
}
|
|
2071
2101
|
}
|
|
2072
2102
|
case "projects.read":
|
|
@@ -2083,14 +2113,14 @@ var Bridge = class {
|
|
|
2083
2113
|
sortKey: "updated_at",
|
|
2084
2114
|
sortDirection: "desc"
|
|
2085
2115
|
});
|
|
2086
|
-
const
|
|
2116
|
+
const data2 = [];
|
|
2087
2117
|
for (const t of r.data)
|
|
2088
2118
|
try {
|
|
2089
2119
|
await authorizedPath(g.roots, t.cwd);
|
|
2090
|
-
|
|
2120
|
+
data2.push(t);
|
|
2091
2121
|
} catch {
|
|
2092
2122
|
}
|
|
2093
|
-
return { data, nextCursor: r.nextCursor ?? null };
|
|
2123
|
+
return { data: data2, nextCursor: r.nextCursor ?? null };
|
|
2094
2124
|
}
|
|
2095
2125
|
case "threads.read":
|
|
2096
2126
|
return { thread: await this.thread(owner, p.threadId) };
|
|
@@ -2124,6 +2154,7 @@ var Bridge = class {
|
|
|
2124
2154
|
}
|
|
2125
2155
|
case "threads.start": {
|
|
2126
2156
|
const project = await this.project(owner, p.projectId);
|
|
2157
|
+
await this.writable(owner, project.cwd);
|
|
2127
2158
|
const r = await this.upstream.call("thread/start", {
|
|
2128
2159
|
cwd: project.cwd,
|
|
2129
2160
|
sandbox: "workspace-write",
|
|
@@ -2237,7 +2268,8 @@ var Bridge = class {
|
|
|
2237
2268
|
throw new BridgeError("APPROVAL_EXPIRED");
|
|
2238
2269
|
if (!g.write) throw new BridgeError("FORBIDDEN");
|
|
2239
2270
|
this.leases.assert(a.thread, owner);
|
|
2240
|
-
await this.thread(owner, a.thread);
|
|
2271
|
+
const thread2 = await this.thread(owner, a.thread);
|
|
2272
|
+
await this.writable(owner, thread2.cwd);
|
|
2241
2273
|
if (this.approvals.get(id2) !== a) throw new BridgeError("APPROVAL_EXPIRED");
|
|
2242
2274
|
this.grant(owner);
|
|
2243
2275
|
this.leases.assert(a.thread, owner);
|
|
@@ -2296,7 +2328,84 @@ var Bridge = class {
|
|
|
2296
2328
|
}
|
|
2297
2329
|
};
|
|
2298
2330
|
|
|
2331
|
+
// src/admin-errors.ts
|
|
2332
|
+
var adminMessages = {
|
|
2333
|
+
TOKEN_PAIRING_AUTOMATIC: "\u4F7F\u7528\u4E0E Client \u76F8\u540C\u7684 Token \u767B\u5F55\u7F51\u9875\u5373\u53EF\uFF0C\u65E0\u9700\u8FD0\u884C pair \u6216 pair-approve\u3002",
|
|
2334
|
+
FINGERPRINT_NOT_FOUND: "\u5F53\u524D\u6307\u7EB9\u4E0D\u5B58\u5728\u3002\u8BF7\u5728\u7F51\u9875\u91CD\u65B0\u8FDE\u63A5\uFF0C\u518D\u8FD0\u884C web-codex-client pair\u3002",
|
|
2335
|
+
BROWSER_NOT_CONFIRMED: "\u8BF7\u5148\u5728\u7F51\u9875\u70B9\u51FB\u201C\u4E0E\u672C\u673A\u663E\u793A\u4E00\u81F4\u201D\uFF0C\u7136\u540E\u91CD\u8BD5\u3002",
|
|
2336
|
+
PAIRING_ROUTE_EXPIRED: "\u6B64\u914D\u5BF9\u5DF2\u8FC7\u671F\u3002\u8BF7\u5728\u7F51\u9875\u91CD\u65B0\u8FDE\u63A5\u5E76\u6838\u5BF9\u65B0\u6307\u7EB9\u3002",
|
|
2337
|
+
PAIRING_WINDOW_EXPIRED: "\u914D\u5BF9\u7A97\u53E3\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FDE\u63A5\u7F51\u9875\u3002",
|
|
2338
|
+
DIRECTORY_UNAVAILABLE: "\u76EE\u5F55\u4E0D\u5B58\u5728\u3001\u4E0D\u662F\u76EE\u5F55\u6216\u5F53\u524D\u7528\u6237\u65E0\u6CD5\u8BBF\u95EE\u3002",
|
|
2339
|
+
GRANT_SAVE_FAILED: "\u6388\u6743\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u72B6\u6001\u76EE\u5F55\u6743\u9650\u548C\u78C1\u76D8\u7A7A\u95F4\u3002",
|
|
2340
|
+
GRANT_LIMIT: "\u6388\u6743\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u5148\u64A4\u9500\u4E0D\u518D\u4F7F\u7528\u7684\u6D4F\u89C8\u5668\u3002",
|
|
2341
|
+
CODEX_UNAVAILABLE: "\u65E0\u6CD5\u8BFB\u53D6\u672C\u673A Codex \u9879\u76EE\uFF0C\u8BF7\u8FD0\u884C web-codex-client doctor\u3002",
|
|
2342
|
+
PROJECT_DISCOVERY_LIMIT: "\u9879\u76EE\u76EE\u5F55\u626B\u63CF\u8FBE\u5230\u4E0A\u9650\uFF0C\u8BF7\u7528 --root \u660E\u786E\u6307\u5B9A\u9879\u76EE\u3002",
|
|
2343
|
+
NO_KNOWN_PROJECTS: "\u6CA1\u6709\u627E\u5230\u53EF\u81EA\u52A8\u6388\u6743\u7684 Codex \u9879\u76EE\u3002\u8BF7\u7528 --root \u6307\u5B9A\u76EE\u5F55\u5E76\u786E\u8BA4\u6388\u6743\u3002",
|
|
2344
|
+
DIRECTORY_APPROVAL_REQUIRED: "\u8BE5\u76EE\u5F55\u4E0D\u662F\u5DF2\u8BC6\u522B\u7684 Codex \u9879\u76EE\uFF1B\u9996\u6B21\u914D\u5BF9\u7528 pair --root \u8DEF\u5F84\uFF0C\u5DF2\u914D\u5BF9\u7528 authorize --root \u8DEF\u5F84\uFF0C\u5728\u672C\u673A\u786E\u8BA4\u3002",
|
|
2345
|
+
PEER_REQUIRED: "\u5B58\u5728\u591A\u4E2A\u6D4F\u89C8\u5668\uFF0C\u8BF7\u7528 --peer \u6307\u5B9A pair-list \u6216 status \u4E2D\u7684\u6D4F\u89C8\u5668\u3002",
|
|
2346
|
+
NOT_PAIRED: "\u5C1A\u672A\u4FE1\u4EFB\u6D4F\u89C8\u5668\uFF0C\u8BF7\u5148\u5728\u7F51\u9875\u8FDE\u63A5\u5E76\u8FD0\u884C web-codex-client pair\u3002",
|
|
2347
|
+
INVALID_ADMIN_REQUEST: "\u547D\u4EE4\u53C2\u6570\u65E0\u6548\uFF0C\u8BF7\u67E5\u770B web-codex-client --help\u3002",
|
|
2348
|
+
ADMIN_REQUEST_REJECTED: "\u672C\u673A\u63A7\u5236\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 Client \u72B6\u6001\u3002"
|
|
2349
|
+
};
|
|
2350
|
+
var AdminError = class extends Error {
|
|
2351
|
+
constructor(code) {
|
|
2352
|
+
super(adminMessages[code]);
|
|
2353
|
+
this.code = code;
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
function adminFailure(error) {
|
|
2357
|
+
const code = error instanceof AdminError ? error.code : error instanceof Error && error.name === "ZodError" || error instanceof SyntaxError ? "INVALID_ADMIN_REQUEST" : "ADMIN_REQUEST_REJECTED";
|
|
2358
|
+
return { error: code, message: adminMessages[code] };
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/known-projects.ts
|
|
2362
|
+
import { realpath as realpath3, stat as stat2 } from "node:fs/promises";
|
|
2363
|
+
import { homedir as homedir2 } from "node:os";
|
|
2364
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
2365
|
+
async function knownProjects(upstream, stateDir) {
|
|
2366
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2367
|
+
async function add(value) {
|
|
2368
|
+
if (typeof value !== "string" || !isAbsolute2(value)) return;
|
|
2369
|
+
try {
|
|
2370
|
+
const root = await realpath3(value);
|
|
2371
|
+
if (!(await stat2(root)).isDirectory() || root === "/" || root === homedir2() || within(root, stateDir)) return;
|
|
2372
|
+
roots.add(root);
|
|
2373
|
+
if (roots.size > 256) throw new AdminError("PROJECT_DISCOVERY_LIMIT");
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
if (error instanceof AdminError) throw error;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
async function scan(method, extra, consume) {
|
|
2379
|
+
let cursor;
|
|
2380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2381
|
+
for (let page2 = 0; page2 < 100; page2++) {
|
|
2382
|
+
const result = await upstream.call(method, { ...extra, limit: 100, cursor });
|
|
2383
|
+
if (!Array.isArray(result.data) || result.data.length > 100) throw new AdminError("CODEX_UNAVAILABLE");
|
|
2384
|
+
for (const row of result.data) await consume(row);
|
|
2385
|
+
if (!result.nextCursor) return;
|
|
2386
|
+
if (typeof result.nextCursor !== "string" || seen.has(result.nextCursor)) throw new AdminError("PROJECT_DISCOVERY_LIMIT");
|
|
2387
|
+
cursor = result.nextCursor;
|
|
2388
|
+
seen.add(result.nextCursor);
|
|
2389
|
+
}
|
|
2390
|
+
throw new AdminError("PROJECT_DISCOVERY_LIMIT");
|
|
2391
|
+
}
|
|
2392
|
+
try {
|
|
2393
|
+
await scan("project/list", {}, async (row) => {
|
|
2394
|
+
for (const root of row.roots ?? []) await add(root.path);
|
|
2395
|
+
});
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
if (!(error instanceof RpcError && error.code === -32601)) throw error;
|
|
2398
|
+
}
|
|
2399
|
+
for (const archived of [false, true]) {
|
|
2400
|
+
await scan("thread/list", { useStateDbOnly: true, archived, sourceKinds: ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview", "subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"] }, async (row) => {
|
|
2401
|
+
await add(row.cwd);
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
return [...roots].sort();
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2299
2407
|
// src/daemon.ts
|
|
2408
|
+
var PAIRING_MS = 5 * 60 * 1e3;
|
|
2300
2409
|
var ClientDaemon = class {
|
|
2301
2410
|
constructor(config, identity, grants, connectUpstream = () => CodexRpc.connect(config.upstream)) {
|
|
2302
2411
|
this.config = config;
|
|
@@ -2310,18 +2419,24 @@ var ClientDaemon = class {
|
|
|
2310
2419
|
stopped = false;
|
|
2311
2420
|
routes = /* @__PURE__ */ new Map();
|
|
2312
2421
|
pairUntil = 0;
|
|
2422
|
+
lastRouteClosure;
|
|
2423
|
+
expiredFingerprints = /* @__PURE__ */ new Set();
|
|
2313
2424
|
admin;
|
|
2425
|
+
adminQueue = Promise.resolve();
|
|
2314
2426
|
timer;
|
|
2315
2427
|
retry;
|
|
2316
2428
|
attempts = 0;
|
|
2317
2429
|
noise;
|
|
2318
2430
|
token = "";
|
|
2431
|
+
relayToken = "";
|
|
2432
|
+
tokenPeers = /* @__PURE__ */ new Set();
|
|
2319
2433
|
alive = true;
|
|
2320
2434
|
connecting;
|
|
2321
2435
|
async start() {
|
|
2322
2436
|
this.noise = await loadNoise();
|
|
2323
2437
|
this.token = (await privateRead(this.config.deviceTokenFile)).trim();
|
|
2324
2438
|
if (!this.token || /[\r\n]/.test(this.token)) throw Error("DEVICE_TOKEN");
|
|
2439
|
+
this.relayToken = await relayCredential(this.token);
|
|
2325
2440
|
await this.startAdmin();
|
|
2326
2441
|
this.connect();
|
|
2327
2442
|
this.timer = setInterval(() => {
|
|
@@ -2333,7 +2448,7 @@ var ClientDaemon = class {
|
|
|
2333
2448
|
}
|
|
2334
2449
|
}
|
|
2335
2450
|
for (const [id2, r] of this.routes)
|
|
2336
|
-
if (Date.now() > r.expires && !r.owner) this.closeRoute(id2);
|
|
2451
|
+
if (Date.now() > r.expires && !r.owner) this.closeRoute(id2, true, "PAIRING_ROUTE_EXPIRED");
|
|
2337
2452
|
}, 1e4);
|
|
2338
2453
|
this.timer.unref();
|
|
2339
2454
|
}
|
|
@@ -2358,7 +2473,7 @@ var ClientDaemon = class {
|
|
|
2358
2473
|
connect() {
|
|
2359
2474
|
if (this.stopped) return;
|
|
2360
2475
|
this.ws = new WebSocket2(new URL("/ws/v1/client", this.config.relayUrl), {
|
|
2361
|
-
headers: { Authorization: "Bearer " + this.token },
|
|
2476
|
+
headers: { Authorization: this.config.pairingMode === "manual" ? "Bearer " + this.token : "TokenHash " + this.relayToken },
|
|
2362
2477
|
perMessageDeflate: false,
|
|
2363
2478
|
maxPayload: 128 * 1024,
|
|
2364
2479
|
handshakeTimeout: 1e4
|
|
@@ -2370,9 +2485,9 @@ var ClientDaemon = class {
|
|
|
2370
2485
|
this.ws.on("pong", () => this.alive = true);
|
|
2371
2486
|
this.ws.on("error", () => {
|
|
2372
2487
|
});
|
|
2373
|
-
this.ws.on("message", (
|
|
2488
|
+
this.ws.on("message", (data2) => {
|
|
2374
2489
|
try {
|
|
2375
|
-
this.receive(
|
|
2490
|
+
this.receive(data2.toString());
|
|
2376
2491
|
} catch {
|
|
2377
2492
|
this.ws?.close(1008, "Protocol violation");
|
|
2378
2493
|
}
|
|
@@ -2406,7 +2521,7 @@ var ClientDaemon = class {
|
|
|
2406
2521
|
}
|
|
2407
2522
|
this.routes.set(m.routeId, {
|
|
2408
2523
|
confirmed: false,
|
|
2409
|
-
expires: Date.now() +
|
|
2524
|
+
expires: Date.now() + PAIRING_MS,
|
|
2410
2525
|
queue: Promise.resolve(),
|
|
2411
2526
|
pending: 0,
|
|
2412
2527
|
out: [],
|
|
@@ -2416,7 +2531,7 @@ var ClientDaemon = class {
|
|
|
2416
2531
|
return;
|
|
2417
2532
|
}
|
|
2418
2533
|
if (m.type === "route.closed") {
|
|
2419
|
-
this.closeRoute(m.routeId, false);
|
|
2534
|
+
this.closeRoute(m.routeId, false, "RELAY_CLOSED");
|
|
2420
2535
|
return;
|
|
2421
2536
|
}
|
|
2422
2537
|
if (m.type !== "secure") return;
|
|
@@ -2448,7 +2563,14 @@ var ClientDaemon = class {
|
|
|
2448
2563
|
if (reply) this.ws.send(encodeFrame(reply));
|
|
2449
2564
|
if (r.session.ready) {
|
|
2450
2565
|
const grant = this.grants.get(r.session.peerPublicKey);
|
|
2451
|
-
if (
|
|
2566
|
+
if (this.config.pairingMode !== "manual") {
|
|
2567
|
+
this.send(frame.routeId, {
|
|
2568
|
+
kind: "pairing",
|
|
2569
|
+
status: "pending",
|
|
2570
|
+
fingerprint: r.session.fingerprint,
|
|
2571
|
+
proof: await tokenProof(this.token, r.session.fingerprint, "client")
|
|
2572
|
+
});
|
|
2573
|
+
} else if (grant) {
|
|
2452
2574
|
r.session.trust(r.session.fingerprint);
|
|
2453
2575
|
r.owner = grant.peer;
|
|
2454
2576
|
this.send(frame.routeId, {
|
|
@@ -2457,8 +2579,6 @@ var ClientDaemon = class {
|
|
|
2457
2579
|
fingerprint: r.session.fingerprint
|
|
2458
2580
|
});
|
|
2459
2581
|
} else {
|
|
2460
|
-
if (Date.now() > this.pairUntil)
|
|
2461
|
-
throw new BridgeError("NOT_PAIRED");
|
|
2462
2582
|
this.send(frame.routeId, {
|
|
2463
2583
|
kind: "pairing",
|
|
2464
2584
|
status: "pending",
|
|
@@ -2471,28 +2591,47 @@ var ClientDaemon = class {
|
|
|
2471
2591
|
const message = r.session.open(frame);
|
|
2472
2592
|
if (!message) return;
|
|
2473
2593
|
if (message.kind === "pairing") {
|
|
2474
|
-
if (message.fingerprint !== r.session.fingerprint || message.status !== "confirmed" || Date.now() > r.expires)
|
|
2594
|
+
if (r.confirmed || message.fingerprint !== r.session.fingerprint || message.status !== "confirmed" || Date.now() > r.expires)
|
|
2475
2595
|
throw Error("PAIRING_MISMATCH");
|
|
2596
|
+
if (this.config.pairingMode !== "manual") {
|
|
2597
|
+
if (!await verifyTokenProof(this.token, r.session.fingerprint, "browser", message.proof)) throw new BridgeError("TOKEN_AUTH_FAILED");
|
|
2598
|
+
const roots = await this.discoverProjects();
|
|
2599
|
+
if (this.routes.get(frame.routeId) !== r || Date.now() > r.expires) return;
|
|
2600
|
+
const peer = r.session.peerPublicKey;
|
|
2601
|
+
this.grants.set(peer, { peer, roots, write: true, writeRoots: roots });
|
|
2602
|
+
this.tokenPeers.add(peer);
|
|
2603
|
+
r.session.trust(r.session.fingerprint);
|
|
2604
|
+
r.owner = peer;
|
|
2605
|
+
this.send(frame.routeId, { kind: "pairing", status: "ready", fingerprint: r.session.fingerprint });
|
|
2606
|
+
}
|
|
2476
2607
|
r.confirmed = true;
|
|
2477
2608
|
return;
|
|
2478
2609
|
}
|
|
2479
2610
|
if (!r.owner || !this.grants.has(r.owner))
|
|
2480
2611
|
throw new BridgeError("NOT_PAIRED");
|
|
2481
|
-
|
|
2612
|
+
try {
|
|
2613
|
+
await this.ensureUpstream();
|
|
2614
|
+
} catch {
|
|
2615
|
+
if (message.kind === "request") {
|
|
2616
|
+
this.send(frame.routeId, { kind: "response", requestId: message.requestId, error: { code: "UPSTREAM_UNAVAILABLE", message: "\u672C\u673A Codex \u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u8FD0\u884C web-codex-client doctor \u540E\u91CD\u8BD5\u3002", retryable: true } });
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
throw new BridgeError("UPSTREAM_UNAVAILABLE");
|
|
2620
|
+
}
|
|
2482
2621
|
const response = await this.bridge.handle(r.owner, message);
|
|
2483
2622
|
if (response) this.send(frame.routeId, response);
|
|
2484
|
-
}).catch(() => this.closeRoute(frame.routeId)).finally(() => r.pending--);
|
|
2623
|
+
}).catch((error) => this.closeRoute(frame.routeId, true, error instanceof BridgeError ? error.code : "PROTOCOL_ERROR")).finally(() => r.pending--);
|
|
2485
2624
|
}
|
|
2486
2625
|
send(id2, message) {
|
|
2487
2626
|
const r = this.routes.get(id2);
|
|
2488
2627
|
if (!r?.session?.ready) return;
|
|
2489
|
-
const
|
|
2490
|
-
if (r.bytes +
|
|
2628
|
+
const bytes2 = Buffer.byteLength(JSON.stringify(message));
|
|
2629
|
+
if (r.bytes + bytes2 > 1024 * 1024 || this.ws.bufferedAmount > 4 * 1024 * 1024) {
|
|
2491
2630
|
this.closeRoute(id2);
|
|
2492
2631
|
return;
|
|
2493
2632
|
}
|
|
2494
2633
|
r.out.push(message);
|
|
2495
|
-
r.bytes +=
|
|
2634
|
+
r.bytes += bytes2;
|
|
2496
2635
|
if (!r.sending) void this.flush(id2, r);
|
|
2497
2636
|
}
|
|
2498
2637
|
async flush(id2, r) {
|
|
@@ -2518,14 +2657,21 @@ var ClientDaemon = class {
|
|
|
2518
2657
|
r.sending = false;
|
|
2519
2658
|
}
|
|
2520
2659
|
}
|
|
2521
|
-
closeRoute(id2, notify = true) {
|
|
2660
|
+
closeRoute(id2, notify = true, reason = "CLOSED") {
|
|
2522
2661
|
const r = this.routes.get(id2);
|
|
2523
2662
|
if (!r) return;
|
|
2663
|
+
this.lastRouteClosure = { at: Date.now(), reason };
|
|
2664
|
+
if (reason === "PAIRING_ROUTE_EXPIRED" && r.session?.fingerprint) {
|
|
2665
|
+
if (this.expiredFingerprints.size >= 64) this.expiredFingerprints.delete(this.expiredFingerprints.values().next().value);
|
|
2666
|
+
this.expiredFingerprints.add(r.session.fingerprint);
|
|
2667
|
+
}
|
|
2524
2668
|
this.routes.delete(id2);
|
|
2525
2669
|
r.session?.close();
|
|
2526
2670
|
r.out = [];
|
|
2527
|
-
if (r.owner && !Array.from(this.routes.values()).some((x) => x.owner === r.owner))
|
|
2671
|
+
if (r.owner && !Array.from(this.routes.values()).some((x) => x.owner === r.owner)) {
|
|
2528
2672
|
this.bridge?.disconnect(r.owner);
|
|
2673
|
+
if (this.tokenPeers.delete(r.owner)) this.grants.delete(r.owner);
|
|
2674
|
+
}
|
|
2529
2675
|
if (notify) this.control({ type: "route.close", routeId: id2 });
|
|
2530
2676
|
}
|
|
2531
2677
|
async adminCommand(value) {
|
|
@@ -2541,50 +2687,66 @@ var ClientDaemon = class {
|
|
|
2541
2687
|
authorizedSessions: Array.from(this.routes.values()).filter(
|
|
2542
2688
|
(r) => r.owner
|
|
2543
2689
|
).length,
|
|
2544
|
-
pairingUntil: this.pairUntil
|
|
2690
|
+
pairingUntil: this.pairUntil,
|
|
2691
|
+
pairingMode: this.config.pairingMode === "manual" ? "local-confirmation" : "token",
|
|
2692
|
+
pairingRemainingMs: Math.max(0, this.pairUntil - Date.now()),
|
|
2693
|
+
lastRouteClosure: this.lastRouteClosure,
|
|
2694
|
+
grants: Array.from(this.grants.values()).map((g) => ({ peer: g.peer, rootCount: g.roots.length, write: g.write }))
|
|
2545
2695
|
};
|
|
2546
2696
|
case "pair-open":
|
|
2547
|
-
this.pairUntil = Date.now() +
|
|
2697
|
+
this.pairUntil = Date.now() + PAIRING_MS;
|
|
2698
|
+
for (const r of this.routes.values()) if (!r.owner) r.expires = this.pairUntil;
|
|
2548
2699
|
return { expiresAt: this.pairUntil };
|
|
2549
2700
|
case "pair-list":
|
|
2550
2701
|
return {
|
|
2702
|
+
pairingMode: this.config.pairingMode === "manual" ? "local-confirmation" : "token",
|
|
2703
|
+
windowRequired: false,
|
|
2704
|
+
pairingUntil: this.pairUntil,
|
|
2705
|
+
pairingRemainingMs: Math.max(0, this.pairUntil - Date.now()),
|
|
2551
2706
|
pending: Array.from(this.routes.entries()).filter(([, r]) => r.session?.ready && !r.owner).map(([routeId, r]) => ({
|
|
2552
2707
|
routeId,
|
|
2553
2708
|
fingerprint: r.session.fingerprint,
|
|
2554
2709
|
peer: r.session.peerPublicKey,
|
|
2555
2710
|
browserConfirmed: r.confirmed,
|
|
2556
|
-
expiresAt: r.expires
|
|
2711
|
+
expiresAt: r.expires,
|
|
2712
|
+
remainingMs: Math.max(0, r.expires - Date.now()),
|
|
2713
|
+
canApprove: r.confirmed && Date.now() <= r.expires
|
|
2557
2714
|
}))
|
|
2558
2715
|
};
|
|
2559
2716
|
case "pair-approve": {
|
|
2717
|
+
if (this.config.pairingMode !== "manual") throw new AdminError("TOKEN_PAIRING_AUTOMATIC");
|
|
2560
2718
|
const p = z5.object({
|
|
2561
2719
|
fingerprint: z5.string().regex(/^[a-f0-9]{64}$/),
|
|
2562
|
-
roots: z5.array(z5.string()).min(1).max(32),
|
|
2563
|
-
write: z5.boolean().
|
|
2720
|
+
roots: z5.array(z5.string()).min(1).max(32).optional(),
|
|
2721
|
+
write: z5.boolean().optional(),
|
|
2722
|
+
allowUnknown: z5.boolean().default(false)
|
|
2564
2723
|
}).parse(m);
|
|
2565
2724
|
const found = Array.from(this.routes.entries()).find(
|
|
2566
2725
|
([, r2]) => r2.session?.fingerprint === p.fingerprint
|
|
2567
2726
|
);
|
|
2568
|
-
if (!found
|
|
2569
|
-
throw Error("PAIRING_EXPIRED_OR_UNCONFIRMED");
|
|
2570
|
-
if (this.grants.size >= 256) throw Error("GRANT_LIMIT");
|
|
2727
|
+
if (!found) throw new AdminError(this.expiredFingerprints.has(p.fingerprint) ? "PAIRING_ROUTE_EXPIRED" : "FINGERPRINT_NOT_FOUND");
|
|
2571
2728
|
const [id2, r] = found;
|
|
2729
|
+
if (r.owner) return { peer: r.owner, approved: true, alreadyApproved: true };
|
|
2730
|
+
if (Date.now() > r.expires) throw new AdminError("PAIRING_ROUTE_EXPIRED");
|
|
2731
|
+
if (!r.confirmed) throw new AdminError("BROWSER_NOT_CONFIRMED");
|
|
2732
|
+
if (this.grants.size >= 256) throw new AdminError("GRANT_LIMIT");
|
|
2572
2733
|
const peer = r.session.peerPublicKey;
|
|
2734
|
+
const selectedRoots = p.roots ? await this.canonicalRoots(p.roots) : void 0;
|
|
2735
|
+
let known = [];
|
|
2736
|
+
if (p.write === void 0 || !p.roots) known = await this.discoverProjects();
|
|
2737
|
+
const roots = selectedRoots ?? known;
|
|
2738
|
+
if (!roots.length) throw new AdminError("NO_KNOWN_PROJECTS");
|
|
2739
|
+
const unknown = roots.filter((root) => !known.includes(root));
|
|
2740
|
+
if (unknown.length && p.write === void 0 && !p.allowUnknown) throw new AdminError("DIRECTORY_APPROVAL_REQUIRED");
|
|
2741
|
+
const writeRoots = p.write === false ? [] : p.write || p.allowUnknown ? roots : roots.filter((root) => known.includes(root));
|
|
2742
|
+
if (this.routes.get(id2) !== r || Date.now() > r.expires) throw new AdminError("PAIRING_ROUTE_EXPIRED");
|
|
2573
2743
|
const grant = {
|
|
2574
2744
|
peer,
|
|
2575
|
-
roots
|
|
2576
|
-
write:
|
|
2745
|
+
roots,
|
|
2746
|
+
write: writeRoots.length > 0,
|
|
2747
|
+
writeRoots
|
|
2577
2748
|
};
|
|
2578
|
-
this.
|
|
2579
|
-
try {
|
|
2580
|
-
await atomicPrivate(
|
|
2581
|
-
this.config.stateDir + "/grants.json",
|
|
2582
|
-
Array.from(this.grants.values())
|
|
2583
|
-
);
|
|
2584
|
-
} catch (e) {
|
|
2585
|
-
this.grants.delete(peer);
|
|
2586
|
-
throw e;
|
|
2587
|
-
}
|
|
2749
|
+
await this.saveGrant(grant, () => this.routes.get(id2) === r && Date.now() <= r.expires);
|
|
2588
2750
|
r.session.trust(p.fingerprint);
|
|
2589
2751
|
r.owner = peer;
|
|
2590
2752
|
this.pairUntil = 0;
|
|
@@ -2593,7 +2755,24 @@ var ClientDaemon = class {
|
|
|
2593
2755
|
status: "ready",
|
|
2594
2756
|
fingerprint: p.fingerprint
|
|
2595
2757
|
});
|
|
2596
|
-
return { peer, approved: true };
|
|
2758
|
+
return { peer, approved: true, roots, write: grant.write, writeRoots };
|
|
2759
|
+
}
|
|
2760
|
+
case "projects-known":
|
|
2761
|
+
return { roots: await this.discoverProjects() };
|
|
2762
|
+
case "authorize": {
|
|
2763
|
+
const p = z5.object({ peer: z5.string().optional(), roots: z5.array(z5.string()).min(1).max(32), allowUnknown: z5.boolean().default(false) }).parse(m);
|
|
2764
|
+
const activePeers = [...new Set([...this.routes.values()].map((r) => r.owner).filter((peer2) => !!peer2))];
|
|
2765
|
+
const peer = p.peer ?? (activePeers.length === 1 ? activePeers[0] : this.grants.size === 1 ? this.grants.keys().next().value : void 0);
|
|
2766
|
+
if (!peer) throw new AdminError(this.grants.size ? "PEER_REQUIRED" : "NOT_PAIRED");
|
|
2767
|
+
const old = this.grants.get(peer);
|
|
2768
|
+
if (!old) throw new AdminError("NOT_PAIRED");
|
|
2769
|
+
const roots = await this.canonicalRoots(p.roots), known = await this.discoverProjects();
|
|
2770
|
+
if (!p.allowUnknown && roots.some((root) => !known.includes(root))) throw new AdminError("DIRECTORY_APPROVAL_REQUIRED");
|
|
2771
|
+
const nextRoots = [.../* @__PURE__ */ new Set([...old.roots, ...roots])];
|
|
2772
|
+
if (nextRoots.length > 256) throw new AdminError("GRANT_LIMIT");
|
|
2773
|
+
const writeRoots = [.../* @__PURE__ */ new Set([...old.writeRoots ?? (old.write ? old.roots : []), ...roots])];
|
|
2774
|
+
await this.saveGrant({ ...old, roots: nextRoots, write: true, writeRoots });
|
|
2775
|
+
return { peer, approved: true, roots: nextRoots, writeRoots };
|
|
2597
2776
|
}
|
|
2598
2777
|
case "revoke": {
|
|
2599
2778
|
const peer = z5.string().parse(m.peer);
|
|
@@ -2609,9 +2788,47 @@ var ClientDaemon = class {
|
|
|
2609
2788
|
return { revoked: true };
|
|
2610
2789
|
}
|
|
2611
2790
|
default:
|
|
2612
|
-
throw
|
|
2791
|
+
throw new AdminError("INVALID_ADMIN_REQUEST");
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
async canonicalRoots(roots) {
|
|
2795
|
+
try {
|
|
2796
|
+
return await Promise.all(roots.map(async (path) => {
|
|
2797
|
+
const root = await realpath4(path);
|
|
2798
|
+
if (!(await stat3(root)).isDirectory()) throw Error();
|
|
2799
|
+
return root;
|
|
2800
|
+
}));
|
|
2801
|
+
} catch {
|
|
2802
|
+
throw new AdminError("DIRECTORY_UNAVAILABLE");
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
async discoverProjects() {
|
|
2806
|
+
try {
|
|
2807
|
+
await this.ensureUpstream();
|
|
2808
|
+
return await knownProjects(this.bridge.upstream, this.config.stateDir);
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
if (error instanceof AdminError) throw error;
|
|
2811
|
+
throw new AdminError("CODEX_UNAVAILABLE");
|
|
2613
2812
|
}
|
|
2614
2813
|
}
|
|
2814
|
+
async saveGrant(grant, stillValid) {
|
|
2815
|
+
const values = new Map(this.grants);
|
|
2816
|
+
values.set(grant.peer, grant);
|
|
2817
|
+
try {
|
|
2818
|
+
await atomicPrivate(this.config.stateDir + "/grants.json", [...values.values()]);
|
|
2819
|
+
} catch {
|
|
2820
|
+
throw new AdminError("GRANT_SAVE_FAILED");
|
|
2821
|
+
}
|
|
2822
|
+
if (stillValid && !stillValid()) {
|
|
2823
|
+
try {
|
|
2824
|
+
await atomicPrivate(this.config.stateDir + "/grants.json", [...this.grants.values()]);
|
|
2825
|
+
} catch {
|
|
2826
|
+
throw new AdminError("GRANT_SAVE_FAILED");
|
|
2827
|
+
}
|
|
2828
|
+
throw new AdminError("PAIRING_ROUTE_EXPIRED");
|
|
2829
|
+
}
|
|
2830
|
+
this.grants.set(grant.peer, grant);
|
|
2831
|
+
}
|
|
2615
2832
|
async startAdmin() {
|
|
2616
2833
|
const path = this.config.stateDir + "/control.sock";
|
|
2617
2834
|
try {
|
|
@@ -2641,20 +2858,27 @@ var ClientDaemon = class {
|
|
|
2641
2858
|
if (e.code !== "ENOENT") throw e;
|
|
2642
2859
|
}
|
|
2643
2860
|
this.admin = createServer((socket) => {
|
|
2644
|
-
socket.setTimeout(
|
|
2645
|
-
let
|
|
2861
|
+
socket.setTimeout(6e4, () => socket.destroy());
|
|
2862
|
+
let data2 = "";
|
|
2646
2863
|
socket.on("data", (chunk) => {
|
|
2647
|
-
|
|
2648
|
-
if (
|
|
2864
|
+
data2 += chunk.toString();
|
|
2865
|
+
if (data2.length > 16384) {
|
|
2649
2866
|
socket.destroy();
|
|
2650
2867
|
return;
|
|
2651
2868
|
}
|
|
2652
|
-
if (!
|
|
2869
|
+
if (!data2.includes("\n")) return;
|
|
2653
2870
|
socket.pause();
|
|
2654
|
-
void Promise.resolve().then(() =>
|
|
2871
|
+
void Promise.resolve().then(() => {
|
|
2872
|
+
const request = JSON.parse(data2);
|
|
2873
|
+
const result = this.adminQueue.then(() => this.adminCommand(request));
|
|
2874
|
+
this.adminQueue = result.then(() => {
|
|
2875
|
+
}, () => {
|
|
2876
|
+
});
|
|
2877
|
+
return result;
|
|
2878
|
+
}).then(
|
|
2655
2879
|
(result) => socket.end(JSON.stringify({ result }) + "\n"),
|
|
2656
|
-
() => socket.end(
|
|
2657
|
-
JSON.stringify(
|
|
2880
|
+
(error) => socket.end(
|
|
2881
|
+
JSON.stringify(adminFailure(error)) + "\n"
|
|
2658
2882
|
)
|
|
2659
2883
|
);
|
|
2660
2884
|
});
|
|
@@ -2689,17 +2913,17 @@ import { join as join2 } from "node:path";
|
|
|
2689
2913
|
function control(stateDir, request) {
|
|
2690
2914
|
return new Promise((resolve5, reject) => {
|
|
2691
2915
|
const socket = createConnection2(join2(stateDir, "control.sock"));
|
|
2692
|
-
let
|
|
2693
|
-
socket.setTimeout(
|
|
2916
|
+
let data2 = "";
|
|
2917
|
+
socket.setTimeout(6e4, () => socket.destroy(Error("ADMIN_TIMEOUT: \u672C\u673A\u5904\u7406\u8D85\u65F6\uFF0C\u8BF7\u8FD0\u884C doctor \u540E\u91CD\u8BD5")));
|
|
2694
2918
|
socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
|
|
2695
2919
|
socket.on("data", (chunk) => {
|
|
2696
|
-
|
|
2697
|
-
if (Buffer.byteLength(
|
|
2920
|
+
data2 += chunk.toString();
|
|
2921
|
+
if (Buffer.byteLength(data2) > 65536) socket.destroy(Error("ADMIN_TOO_LARGE"));
|
|
2698
2922
|
});
|
|
2699
2923
|
socket.on("error", reject);
|
|
2700
2924
|
socket.on("end", () => {
|
|
2701
2925
|
try {
|
|
2702
|
-
resolve5(JSON.parse(
|
|
2926
|
+
resolve5(JSON.parse(data2));
|
|
2703
2927
|
} catch {
|
|
2704
2928
|
reject(Error("ADMIN_INVALID_RESPONSE"));
|
|
2705
2929
|
}
|
|
@@ -2708,14 +2932,14 @@ function control(stateDir, request) {
|
|
|
2708
2932
|
}
|
|
2709
2933
|
|
|
2710
2934
|
// src/setup.ts
|
|
2711
|
-
import { homedir as
|
|
2935
|
+
import { homedir as homedir3 } from "node:os";
|
|
2712
2936
|
import { dirname as dirname3, join as join3, resolve as resolve3, basename as basename3 } from "node:path";
|
|
2713
2937
|
import { mkdir as mkdir2, lstat as lstat3, writeFile as writeFile2, unlink as unlink2 } from "node:fs/promises";
|
|
2714
2938
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
2715
2939
|
import { createInterface } from "node:readline/promises";
|
|
2716
2940
|
import { Writable } from "node:stream";
|
|
2717
2941
|
function defaultConfigPath() {
|
|
2718
|
-
return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(
|
|
2942
|
+
return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(homedir3(), ".config"), "web-codex-client", "config.json"));
|
|
2719
2943
|
}
|
|
2720
2944
|
function validateToken(value) {
|
|
2721
2945
|
const token = value.trim();
|
|
@@ -2756,6 +2980,13 @@ async function question(prompt, secret = false) {
|
|
|
2756
2980
|
async function saveConfiguration(path, relayUrl, token, socket) {
|
|
2757
2981
|
path = resolve3(path);
|
|
2758
2982
|
const directory = dirname3(path);
|
|
2983
|
+
try {
|
|
2984
|
+
const url = new URL(relayUrl);
|
|
2985
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
2986
|
+
relayUrl = url.toString();
|
|
2987
|
+
} catch {
|
|
2988
|
+
throw Error("RELAY_URL_INVALID: use an HTTPS or WSS server address");
|
|
2989
|
+
}
|
|
2759
2990
|
const parsed = configSchema.safeParse({ relayUrl, deviceTokenFile: "placeholder", stateDir: "./state", cryptoPolicy: "remote-wss-v1", upstream: { mode: "unix", ...socket ? { socket: resolve3(socket) } : {} } });
|
|
2760
2991
|
if (!parsed.success) throw Error("RELAY_URL_INVALID: use a WSS origin without path, credentials, query or fragment");
|
|
2761
2992
|
token = validateToken(token);
|
|
@@ -2801,10 +3032,11 @@ async function saveConfiguration(path, relayUrl, token, socket) {
|
|
|
2801
3032
|
import { join as join4, dirname as dirname4 } from "node:path";
|
|
2802
3033
|
import WebSocket3 from "ws";
|
|
2803
3034
|
import { SingleBar, Presets } from "cli-progress";
|
|
2804
|
-
async function authenticateRelay(relayUrl, token) {
|
|
3035
|
+
async function authenticateRelay(relayUrl, token, manual = false) {
|
|
3036
|
+
const credential = manual ? "Bearer " + token : "TokenHash " + await relayCredential(token);
|
|
2805
3037
|
await new Promise((resolve5, reject) => {
|
|
2806
3038
|
const ws = new WebSocket3(new URL("/ws/v1/client", relayUrl), {
|
|
2807
|
-
headers: { Authorization:
|
|
3039
|
+
headers: { Authorization: credential },
|
|
2808
3040
|
perMessageDeflate: false,
|
|
2809
3041
|
maxPayload: 4096,
|
|
2810
3042
|
handshakeTimeout: 1e4
|
|
@@ -2825,9 +3057,9 @@ async function authenticateRelay(relayUrl, token) {
|
|
|
2825
3057
|
finish(Error(response.statusCode === 401 ? "RELAY_UNAUTHORIZED: replace the device token" : "RELAY_HTTP_REJECTED: check the reverse proxy"));
|
|
2826
3058
|
});
|
|
2827
3059
|
ws.on("close", () => finish(Error("RELAY_CLOSED")));
|
|
2828
|
-
ws.on("message", (
|
|
3060
|
+
ws.on("message", (bytes2) => {
|
|
2829
3061
|
try {
|
|
2830
|
-
const frame = JSON.parse(
|
|
3062
|
+
const frame = JSON.parse(bytes2.toString());
|
|
2831
3063
|
if (frame.type === "device.ready" && typeof frame.deviceId === "string") finish();
|
|
2832
3064
|
else finish(Error("RELAY_PROTOCOL"));
|
|
2833
3065
|
} catch {
|
|
@@ -2874,7 +3106,7 @@ async function doctor(configPath, quiet = false, reportPath = join4(dirname4(con
|
|
|
2874
3106
|
} catch (e) {
|
|
2875
3107
|
if (!["ENOENT", "ECONNREFUSED"].includes(e.code ?? "")) throw e;
|
|
2876
3108
|
}
|
|
2877
|
-
await authenticateRelay(config.relayUrl, token);
|
|
3109
|
+
await authenticateRelay(config.relayUrl, token, config.pairingMode === "manual");
|
|
2878
3110
|
});
|
|
2879
3111
|
await check("Codex initialization (no model request)", async () => {
|
|
2880
3112
|
const rpc = await CodexRpc.connect(config.upstream);
|
|
@@ -2891,37 +3123,39 @@ async function doctor(configPath, quiet = false, reportPath = join4(dirname4(con
|
|
|
2891
3123
|
}
|
|
2892
3124
|
|
|
2893
3125
|
// src/cli.ts
|
|
2894
|
-
var { values, positionals } = parseArgs({
|
|
2895
|
-
allowPositionals: true,
|
|
2896
|
-
options: {
|
|
2897
|
-
config: { type: "string" },
|
|
2898
|
-
state: { type: "string" },
|
|
2899
|
-
socket: { type: "string" },
|
|
2900
|
-
binary: { type: "string" },
|
|
2901
|
-
standalone: { type: "boolean" },
|
|
2902
|
-
transport: { type: "string" },
|
|
2903
|
-
"codex-home": { type: "string" },
|
|
2904
|
-
fingerprint: { type: "string" },
|
|
2905
|
-
peer: { type: "string" },
|
|
2906
|
-
root: { type: "string", multiple: true },
|
|
2907
|
-
write: { type: "boolean" },
|
|
2908
|
-
quiet: { type: "boolean" },
|
|
2909
|
-
report: { type: "string" },
|
|
2910
|
-
relay: { type: "string" },
|
|
2911
|
-
"token-stdin": { type: "boolean" },
|
|
2912
|
-
help: { type: "boolean", short: "h" },
|
|
2913
|
-
version: { type: "boolean", short: "v" }
|
|
2914
|
-
}
|
|
2915
|
-
});
|
|
2916
|
-
var command = positionals[0] ?? "help";
|
|
2917
3126
|
async function main() {
|
|
3127
|
+
const { values, positionals } = parseArgs({
|
|
3128
|
+
allowPositionals: true,
|
|
3129
|
+
options: {
|
|
3130
|
+
config: { type: "string" },
|
|
3131
|
+
state: { type: "string" },
|
|
3132
|
+
socket: { type: "string" },
|
|
3133
|
+
binary: { type: "string" },
|
|
3134
|
+
standalone: { type: "boolean" },
|
|
3135
|
+
transport: { type: "string" },
|
|
3136
|
+
"codex-home": { type: "string" },
|
|
3137
|
+
fingerprint: { type: "string" },
|
|
3138
|
+
peer: { type: "string" },
|
|
3139
|
+
root: { type: "string", multiple: true },
|
|
3140
|
+
write: { type: "boolean" },
|
|
3141
|
+
"read-only": { type: "boolean" },
|
|
3142
|
+
quiet: { type: "boolean" },
|
|
3143
|
+
report: { type: "string" },
|
|
3144
|
+
relay: { type: "string" },
|
|
3145
|
+
"token-stdin": { type: "boolean" },
|
|
3146
|
+
help: { type: "boolean", short: "h" },
|
|
3147
|
+
version: { type: "boolean", short: "v" }
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
const command = positionals[0] ?? "help";
|
|
3151
|
+
if (values.write && values["read-only"]) throw Error("\u4E0D\u8981\u540C\u65F6\u4F7F\u7528 --write \u548C --read-only\uFF0C\u8BF7\u67E5\u770B --help\u3002");
|
|
2918
3152
|
if (values.version) {
|
|
2919
3153
|
console.log(JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
2920
3154
|
return;
|
|
2921
3155
|
}
|
|
2922
3156
|
if (command === "help" || values.help) {
|
|
2923
3157
|
console.log(
|
|
2924
|
-
"web-codex-client configure [--config FILE] [--relay
|
|
3158
|
+
"web-codex-client configure [--config FILE] [--relay https://HOST --token-stdin]\nweb-codex-client run [--config FILE]\nweb-codex-client status [--config FILE]\nweb-codex-client doctor [--config FILE] [--quiet]\n\n\u914D\u7F6E\u670D\u52A1\u7AEF\u5730\u5740\u548C Token\uFF0C\u8FD0\u884C Client\uFF1B\u7F51\u9875\u4F7F\u7528\u540C\u4E00\u4E2A Token \u5373\u53EF\u8BBF\u95EE\u9879\u76EE\u4E0E\u5BF9\u8BDD\u3002\nDefault config: " + defaultConfigPath()
|
|
2925
3159
|
);
|
|
2926
3160
|
return;
|
|
2927
3161
|
}
|
|
@@ -2929,12 +3163,12 @@ async function main() {
|
|
|
2929
3163
|
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") throw Error("TLS_VERIFICATION_REQUIRED: remove NODE_TLS_REJECT_UNAUTHORIZED=0");
|
|
2930
3164
|
const configPath = resolve4(values.config ?? defaultConfigPath());
|
|
2931
3165
|
if (command === "configure") {
|
|
2932
|
-
const relay = values.relay ?? (await question("
|
|
3166
|
+
const relay = values.relay ?? (await question("\u670D\u52A1\u7AEF\u5730\u5740\uFF08https:// \u6216 wss://\uFF09\uFF1A")).trim();
|
|
2933
3167
|
if (values["token-stdin"] && !values.relay) throw Error("--token-stdin requires --relay");
|
|
2934
|
-
const socket = values.socket
|
|
2935
|
-
const token = values["token-stdin"] ? await tokenFromStdin() : await question("
|
|
3168
|
+
const socket = values.socket;
|
|
3169
|
+
const token = values["token-stdin"] ? await tokenFromStdin() : await question("\u8FDE\u63A5 Token\uFF08\u9690\u85CF\u8F93\u5165\uFF09\uFF1A", true);
|
|
2936
3170
|
const result2 = await saveConfiguration(configPath, relay, token, socket);
|
|
2937
|
-
console.log("Configuration saved: " + result2.path + "\
|
|
3171
|
+
console.log("Configuration saved: " + result2.path + "\n\u8FD0\u884C web-codex-client run\uFF0C\u7136\u540E\u5728\u7F51\u9875\u8F93\u5165\u540C\u4E00\u4E2A Token \u5373\u53EF\u4F7F\u7528\u3002");
|
|
2938
3172
|
return;
|
|
2939
3173
|
}
|
|
2940
3174
|
if (command === "doctor") {
|
|
@@ -2967,7 +3201,7 @@ async function main() {
|
|
|
2967
3201
|
const daemon = new ClientDaemon(config, state.identity, state.grants);
|
|
2968
3202
|
await daemon.start();
|
|
2969
3203
|
console.log(
|
|
2970
|
-
"Client running
|
|
3204
|
+
"Client running. \u7F51\u9875\u8F93\u5165\u76F8\u540C Token \u5373\u53EF\u67E5\u770B\u9879\u76EE\u3001\u4F1A\u8BDD\u5E76\u53D1\u9001\u6D88\u606F\u3002"
|
|
2971
3205
|
);
|
|
2972
3206
|
let stopping = false;
|
|
2973
3207
|
const stop = () => {
|
|
@@ -2979,21 +3213,59 @@ async function main() {
|
|
|
2979
3213
|
process.on("SIGTERM", stop);
|
|
2980
3214
|
return;
|
|
2981
3215
|
}
|
|
2982
|
-
if (!["status", "pair-open", "pair-list", "pair-approve", "revoke"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
|
|
3216
|
+
if (!["status", "pair", "pair-open", "pair-list", "pair-approve", "revoke", "authorize", "projects-known"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
|
|
2983
3217
|
const stateDir = values.state ? resolve4(values.state) : (await readConfig(configPath)).stateDir;
|
|
3218
|
+
const send = async (request2) => {
|
|
3219
|
+
const reply = await control(stateDir, request2);
|
|
3220
|
+
if (reply.error) throw Error(reply.error + ": " + (reply.message ?? "\u672C\u673A Client \u62D2\u7EDD\u8BF7\u6C42\uFF0C\u8BF7\u68C0\u67E5 pair-list \u5E76\u786E\u8BA4 run \u4F7F\u7528\u540C\u4E00\u914D\u7F6E\u3002"));
|
|
3221
|
+
return reply.result;
|
|
3222
|
+
};
|
|
3223
|
+
if (command === "pair") {
|
|
3224
|
+
const list = await send({ command: "pair-list" });
|
|
3225
|
+
if (list.pairingMode === "token") {
|
|
3226
|
+
console.log("\u7F51\u9875\u8F93\u5165\u4E0E Client \u76F8\u540C\u7684 Token \u5373\u53EF\uFF0C\u65E0\u9700\u914D\u5BF9\u547D\u4EE4\u3002");
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
const pending2 = list.pending.filter((p) => (p.remainingMs ?? p.expiresAt - Date.now()) > 0);
|
|
3230
|
+
if (!pending2.length) throw Error("NO_PENDING_BROWSER: \u8BF7\u5148\u5728\u7F51\u9875\u9009\u62E9\u8BBE\u5907\u6216\u91CD\u65B0\u8FDE\u63A5\uFF0C\u7136\u540E\u518D\u6B21\u8FD0\u884C pair\u3002\u65E0\u9700\u5148\u6267\u884C pair-open\u3002");
|
|
3231
|
+
for (let index = 0; index < pending2.length; index++) console.log(`${index + 1}. ${pending2[index].fingerprint} \u7F51\u9875\u786E\u8BA4\uFF1A${pending2[index].browserConfirmed ? "\u662F" : "\u5426"}`);
|
|
3232
|
+
const selected = pending2.length === 1 ? pending2[0] : pending2[Number(await question("\u9009\u62E9\u6D4F\u89C8\u5668\u7F16\u53F7\uFF1A")) - 1];
|
|
3233
|
+
if (!selected) throw Error("INVALID_SELECTION");
|
|
3234
|
+
console.log("\u8BF7\u6838\u5BF9\u7F51\u9875\u4E2D\u7684\u5B8C\u6574\u6307\u7EB9\uFF0C\u5E76\u5148\u70B9\u51FB\u7F51\u9875\u7684\u201C\u4E0E\u672C\u673A\u663E\u793A\u4E00\u81F4\u201D\u3002");
|
|
3235
|
+
const access = values["read-only"] ? "\u53EA\u8BFB" : "\u8BFB\u5199";
|
|
3236
|
+
console.log(values.root ? `\u5C06\u6388\u6743\u6307\u5B9A\u76EE\u5F55${access}\uFF1A` + values.root.map((root) => resolve4(root)).join(", ") : `\u786E\u8BA4\u540E\uFF0C\u4EC5\u5BF9\u672C\u673A Codex \u5DF2\u6709\u9879\u76EE\u6388\u4E88${access}\u6743\u9650\u3002`);
|
|
3237
|
+
if ((await question("\u6307\u7EB9\u5B8C\u5168\u4E00\u81F4\u5E76\u4FE1\u4EFB\u6B64\u6D4F\u89C8\u5668\uFF1F\u8F93\u5165 yes\uFF1A")).trim().toLowerCase() !== "yes") throw Error("PAIRING_CANCELLED");
|
|
3238
|
+
console.log("\u6B63\u5728\u8BFB\u53D6\u672C\u673A\u9879\u76EE\u5E76\u4FDD\u5B58\u6388\u6743\u2026");
|
|
3239
|
+
const result2 = await send({ command: "pair-approve", fingerprint: selected.fingerprint, roots: values.root?.map((root) => resolve4(root)), ...values.root ? { write: !values["read-only"], allowUnknown: true } : values["read-only"] ? { write: false } : {} });
|
|
3240
|
+
console.log(`\u5DF2\u4FE1\u4EFB\u6D4F\u89C8\u5668\uFF0C${result2.roots?.length ?? 0} \u4E2A\u9879\u76EE\u5DF2\u6388\u6743\u3002\u8FD4\u56DE\u7F51\u9875\u5373\u53EF\u4F7F\u7528\u3002`);
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
if (command === "authorize") {
|
|
3244
|
+
if (!values.root?.length) throw Error("ROOT_REQUIRED: \u8BF7\u7528 --root \u6307\u5B9A\u76EE\u5F55");
|
|
3245
|
+
const request2 = { command, roots: values.root.map((root) => resolve4(root)), peer: values.peer };
|
|
3246
|
+
let reply = await control(stateDir, request2);
|
|
3247
|
+
if (reply.error === "DIRECTORY_APPROVAL_REQUIRED") {
|
|
3248
|
+
console.log("Codex \u672A\u8BC6\u522B\u4E3A\u5DF2\u6709\u9879\u76EE\uFF0C\u9700\u8981\u4F60\u5728\u672C\u673A\u786E\u8BA4\u8BFB\u5199\u6388\u6743\uFF1A" + request2.roots.join(", "));
|
|
3249
|
+
if ((await question("\u5141\u8BB8\u8BE5\u6D4F\u89C8\u5668\u8BBF\u95EE\u8FD9\u4E9B\u76EE\u5F55\uFF1F\u8F93\u5165 yes\uFF1A")).trim().toLowerCase() !== "yes") throw Error("AUTHORIZATION_CANCELLED");
|
|
3250
|
+
reply = await control(stateDir, { ...request2, allowUnknown: true });
|
|
3251
|
+
}
|
|
3252
|
+
console.log(JSON.stringify(reply));
|
|
3253
|
+
if (reply.error) process.exitCode = 1;
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
2984
3256
|
const request = {
|
|
2985
3257
|
command,
|
|
2986
3258
|
fingerprint: values.fingerprint,
|
|
2987
3259
|
peer: values.peer,
|
|
2988
|
-
roots: values.root,
|
|
2989
|
-
write: values
|
|
3260
|
+
roots: values.root?.map((root) => resolve4(root)),
|
|
3261
|
+
write: values["read-only"] ? false : values.write
|
|
2990
3262
|
};
|
|
2991
3263
|
const result = await control(stateDir, request);
|
|
2992
3264
|
console.log(JSON.stringify(result));
|
|
2993
3265
|
if (result.error) process.exitCode = 1;
|
|
2994
3266
|
}
|
|
2995
3267
|
main().catch((e) => {
|
|
2996
|
-
console.error(e?.code === "ENOENT" ? "CONFIG_OR_FILE_MISSING: run configure; check the configured paths" : e?.name === "ZodError" ? "CONFIG_INVALID: check the configuration fields" : e instanceof Error ? e.message : "CLIENT_FAILED");
|
|
3268
|
+
console.error(e?.code?.startsWith("ERR_PARSE_ARGS") ? "\u53C2\u6570\u4E0D\u53D7\u652F\u6301\u6216\u683C\u5F0F\u65E0\u6548\uFF08\u6CA1\u6709 --admin \u53C2\u6570\uFF09\uFF1B\u8BF7\u67E5\u770B web-codex-client --help\u3002" : e?.code === "ENOENT" ? "CONFIG_OR_FILE_MISSING: run configure; check the configured paths" : e?.name === "ZodError" ? "CONFIG_INVALID: check the configuration fields" : e instanceof Error ? e.message : "CLIENT_FAILED");
|
|
2997
3269
|
process.exitCode = 1;
|
|
2998
3270
|
});
|
|
2999
3271
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@landh93/web-codex-client",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
4
|
-
"description": "Encrypted local Codex bridge with
|
|
3
|
+
"version": "0.1.0-rc.3",
|
|
4
|
+
"description": "Encrypted local Codex bridge with automatic token authentication and workspace access",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": { "web-codex-client": "dist/cli.js" },
|
|
7
7
|
"files": ["dist/cli.js", "dist/assets/noise.wasm", "README.md", "NPM_RELEASE.md", "sdk/THIRD_PARTY_NOTICES.md", "deploy/config.example.json", "deploy/config.remote.example.json", "deploy/web-codex-client.service"],
|