@landh93/web-codex-client 0.1.0-rc.1 → 0.1.0-rc.2
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 +13 -3
- package/README.md +3 -3
- package/dist/cli.js +288 -72
- package/package.json +1 -1
package/NPM_RELEASE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# npm 客户端发布与安装
|
|
2
2
|
|
|
3
|
-
包名:`@landh93/web-codex-client
|
|
3
|
+
包名:`@landh93/web-codex-client`。当前候选版为 `0.1.0-rc.2`,使用 `next` 标签。仅支持 Linux / Node.js 24;这是独立安装候选版,不代表公网、多人或真实模型执行已完成生产验收。`remote-wss-v1` 明确允许远端 WSS,继续使用固定 Noise 加密、TLS 校验及本机指纹授权,不新增任何加密实现。项目尚未选定开源许可证,包暂保留 `UNLICENSED`;第三方代码许可见包内 `sdk/THIRD_PARTY_NOTICES.md`。
|
|
4
4
|
|
|
5
5
|
## 用户安装
|
|
6
6
|
|
|
@@ -25,7 +25,17 @@ web-codex-client configure --relay wss://relay.example.com --token-stdin < /path
|
|
|
25
25
|
|
|
26
26
|
使用公开 CA 证书时通常无需额外设置;私有 CA 可在启动 Node 前设置 `NODE_EXTRA_CA_CERTS=/path/to/ca.pem`。禁止 `NODE_TLS_REJECT_UNAUTHORIZED=0`。Nginx 要正确代理 `/ws/v1/client`,token 经 Authorization 头发送。
|
|
27
27
|
|
|
28
|
-
Client
|
|
28
|
+
Client 运行后,在网页选择设备,然后在另一终端执行 **`web-codex-client pair`**。对照终端与网页的完整指纹,在网页点击“与本机显示一致”,再在终端输入 `yes`。无需提前执行 `pair-open`,无需复制 64 位指纹或逐个配置已有项目目录。
|
|
29
|
+
|
|
30
|
+
核对并信任浏览器后,从本机 Codex 项目目录及活动/归档历史的 cwd 识别已有项目,自动授予这些目录的读写权限;扫描有分页、去重和数量上限,不会把浏览器任意指定的路径当作已有项目。系统根目录、用户家目录以及包含 Client 状态的父目录不自动授予。没有识别到项目时明确报错,可以使用 `pair --root /absolute/project` 在终端明确确认目录读写授权。
|
|
31
|
+
|
|
32
|
+
新增目录执行 **`web-codex-client authorize --root /absolute/directory`**:已有 Codex 项目直接授权读写,其他目录要求终端输入 `yes`。若有多个活动浏览器,使用 `--peer` 指定 `status` 中的浏览器公钥。权限按目录检查,新增写权限不会提升其他只读目录。Codex 执行任务仍使用工作区沙箱、操作审批及写租约;网页中的“申请控制权”管理同一对话的写入归属。
|
|
33
|
+
|
|
34
|
+
自动授权是配对时的目录快照。配对后新建的 Codex 项目可使用上述 `authorize` 命令加入,无需重新配对。升级不会擅自提升已有只读授权。
|
|
35
|
+
|
|
36
|
+
旧的 `pair-approve --fingerprint FULL_HASH`、`pair-list`、`--state` 用法仍有效。`pair-approve` 不传 `--root` 时采用已有项目;指定已有项目且不传权限参数时自动读写;`--read-only` 明确保持只读;未知目录可通过 `pair --root` 交互确认,或通过显式 `pair-approve --root PATH --write` 授权。重复批准同一已授权指纹返回成功,不会因为窗口已关闭而失败。
|
|
37
|
+
|
|
38
|
+
配对等待最多 5 分钟,期间只能核对身份,不能读取内容。`pair-list` 返回网页是否确认、剩余时间和可否批准;本机错误响应区分指纹不存在、网页未确认、路由过期、目录不可访问、授权保存失败和授权数量上限,且不包含底层异常正文。网页刷新或重新连接后必须使用新的指纹。旧 Web 的 2 分钟等待仍可能先到期;更新 Server 前端后会显示新指引并使用 5 分钟等待。协议及固定 SDK 版本保持兼容。
|
|
29
39
|
|
|
30
40
|
后台常驻可使用 systemd 用户服务,将 `ExecStart` 设为实际安装的 `web-codex-client run --config /absolute/config.json`。npm 安装不会自动启用服务或打开配对窗口。
|
|
31
41
|
|
|
@@ -65,7 +75,7 @@ npm whoami --registry=https://registry.npmjs.org/ --userconfig="$HOME/.config/we
|
|
|
65
75
|
随后发布已通过验证的**具体 tgz**,不重新构建另一个包:
|
|
66
76
|
|
|
67
77
|
```bash
|
|
68
|
-
npm publish .test-output/landh93-web-codex-client-0.1.0-rc.
|
|
78
|
+
npm publish .test-output/landh93-web-codex-client-0.1.0-rc.2.tgz \
|
|
69
79
|
--access public --tag next --ignore-scripts \
|
|
70
80
|
--registry=https://registry.npmjs.org/ \
|
|
71
81
|
--userconfig="$HOME/.config/web-codex-npm/npmrc"
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ web-codex-client run
|
|
|
19
19
|
|
|
20
20
|
`configure` 交互输入 WSS 地址、Codex socket 和隐藏的设备 token,保存在用户配置目录。`doctor` 检查私有配置、WASM、WSS 认证和 Codex 初始化,失败退出非零;既有 Client 在线时不会抢占它的连接。支持 `--quiet`。安装和升级不会重启 Codex,也不会自动批准浏览器。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
网页选择设备后,另开终端运行 `web-codex-client pair`,核对完整指纹并在网页和终端确认。无需提前开启窗口;Codex 已有项目自动授予读写,新目录用 `web-codex-client authorize --root /absolute/path` 在本机确认。配置、身份和授权独立于 npm 安装目录,升级时保留。完整安装、token 标准输入、证书、后台运行与发布说明见 [NPM_RELEASE.md](./NPM_RELEASE.md)。
|
|
23
23
|
|
|
24
24
|
## 安装、构建、自测
|
|
25
25
|
|
|
@@ -64,7 +64,7 @@ node dist/cli.js run --config ~/.config/web-codex-client/config.json
|
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
66
|
node dist/cli.js status --state ~/.config/web-codex-client/state
|
|
67
|
-
node dist/cli.js pair
|
|
67
|
+
node dist/cli.js pair --config ~/.config/web-codex-client/config.json
|
|
68
68
|
node dist/cli.js pair-list --state ~/.config/web-codex-client/state
|
|
69
69
|
# 浏览器完成握手后,与本地 pair-list 核对完整的 64 位指纹。
|
|
70
70
|
# 在可信浏览器点击确认,再在本机明确授予目录权限:
|
|
@@ -75,7 +75,7 @@ node dist/cli.js revoke --state ~/.config/web-codex-client/state \
|
|
|
75
75
|
--peer BROWSER_PUBLIC_KEY_FROM_PAIR_LIST
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
rc.2 配对等待 5 分钟,本机确认还要求浏览器已确认相同指纹;不再强制先执行 `pair-open`。默认对识别到的 Codex 项目授予读写,其他目录需要显式本机授权;`--read-only` 可指定只读。`pair-list` 显示剩余时间,失败返回白名单错误码和处理提示。撤销立即关闭该身份的会话并拒绝后续业务。浏览器身份默认仅存内存,刷新后重新配对;同一页面断线重连可以继续使用内存身份和固定公钥。
|
|
79
79
|
|
|
80
80
|
`status` 分别返回 relayOnline、handshakes、authorizedSessions、codexAvailable。Codex 连接按第一次已授权请求惰性建立,因此仅中继上线时 codexAvailable 仍为 false。上游断线后下一次请求重连,旧 epoch/订阅失效。后台服务无有限总工作量,不显示伪造百分比。
|
|
81
81
|
|
package/dist/cli.js
CHANGED
|
@@ -1356,15 +1356,18 @@ async function loadState(dir) {
|
|
|
1356
1356
|
list = z4.array(
|
|
1357
1357
|
z4.strictObject({
|
|
1358
1358
|
peer: z4.string(),
|
|
1359
|
-
roots: z4.array(z4.string()).min(1).max(
|
|
1360
|
-
write: z4.boolean()
|
|
1359
|
+
roots: z4.array(z4.string()).min(1).max(256),
|
|
1360
|
+
write: z4.boolean(),
|
|
1361
|
+
writeRoots: z4.array(z4.string()).max(256).optional()
|
|
1361
1362
|
})
|
|
1362
1363
|
).max(256).parse(JSON.parse(await privateRead(dir + "/grants.json")));
|
|
1363
1364
|
} catch (e) {
|
|
1364
1365
|
if (e.code !== "ENOENT") throw e;
|
|
1365
1366
|
}
|
|
1366
|
-
for (const g of list)
|
|
1367
|
+
for (const g of list) {
|
|
1367
1368
|
g.roots = await Promise.all(g.roots.map((r) => realpath(r)));
|
|
1369
|
+
if (g.writeRoots) g.writeRoots = await Promise.all(g.writeRoots.map((r) => realpath(r)));
|
|
1370
|
+
}
|
|
1368
1371
|
return { identity, grants: new Map(list.map((g) => [g.peer, g])) };
|
|
1369
1372
|
}
|
|
1370
1373
|
|
|
@@ -1374,7 +1377,7 @@ import {
|
|
|
1374
1377
|
createServer,
|
|
1375
1378
|
createConnection
|
|
1376
1379
|
} from "node:net";
|
|
1377
|
-
import { chmod, realpath as
|
|
1380
|
+
import { chmod, realpath as realpath4, unlink, lstat as lstat2, stat as stat3 } from "node:fs/promises";
|
|
1378
1381
|
import { z as z5 } from "zod";
|
|
1379
1382
|
|
|
1380
1383
|
// src/bridge.ts
|
|
@@ -1772,6 +1775,11 @@ var Bridge = class {
|
|
|
1772
1775
|
if (!grant) throw new BridgeError("NOT_PAIRED");
|
|
1773
1776
|
return grant;
|
|
1774
1777
|
}
|
|
1778
|
+
async writable(owner, path) {
|
|
1779
|
+
const grant = this.grant(owner);
|
|
1780
|
+
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");
|
|
1781
|
+
await authorizedPath(grant.writeRoots ?? grant.roots, path);
|
|
1782
|
+
}
|
|
1775
1783
|
async project(owner, id2) {
|
|
1776
1784
|
const g = this.grant(owner);
|
|
1777
1785
|
if (id2.startsWith("cwd:")) {
|
|
@@ -1979,7 +1987,10 @@ var Bridge = class {
|
|
|
1979
1987
|
historyFallback: "bounded",
|
|
1980
1988
|
activeTakeover: "unverified"
|
|
1981
1989
|
};
|
|
1982
|
-
if ("threadId" in p)
|
|
1990
|
+
if ("threadId" in p) {
|
|
1991
|
+
const thread2 = await this.thread(owner, p.threadId);
|
|
1992
|
+
if (writes.has(method) || method === "leases.acquire") await this.writable(owner, thread2.cwd);
|
|
1993
|
+
}
|
|
1983
1994
|
if (writes.has(method) && "threadId" in p) {
|
|
1984
1995
|
this.leases.assert(p.threadId, owner);
|
|
1985
1996
|
if ((method === "turns.start" || method === "turns.interrupt") && p.connectionEpoch !== this.subscriptions.epoch)
|
|
@@ -2124,6 +2135,7 @@ var Bridge = class {
|
|
|
2124
2135
|
}
|
|
2125
2136
|
case "threads.start": {
|
|
2126
2137
|
const project = await this.project(owner, p.projectId);
|
|
2138
|
+
await this.writable(owner, project.cwd);
|
|
2127
2139
|
const r = await this.upstream.call("thread/start", {
|
|
2128
2140
|
cwd: project.cwd,
|
|
2129
2141
|
sandbox: "workspace-write",
|
|
@@ -2237,7 +2249,8 @@ var Bridge = class {
|
|
|
2237
2249
|
throw new BridgeError("APPROVAL_EXPIRED");
|
|
2238
2250
|
if (!g.write) throw new BridgeError("FORBIDDEN");
|
|
2239
2251
|
this.leases.assert(a.thread, owner);
|
|
2240
|
-
await this.thread(owner, a.thread);
|
|
2252
|
+
const thread2 = await this.thread(owner, a.thread);
|
|
2253
|
+
await this.writable(owner, thread2.cwd);
|
|
2241
2254
|
if (this.approvals.get(id2) !== a) throw new BridgeError("APPROVAL_EXPIRED");
|
|
2242
2255
|
this.grant(owner);
|
|
2243
2256
|
this.leases.assert(a.thread, owner);
|
|
@@ -2296,7 +2309,83 @@ var Bridge = class {
|
|
|
2296
2309
|
}
|
|
2297
2310
|
};
|
|
2298
2311
|
|
|
2312
|
+
// src/admin-errors.ts
|
|
2313
|
+
var adminMessages = {
|
|
2314
|
+
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",
|
|
2315
|
+
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",
|
|
2316
|
+
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",
|
|
2317
|
+
PAIRING_WINDOW_EXPIRED: "\u914D\u5BF9\u7A97\u53E3\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FDE\u63A5\u7F51\u9875\u3002",
|
|
2318
|
+
DIRECTORY_UNAVAILABLE: "\u76EE\u5F55\u4E0D\u5B58\u5728\u3001\u4E0D\u662F\u76EE\u5F55\u6216\u5F53\u524D\u7528\u6237\u65E0\u6CD5\u8BBF\u95EE\u3002",
|
|
2319
|
+
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",
|
|
2320
|
+
GRANT_LIMIT: "\u6388\u6743\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u8BF7\u5148\u64A4\u9500\u4E0D\u518D\u4F7F\u7528\u7684\u6D4F\u89C8\u5668\u3002",
|
|
2321
|
+
CODEX_UNAVAILABLE: "\u65E0\u6CD5\u8BFB\u53D6\u672C\u673A Codex \u9879\u76EE\uFF0C\u8BF7\u8FD0\u884C web-codex-client doctor\u3002",
|
|
2322
|
+
PROJECT_DISCOVERY_LIMIT: "\u9879\u76EE\u76EE\u5F55\u626B\u63CF\u8FBE\u5230\u4E0A\u9650\uFF0C\u8BF7\u7528 --root \u660E\u786E\u6307\u5B9A\u9879\u76EE\u3002",
|
|
2323
|
+
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",
|
|
2324
|
+
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",
|
|
2325
|
+
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",
|
|
2326
|
+
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",
|
|
2327
|
+
INVALID_ADMIN_REQUEST: "\u547D\u4EE4\u53C2\u6570\u65E0\u6548\uFF0C\u8BF7\u67E5\u770B web-codex-client --help\u3002",
|
|
2328
|
+
ADMIN_REQUEST_REJECTED: "\u672C\u673A\u63A7\u5236\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 Client \u72B6\u6001\u3002"
|
|
2329
|
+
};
|
|
2330
|
+
var AdminError = class extends Error {
|
|
2331
|
+
constructor(code) {
|
|
2332
|
+
super(adminMessages[code]);
|
|
2333
|
+
this.code = code;
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
function adminFailure(error) {
|
|
2337
|
+
const code = error instanceof AdminError ? error.code : error instanceof Error && error.name === "ZodError" || error instanceof SyntaxError ? "INVALID_ADMIN_REQUEST" : "ADMIN_REQUEST_REJECTED";
|
|
2338
|
+
return { error: code, message: adminMessages[code] };
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/known-projects.ts
|
|
2342
|
+
import { realpath as realpath3, stat as stat2 } from "node:fs/promises";
|
|
2343
|
+
import { homedir as homedir2 } from "node:os";
|
|
2344
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
2345
|
+
async function knownProjects(upstream, stateDir) {
|
|
2346
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2347
|
+
async function add(value) {
|
|
2348
|
+
if (typeof value !== "string" || !isAbsolute2(value)) return;
|
|
2349
|
+
try {
|
|
2350
|
+
const root = await realpath3(value);
|
|
2351
|
+
if (!(await stat2(root)).isDirectory() || root === "/" || root === homedir2() || within(root, stateDir)) return;
|
|
2352
|
+
roots.add(root);
|
|
2353
|
+
if (roots.size > 256) throw new AdminError("PROJECT_DISCOVERY_LIMIT");
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
if (error instanceof AdminError) throw error;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
async function scan(method, extra, consume) {
|
|
2359
|
+
let cursor;
|
|
2360
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2361
|
+
for (let page2 = 0; page2 < 100; page2++) {
|
|
2362
|
+
const result = await upstream.call(method, { ...extra, limit: 100, cursor });
|
|
2363
|
+
if (!Array.isArray(result.data) || result.data.length > 100) throw new AdminError("CODEX_UNAVAILABLE");
|
|
2364
|
+
for (const row of result.data) await consume(row);
|
|
2365
|
+
if (!result.nextCursor) return;
|
|
2366
|
+
if (typeof result.nextCursor !== "string" || seen.has(result.nextCursor)) throw new AdminError("PROJECT_DISCOVERY_LIMIT");
|
|
2367
|
+
cursor = result.nextCursor;
|
|
2368
|
+
seen.add(result.nextCursor);
|
|
2369
|
+
}
|
|
2370
|
+
throw new AdminError("PROJECT_DISCOVERY_LIMIT");
|
|
2371
|
+
}
|
|
2372
|
+
try {
|
|
2373
|
+
await scan("project/list", {}, async (row) => {
|
|
2374
|
+
for (const root of row.roots ?? []) await add(root.path);
|
|
2375
|
+
});
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
if (!(error instanceof RpcError && error.code === -32601)) throw error;
|
|
2378
|
+
}
|
|
2379
|
+
for (const archived of [false, true]) {
|
|
2380
|
+
await scan("thread/list", { useStateDbOnly: true, archived, sourceKinds: ["cli", "vscode", "exec", "appServer", "subAgent", "subAgentReview", "subAgentCompact", "subAgentThreadSpawn", "subAgentOther", "unknown"] }, async (row) => {
|
|
2381
|
+
await add(row.cwd);
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
return [...roots].sort();
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2299
2387
|
// src/daemon.ts
|
|
2388
|
+
var PAIRING_MS = 5 * 60 * 1e3;
|
|
2300
2389
|
var ClientDaemon = class {
|
|
2301
2390
|
constructor(config, identity, grants, connectUpstream = () => CodexRpc.connect(config.upstream)) {
|
|
2302
2391
|
this.config = config;
|
|
@@ -2310,7 +2399,10 @@ var ClientDaemon = class {
|
|
|
2310
2399
|
stopped = false;
|
|
2311
2400
|
routes = /* @__PURE__ */ new Map();
|
|
2312
2401
|
pairUntil = 0;
|
|
2402
|
+
lastRouteClosure;
|
|
2403
|
+
expiredFingerprints = /* @__PURE__ */ new Set();
|
|
2313
2404
|
admin;
|
|
2405
|
+
adminQueue = Promise.resolve();
|
|
2314
2406
|
timer;
|
|
2315
2407
|
retry;
|
|
2316
2408
|
attempts = 0;
|
|
@@ -2333,7 +2425,7 @@ var ClientDaemon = class {
|
|
|
2333
2425
|
}
|
|
2334
2426
|
}
|
|
2335
2427
|
for (const [id2, r] of this.routes)
|
|
2336
|
-
if (Date.now() > r.expires && !r.owner) this.closeRoute(id2);
|
|
2428
|
+
if (Date.now() > r.expires && !r.owner) this.closeRoute(id2, true, "PAIRING_ROUTE_EXPIRED");
|
|
2337
2429
|
}, 1e4);
|
|
2338
2430
|
this.timer.unref();
|
|
2339
2431
|
}
|
|
@@ -2406,7 +2498,7 @@ var ClientDaemon = class {
|
|
|
2406
2498
|
}
|
|
2407
2499
|
this.routes.set(m.routeId, {
|
|
2408
2500
|
confirmed: false,
|
|
2409
|
-
expires: Date.now() +
|
|
2501
|
+
expires: Date.now() + PAIRING_MS,
|
|
2410
2502
|
queue: Promise.resolve(),
|
|
2411
2503
|
pending: 0,
|
|
2412
2504
|
out: [],
|
|
@@ -2416,7 +2508,7 @@ var ClientDaemon = class {
|
|
|
2416
2508
|
return;
|
|
2417
2509
|
}
|
|
2418
2510
|
if (m.type === "route.closed") {
|
|
2419
|
-
this.closeRoute(m.routeId, false);
|
|
2511
|
+
this.closeRoute(m.routeId, false, "RELAY_CLOSED");
|
|
2420
2512
|
return;
|
|
2421
2513
|
}
|
|
2422
2514
|
if (m.type !== "secure") return;
|
|
@@ -2457,8 +2549,6 @@ var ClientDaemon = class {
|
|
|
2457
2549
|
fingerprint: r.session.fingerprint
|
|
2458
2550
|
});
|
|
2459
2551
|
} else {
|
|
2460
|
-
if (Date.now() > this.pairUntil)
|
|
2461
|
-
throw new BridgeError("NOT_PAIRED");
|
|
2462
2552
|
this.send(frame.routeId, {
|
|
2463
2553
|
kind: "pairing",
|
|
2464
2554
|
status: "pending",
|
|
@@ -2478,10 +2568,18 @@ var ClientDaemon = class {
|
|
|
2478
2568
|
}
|
|
2479
2569
|
if (!r.owner || !this.grants.has(r.owner))
|
|
2480
2570
|
throw new BridgeError("NOT_PAIRED");
|
|
2481
|
-
|
|
2571
|
+
try {
|
|
2572
|
+
await this.ensureUpstream();
|
|
2573
|
+
} catch {
|
|
2574
|
+
if (message.kind === "request") {
|
|
2575
|
+
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 } });
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
throw new BridgeError("UPSTREAM_UNAVAILABLE");
|
|
2579
|
+
}
|
|
2482
2580
|
const response = await this.bridge.handle(r.owner, message);
|
|
2483
2581
|
if (response) this.send(frame.routeId, response);
|
|
2484
|
-
}).catch(() => this.closeRoute(frame.routeId)).finally(() => r.pending--);
|
|
2582
|
+
}).catch((error) => this.closeRoute(frame.routeId, true, error instanceof BridgeError ? error.code : "PROTOCOL_ERROR")).finally(() => r.pending--);
|
|
2485
2583
|
}
|
|
2486
2584
|
send(id2, message) {
|
|
2487
2585
|
const r = this.routes.get(id2);
|
|
@@ -2518,9 +2616,14 @@ var ClientDaemon = class {
|
|
|
2518
2616
|
r.sending = false;
|
|
2519
2617
|
}
|
|
2520
2618
|
}
|
|
2521
|
-
closeRoute(id2, notify = true) {
|
|
2619
|
+
closeRoute(id2, notify = true, reason = "CLOSED") {
|
|
2522
2620
|
const r = this.routes.get(id2);
|
|
2523
2621
|
if (!r) return;
|
|
2622
|
+
this.lastRouteClosure = { at: Date.now(), reason };
|
|
2623
|
+
if (reason === "PAIRING_ROUTE_EXPIRED" && r.session?.fingerprint) {
|
|
2624
|
+
if (this.expiredFingerprints.size >= 64) this.expiredFingerprints.delete(this.expiredFingerprints.values().next().value);
|
|
2625
|
+
this.expiredFingerprints.add(r.session.fingerprint);
|
|
2626
|
+
}
|
|
2524
2627
|
this.routes.delete(id2);
|
|
2525
2628
|
r.session?.close();
|
|
2526
2629
|
r.out = [];
|
|
@@ -2541,50 +2644,65 @@ var ClientDaemon = class {
|
|
|
2541
2644
|
authorizedSessions: Array.from(this.routes.values()).filter(
|
|
2542
2645
|
(r) => r.owner
|
|
2543
2646
|
).length,
|
|
2544
|
-
pairingUntil: this.pairUntil
|
|
2647
|
+
pairingUntil: this.pairUntil,
|
|
2648
|
+
pairingMode: "local-confirmation",
|
|
2649
|
+
pairingRemainingMs: Math.max(0, this.pairUntil - Date.now()),
|
|
2650
|
+
lastRouteClosure: this.lastRouteClosure,
|
|
2651
|
+
grants: Array.from(this.grants.values()).map((g) => ({ peer: g.peer, rootCount: g.roots.length, write: g.write }))
|
|
2545
2652
|
};
|
|
2546
2653
|
case "pair-open":
|
|
2547
|
-
this.pairUntil = Date.now() +
|
|
2654
|
+
this.pairUntil = Date.now() + PAIRING_MS;
|
|
2655
|
+
for (const r of this.routes.values()) if (!r.owner) r.expires = this.pairUntil;
|
|
2548
2656
|
return { expiresAt: this.pairUntil };
|
|
2549
2657
|
case "pair-list":
|
|
2550
2658
|
return {
|
|
2659
|
+
pairingMode: "local-confirmation",
|
|
2660
|
+
windowRequired: false,
|
|
2661
|
+
pairingUntil: this.pairUntil,
|
|
2662
|
+
pairingRemainingMs: Math.max(0, this.pairUntil - Date.now()),
|
|
2551
2663
|
pending: Array.from(this.routes.entries()).filter(([, r]) => r.session?.ready && !r.owner).map(([routeId, r]) => ({
|
|
2552
2664
|
routeId,
|
|
2553
2665
|
fingerprint: r.session.fingerprint,
|
|
2554
2666
|
peer: r.session.peerPublicKey,
|
|
2555
2667
|
browserConfirmed: r.confirmed,
|
|
2556
|
-
expiresAt: r.expires
|
|
2668
|
+
expiresAt: r.expires,
|
|
2669
|
+
remainingMs: Math.max(0, r.expires - Date.now()),
|
|
2670
|
+
canApprove: r.confirmed && Date.now() <= r.expires
|
|
2557
2671
|
}))
|
|
2558
2672
|
};
|
|
2559
2673
|
case "pair-approve": {
|
|
2560
2674
|
const p = z5.object({
|
|
2561
2675
|
fingerprint: z5.string().regex(/^[a-f0-9]{64}$/),
|
|
2562
|
-
roots: z5.array(z5.string()).min(1).max(32),
|
|
2563
|
-
write: z5.boolean().
|
|
2676
|
+
roots: z5.array(z5.string()).min(1).max(32).optional(),
|
|
2677
|
+
write: z5.boolean().optional(),
|
|
2678
|
+
allowUnknown: z5.boolean().default(false)
|
|
2564
2679
|
}).parse(m);
|
|
2565
2680
|
const found = Array.from(this.routes.entries()).find(
|
|
2566
2681
|
([, r2]) => r2.session?.fingerprint === p.fingerprint
|
|
2567
2682
|
);
|
|
2568
|
-
if (!found
|
|
2569
|
-
throw Error("PAIRING_EXPIRED_OR_UNCONFIRMED");
|
|
2570
|
-
if (this.grants.size >= 256) throw Error("GRANT_LIMIT");
|
|
2683
|
+
if (!found) throw new AdminError(this.expiredFingerprints.has(p.fingerprint) ? "PAIRING_ROUTE_EXPIRED" : "FINGERPRINT_NOT_FOUND");
|
|
2571
2684
|
const [id2, r] = found;
|
|
2685
|
+
if (r.owner) return { peer: r.owner, approved: true, alreadyApproved: true };
|
|
2686
|
+
if (Date.now() > r.expires) throw new AdminError("PAIRING_ROUTE_EXPIRED");
|
|
2687
|
+
if (!r.confirmed) throw new AdminError("BROWSER_NOT_CONFIRMED");
|
|
2688
|
+
if (this.grants.size >= 256) throw new AdminError("GRANT_LIMIT");
|
|
2572
2689
|
const peer = r.session.peerPublicKey;
|
|
2690
|
+
const selectedRoots = p.roots ? await this.canonicalRoots(p.roots) : void 0;
|
|
2691
|
+
let known = [];
|
|
2692
|
+
if (p.write === void 0 || !p.roots) known = await this.discoverProjects();
|
|
2693
|
+
const roots = selectedRoots ?? known;
|
|
2694
|
+
if (!roots.length) throw new AdminError("NO_KNOWN_PROJECTS");
|
|
2695
|
+
const unknown = roots.filter((root) => !known.includes(root));
|
|
2696
|
+
if (unknown.length && p.write === void 0 && !p.allowUnknown) throw new AdminError("DIRECTORY_APPROVAL_REQUIRED");
|
|
2697
|
+
const writeRoots = p.write === false ? [] : p.write || p.allowUnknown ? roots : roots.filter((root) => known.includes(root));
|
|
2698
|
+
if (this.routes.get(id2) !== r || Date.now() > r.expires) throw new AdminError("PAIRING_ROUTE_EXPIRED");
|
|
2573
2699
|
const grant = {
|
|
2574
2700
|
peer,
|
|
2575
|
-
roots
|
|
2576
|
-
write:
|
|
2701
|
+
roots,
|
|
2702
|
+
write: writeRoots.length > 0,
|
|
2703
|
+
writeRoots
|
|
2577
2704
|
};
|
|
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
|
-
}
|
|
2705
|
+
await this.saveGrant(grant, () => this.routes.get(id2) === r && Date.now() <= r.expires);
|
|
2588
2706
|
r.session.trust(p.fingerprint);
|
|
2589
2707
|
r.owner = peer;
|
|
2590
2708
|
this.pairUntil = 0;
|
|
@@ -2593,7 +2711,24 @@ var ClientDaemon = class {
|
|
|
2593
2711
|
status: "ready",
|
|
2594
2712
|
fingerprint: p.fingerprint
|
|
2595
2713
|
});
|
|
2596
|
-
return { peer, approved: true };
|
|
2714
|
+
return { peer, approved: true, roots, write: grant.write, writeRoots };
|
|
2715
|
+
}
|
|
2716
|
+
case "projects-known":
|
|
2717
|
+
return { roots: await this.discoverProjects() };
|
|
2718
|
+
case "authorize": {
|
|
2719
|
+
const p = z5.object({ peer: z5.string().optional(), roots: z5.array(z5.string()).min(1).max(32), allowUnknown: z5.boolean().default(false) }).parse(m);
|
|
2720
|
+
const activePeers = [...new Set([...this.routes.values()].map((r) => r.owner).filter((peer2) => !!peer2))];
|
|
2721
|
+
const peer = p.peer ?? (activePeers.length === 1 ? activePeers[0] : this.grants.size === 1 ? this.grants.keys().next().value : void 0);
|
|
2722
|
+
if (!peer) throw new AdminError(this.grants.size ? "PEER_REQUIRED" : "NOT_PAIRED");
|
|
2723
|
+
const old = this.grants.get(peer);
|
|
2724
|
+
if (!old) throw new AdminError("NOT_PAIRED");
|
|
2725
|
+
const roots = await this.canonicalRoots(p.roots), known = await this.discoverProjects();
|
|
2726
|
+
if (!p.allowUnknown && roots.some((root) => !known.includes(root))) throw new AdminError("DIRECTORY_APPROVAL_REQUIRED");
|
|
2727
|
+
const nextRoots = [.../* @__PURE__ */ new Set([...old.roots, ...roots])];
|
|
2728
|
+
if (nextRoots.length > 256) throw new AdminError("GRANT_LIMIT");
|
|
2729
|
+
const writeRoots = [.../* @__PURE__ */ new Set([...old.writeRoots ?? (old.write ? old.roots : []), ...roots])];
|
|
2730
|
+
await this.saveGrant({ ...old, roots: nextRoots, write: true, writeRoots });
|
|
2731
|
+
return { peer, approved: true, roots: nextRoots, writeRoots };
|
|
2597
2732
|
}
|
|
2598
2733
|
case "revoke": {
|
|
2599
2734
|
const peer = z5.string().parse(m.peer);
|
|
@@ -2609,8 +2744,46 @@ var ClientDaemon = class {
|
|
|
2609
2744
|
return { revoked: true };
|
|
2610
2745
|
}
|
|
2611
2746
|
default:
|
|
2612
|
-
throw
|
|
2747
|
+
throw new AdminError("INVALID_ADMIN_REQUEST");
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
async canonicalRoots(roots) {
|
|
2751
|
+
try {
|
|
2752
|
+
return await Promise.all(roots.map(async (path) => {
|
|
2753
|
+
const root = await realpath4(path);
|
|
2754
|
+
if (!(await stat3(root)).isDirectory()) throw Error();
|
|
2755
|
+
return root;
|
|
2756
|
+
}));
|
|
2757
|
+
} catch {
|
|
2758
|
+
throw new AdminError("DIRECTORY_UNAVAILABLE");
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async discoverProjects() {
|
|
2762
|
+
try {
|
|
2763
|
+
await this.ensureUpstream();
|
|
2764
|
+
return await knownProjects(this.bridge.upstream, this.config.stateDir);
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
if (error instanceof AdminError) throw error;
|
|
2767
|
+
throw new AdminError("CODEX_UNAVAILABLE");
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
async saveGrant(grant, stillValid) {
|
|
2771
|
+
const values = new Map(this.grants);
|
|
2772
|
+
values.set(grant.peer, grant);
|
|
2773
|
+
try {
|
|
2774
|
+
await atomicPrivate(this.config.stateDir + "/grants.json", [...values.values()]);
|
|
2775
|
+
} catch {
|
|
2776
|
+
throw new AdminError("GRANT_SAVE_FAILED");
|
|
2777
|
+
}
|
|
2778
|
+
if (stillValid && !stillValid()) {
|
|
2779
|
+
try {
|
|
2780
|
+
await atomicPrivate(this.config.stateDir + "/grants.json", [...this.grants.values()]);
|
|
2781
|
+
} catch {
|
|
2782
|
+
throw new AdminError("GRANT_SAVE_FAILED");
|
|
2783
|
+
}
|
|
2784
|
+
throw new AdminError("PAIRING_ROUTE_EXPIRED");
|
|
2613
2785
|
}
|
|
2786
|
+
this.grants.set(grant.peer, grant);
|
|
2614
2787
|
}
|
|
2615
2788
|
async startAdmin() {
|
|
2616
2789
|
const path = this.config.stateDir + "/control.sock";
|
|
@@ -2641,7 +2814,7 @@ var ClientDaemon = class {
|
|
|
2641
2814
|
if (e.code !== "ENOENT") throw e;
|
|
2642
2815
|
}
|
|
2643
2816
|
this.admin = createServer((socket) => {
|
|
2644
|
-
socket.setTimeout(
|
|
2817
|
+
socket.setTimeout(6e4, () => socket.destroy());
|
|
2645
2818
|
let data = "";
|
|
2646
2819
|
socket.on("data", (chunk) => {
|
|
2647
2820
|
data += chunk.toString();
|
|
@@ -2651,10 +2824,17 @@ var ClientDaemon = class {
|
|
|
2651
2824
|
}
|
|
2652
2825
|
if (!data.includes("\n")) return;
|
|
2653
2826
|
socket.pause();
|
|
2654
|
-
void Promise.resolve().then(() =>
|
|
2827
|
+
void Promise.resolve().then(() => {
|
|
2828
|
+
const request = JSON.parse(data);
|
|
2829
|
+
const result = this.adminQueue.then(() => this.adminCommand(request));
|
|
2830
|
+
this.adminQueue = result.then(() => {
|
|
2831
|
+
}, () => {
|
|
2832
|
+
});
|
|
2833
|
+
return result;
|
|
2834
|
+
}).then(
|
|
2655
2835
|
(result) => socket.end(JSON.stringify({ result }) + "\n"),
|
|
2656
|
-
() => socket.end(
|
|
2657
|
-
JSON.stringify(
|
|
2836
|
+
(error) => socket.end(
|
|
2837
|
+
JSON.stringify(adminFailure(error)) + "\n"
|
|
2658
2838
|
)
|
|
2659
2839
|
);
|
|
2660
2840
|
});
|
|
@@ -2690,7 +2870,7 @@ function control(stateDir, request) {
|
|
|
2690
2870
|
return new Promise((resolve5, reject) => {
|
|
2691
2871
|
const socket = createConnection2(join2(stateDir, "control.sock"));
|
|
2692
2872
|
let data = "";
|
|
2693
|
-
socket.setTimeout(
|
|
2873
|
+
socket.setTimeout(6e4, () => socket.destroy(Error("ADMIN_TIMEOUT: \u672C\u673A\u5904\u7406\u8D85\u65F6\uFF0C\u8BF7\u8FD0\u884C doctor \u540E\u91CD\u8BD5")));
|
|
2694
2874
|
socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
|
|
2695
2875
|
socket.on("data", (chunk) => {
|
|
2696
2876
|
data += chunk.toString();
|
|
@@ -2708,14 +2888,14 @@ function control(stateDir, request) {
|
|
|
2708
2888
|
}
|
|
2709
2889
|
|
|
2710
2890
|
// src/setup.ts
|
|
2711
|
-
import { homedir as
|
|
2891
|
+
import { homedir as homedir3 } from "node:os";
|
|
2712
2892
|
import { dirname as dirname3, join as join3, resolve as resolve3, basename as basename3 } from "node:path";
|
|
2713
2893
|
import { mkdir as mkdir2, lstat as lstat3, writeFile as writeFile2, unlink as unlink2 } from "node:fs/promises";
|
|
2714
2894
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
2715
2895
|
import { createInterface } from "node:readline/promises";
|
|
2716
2896
|
import { Writable } from "node:stream";
|
|
2717
2897
|
function defaultConfigPath() {
|
|
2718
|
-
return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(
|
|
2898
|
+
return resolve3(process.env.WEB_CODEX_CLIENT_CONFIG ?? join3(process.env.XDG_CONFIG_HOME ?? join3(homedir3(), ".config"), "web-codex-client", "config.json"));
|
|
2719
2899
|
}
|
|
2720
2900
|
function validateToken(value) {
|
|
2721
2901
|
const token = value.trim();
|
|
@@ -2891,37 +3071,39 @@ async function doctor(configPath, quiet = false, reportPath = join4(dirname4(con
|
|
|
2891
3071
|
}
|
|
2892
3072
|
|
|
2893
3073
|
// 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
3074
|
async function main() {
|
|
3075
|
+
const { values, positionals } = parseArgs({
|
|
3076
|
+
allowPositionals: true,
|
|
3077
|
+
options: {
|
|
3078
|
+
config: { type: "string" },
|
|
3079
|
+
state: { type: "string" },
|
|
3080
|
+
socket: { type: "string" },
|
|
3081
|
+
binary: { type: "string" },
|
|
3082
|
+
standalone: { type: "boolean" },
|
|
3083
|
+
transport: { type: "string" },
|
|
3084
|
+
"codex-home": { type: "string" },
|
|
3085
|
+
fingerprint: { type: "string" },
|
|
3086
|
+
peer: { type: "string" },
|
|
3087
|
+
root: { type: "string", multiple: true },
|
|
3088
|
+
write: { type: "boolean" },
|
|
3089
|
+
"read-only": { type: "boolean" },
|
|
3090
|
+
quiet: { type: "boolean" },
|
|
3091
|
+
report: { type: "string" },
|
|
3092
|
+
relay: { type: "string" },
|
|
3093
|
+
"token-stdin": { type: "boolean" },
|
|
3094
|
+
help: { type: "boolean", short: "h" },
|
|
3095
|
+
version: { type: "boolean", short: "v" }
|
|
3096
|
+
}
|
|
3097
|
+
});
|
|
3098
|
+
const command = positionals[0] ?? "help";
|
|
3099
|
+
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
3100
|
if (values.version) {
|
|
2919
3101
|
console.log(JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
2920
3102
|
return;
|
|
2921
3103
|
}
|
|
2922
3104
|
if (command === "help" || values.help) {
|
|
2923
3105
|
console.log(
|
|
2924
|
-
"web-codex-client configure [--config FILE] [--relay wss://HOST --token-stdin] [--socket PATH]\nweb-codex-client doctor [--config FILE] [--quiet] [--report FILE]\nweb-codex-client run [--config FILE]\nweb-codex-client init [--state DIR]\nweb-codex-client status|pair-open|pair-list [--state DIR] [--config FILE]\nweb-codex-client pair-approve --fingerprint FULL_HASH --root PATH [--write] [--state DIR]\nweb-codex-client revoke --peer PUBLIC_KEY [--state DIR]\nweb-codex-client probe [--transport unix|proxy] [--socket PATH] [--binary PATH] [--quiet] [--report FILE] (default: unix; --standalone starts an isolated stdio server)\nDefault config: " + defaultConfigPath()
|
|
3106
|
+
"web-codex-client configure [--config FILE] [--relay wss://HOST --token-stdin] [--socket PATH]\nweb-codex-client doctor [--config FILE] [--quiet] [--report FILE]\nweb-codex-client run [--config FILE]\nweb-codex-client pair [--root PATH] [--read-only] [--config FILE] (\u4EA4\u4E92\u6838\u5BF9\u6307\u7EB9\uFF1BCodex \u5DF2\u6709\u9879\u76EE\u9ED8\u8BA4\u8BFB\u5199)\nweb-codex-client authorize --root PATH [--peer PUBLIC_KEY] (\u65B0\u589E\u76EE\u5F55\uFF0C\u672C\u673A\u786E\u8BA4\u540E\u8BFB\u5199)\nweb-codex-client projects-known [--config FILE]\nweb-codex-client init [--state DIR]\nweb-codex-client status|pair-open|pair-list [--state DIR] [--config FILE]\nweb-codex-client pair-approve --fingerprint FULL_HASH [--root PATH] [--write|--read-only] [--state DIR]\nweb-codex-client revoke --peer PUBLIC_KEY [--state DIR]\nweb-codex-client probe [--transport unix|proxy] [--socket PATH] [--binary PATH] [--quiet] [--report FILE] (default: unix; --standalone starts an isolated stdio server)\nDefault config: " + defaultConfigPath()
|
|
2925
3107
|
);
|
|
2926
3108
|
return;
|
|
2927
3109
|
}
|
|
@@ -2967,7 +3149,7 @@ async function main() {
|
|
|
2967
3149
|
const daemon = new ClientDaemon(config, state.identity, state.grants);
|
|
2968
3150
|
await daemon.start();
|
|
2969
3151
|
console.log(
|
|
2970
|
-
"Client running
|
|
3152
|
+
"Client running. \u5728\u7F51\u9875\u9009\u62E9\u8BBE\u5907\u540E\uFF0C\u53E6\u5F00\u7EC8\u7AEF\u6267\u884C web-codex-client pair \u6838\u5BF9\u5E76\u4FE1\u4EFB\u6D4F\u89C8\u5668\u3002"
|
|
2971
3153
|
);
|
|
2972
3154
|
let stopping = false;
|
|
2973
3155
|
const stop = () => {
|
|
@@ -2979,21 +3161,55 @@ async function main() {
|
|
|
2979
3161
|
process.on("SIGTERM", stop);
|
|
2980
3162
|
return;
|
|
2981
3163
|
}
|
|
2982
|
-
if (!["status", "pair-open", "pair-list", "pair-approve", "revoke"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
|
|
3164
|
+
if (!["status", "pair", "pair-open", "pair-list", "pair-approve", "revoke", "authorize", "projects-known"].includes(command)) throw Error("UNKNOWN_COMMAND: use --help");
|
|
2983
3165
|
const stateDir = values.state ? resolve4(values.state) : (await readConfig(configPath)).stateDir;
|
|
3166
|
+
const send = async (request2) => {
|
|
3167
|
+
const reply = await control(stateDir, request2);
|
|
3168
|
+
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"));
|
|
3169
|
+
return reply.result;
|
|
3170
|
+
};
|
|
3171
|
+
if (command === "pair") {
|
|
3172
|
+
const list = await send({ command: "pair-list" });
|
|
3173
|
+
const pending2 = list.pending.filter((p) => (p.remainingMs ?? p.expiresAt - Date.now()) > 0);
|
|
3174
|
+
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");
|
|
3175
|
+
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"}`);
|
|
3176
|
+
const selected = pending2.length === 1 ? pending2[0] : pending2[Number(await question("\u9009\u62E9\u6D4F\u89C8\u5668\u7F16\u53F7\uFF1A")) - 1];
|
|
3177
|
+
if (!selected) throw Error("INVALID_SELECTION");
|
|
3178
|
+
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");
|
|
3179
|
+
const access = values["read-only"] ? "\u53EA\u8BFB" : "\u8BFB\u5199";
|
|
3180
|
+
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`);
|
|
3181
|
+
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");
|
|
3182
|
+
console.log("\u6B63\u5728\u8BFB\u53D6\u672C\u673A\u9879\u76EE\u5E76\u4FDD\u5B58\u6388\u6743\u2026");
|
|
3183
|
+
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 } : {} });
|
|
3184
|
+
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`);
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
if (command === "authorize") {
|
|
3188
|
+
if (!values.root?.length) throw Error("ROOT_REQUIRED: \u8BF7\u7528 --root \u6307\u5B9A\u76EE\u5F55");
|
|
3189
|
+
const request2 = { command, roots: values.root.map((root) => resolve4(root)), peer: values.peer };
|
|
3190
|
+
let reply = await control(stateDir, request2);
|
|
3191
|
+
if (reply.error === "DIRECTORY_APPROVAL_REQUIRED") {
|
|
3192
|
+
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(", "));
|
|
3193
|
+
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");
|
|
3194
|
+
reply = await control(stateDir, { ...request2, allowUnknown: true });
|
|
3195
|
+
}
|
|
3196
|
+
console.log(JSON.stringify(reply));
|
|
3197
|
+
if (reply.error) process.exitCode = 1;
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
2984
3200
|
const request = {
|
|
2985
3201
|
command,
|
|
2986
3202
|
fingerprint: values.fingerprint,
|
|
2987
3203
|
peer: values.peer,
|
|
2988
|
-
roots: values.root,
|
|
2989
|
-
write: values
|
|
3204
|
+
roots: values.root?.map((root) => resolve4(root)),
|
|
3205
|
+
write: values["read-only"] ? false : values.write
|
|
2990
3206
|
};
|
|
2991
3207
|
const result = await control(stateDir, request);
|
|
2992
3208
|
console.log(JSON.stringify(result));
|
|
2993
3209
|
if (result.error) process.exitCode = 1;
|
|
2994
3210
|
}
|
|
2995
3211
|
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");
|
|
3212
|
+
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
3213
|
process.exitCode = 1;
|
|
2998
3214
|
});
|
|
2999
3215
|
//# sourceMappingURL=cli.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@landh93/web-codex-client",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
4
|
"description": "Encrypted local Codex bridge with explicit browser pairing and workspace authorization",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": { "web-codex-client": "dist/cli.js" },
|