@cjhyy/code-shell-server 0.9.51 → 0.10.1
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/dist/account/cli.d.ts +23 -0
- package/dist/account/cli.js +249 -0
- package/dist/account/cloud-contract.d.ts +39 -0
- package/dist/account/cloud-contract.js +134 -0
- package/dist/account/cloud-types.d.ts +47 -0
- package/dist/account/cloud-types.js +1 -0
- package/dist/account/feature-service.d.ts +47 -0
- package/dist/account/feature-service.js +184 -0
- package/dist/account/http.d.ts +17 -0
- package/dist/account/http.js +158 -0
- package/dist/account/links.d.ts +13 -0
- package/dist/account/links.js +137 -0
- package/dist/account/manager.d.ts +96 -0
- package/dist/account/manager.js +976 -0
- package/dist/account/native-lock.d.ts +6 -0
- package/dist/account/native-lock.js +26 -0
- package/dist/account/native-store.d.ts +32 -0
- package/dist/account/native-store.js +162 -0
- package/dist/account/profile.d.ts +4 -0
- package/dist/account/profile.js +46 -0
- package/dist/account/store.d.ts +34 -0
- package/dist/account/store.js +118 -0
- package/dist/account/types.d.ts +138 -0
- package/dist/account/types.js +2 -0
- package/dist/desktop-web/http-api.d.ts +4 -0
- package/dist/desktop-web/http-api.js +12 -4
- package/dist/environment-identity.d.ts +1 -1
- package/dist/environment-identity.js +2 -1
- package/dist/headless-worker-env.d.ts +2 -0
- package/dist/headless-worker-env.js +15 -0
- package/dist/hub/approval-lease.js +6 -1
- package/dist/hub/configuration.d.ts +11 -0
- package/dist/hub/configuration.js +34 -1
- package/dist/hub/model-parameter-metadata.d.ts +26 -0
- package/dist/hub/model-parameter-metadata.js +92 -0
- package/dist/hub/run-replay.js +1 -1
- package/dist/hub/session-management.js +1 -1
- package/dist/index.account.d.ts +12 -0
- package/dist/index.account.js +12 -0
- package/dist/index.mobile-remote.d.ts +2 -0
- package/dist/index.mobile-remote.js +1 -0
- package/dist/index.worker.d.ts +1 -0
- package/dist/index.worker.js +1 -0
- package/dist/mobile-remote/account-relay-access.d.ts +59 -0
- package/dist/mobile-remote/account-relay-access.js +174 -0
- package/dist/mobile-remote/approval-clock.d.ts +3 -0
- package/dist/mobile-remote/approval-clock.js +15 -0
- package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
- package/dist/mobile-remote/codex-room-agent.js +17 -1
- package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
- package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
- package/dist/mobile-remote/mobile-upload-service.js +25 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
- package/dist/mobile-remote/remote-host-manager.js +142 -25
- package/dist/mobile-remote/resident-agent.d.ts +14 -0
- package/dist/mobile-remote/resident-agent.js +38 -11
- package/dist/mobile-remote/room-history.d.ts +7 -0
- package/dist/mobile-remote/room-history.js +56 -1
- package/dist/mobile-remote/room-manager.d.ts +216 -9
- package/dist/mobile-remote/room-manager.js +700 -65
- package/dist/mobile-remote/room-process-stop.d.ts +15 -0
- package/dist/mobile-remote/room-process-stop.js +41 -0
- package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
- package/dist/mobile-remote/trusted-device-store.js +54 -1
- package/dist/mobile-remote/turn-queue.d.ts +183 -0
- package/dist/mobile-remote/turn-queue.js +422 -0
- package/dist/mobile-remote/types.d.ts +1 -1
- package/dist/panels/agent-task-host.js +6 -12
- package/dist/panels/agent-task-service.d.ts +2 -0
- package/dist/panels/agent-task-service.js +7 -0
- package/dist/panels/browser-capture.d.ts +1 -15
- package/dist/panels/browser-capture.js +1 -35
- package/dist/panels/directory-picker.d.ts +48 -0
- package/dist/panels/directory-picker.js +247 -0
- package/dist/panels/http.d.ts +15 -15
- package/dist/panels/management-http.d.ts +9 -9
- package/dist/panels/process-service.d.ts +5 -4
- package/dist/panels/process-service.js +54 -16
- package/dist/panels/runtime.js +148 -30
- package/dist/panels/tool-jobs-storage.d.ts +4 -0
- package/dist/panels/tool-jobs-storage.js +111 -34
- package/dist/panels/types.d.ts +1 -97
- package/dist/remote-relay/connector.d.ts +8 -0
- package/dist/remote-relay/connector.js +29 -2
- package/dist/remote-relay/protocol.d.ts +21 -0
- package/dist/remote-relay/protocol.js +31 -0
- package/dist/serve/cli.js +6 -0
- package/dist/serve/headless-server.js +24 -19
- package/dist/sessions-service.d.ts +4 -0
- package/dist/sessions-service.js +14 -1
- package/package.json +10 -6
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { accountPublicOrigin, readAccountFeatures, resolveAccountFeatures, } from "@cjhyy/code-shell-web/account";
|
|
2
|
+
import { DEFAULT_CLOUD_ACCOUNT_ORIGIN, } from "./types.js";
|
|
3
|
+
/** Host-owned cache, bound to an account generation. Snapshot reads never perform I/O. */
|
|
4
|
+
export class AccountFeatureService {
|
|
5
|
+
deps;
|
|
6
|
+
origin;
|
|
7
|
+
subject = "";
|
|
8
|
+
generation = 0;
|
|
9
|
+
deployment;
|
|
10
|
+
account;
|
|
11
|
+
reachable = false;
|
|
12
|
+
controller;
|
|
13
|
+
inFlight;
|
|
14
|
+
lastAttempt = 0;
|
|
15
|
+
timer;
|
|
16
|
+
disposed = false;
|
|
17
|
+
listeners = new Set();
|
|
18
|
+
unsubscribe;
|
|
19
|
+
constructor(deps) {
|
|
20
|
+
this.deps = deps;
|
|
21
|
+
this.origin = deps.origin ?? DEFAULT_CLOUD_ACCOUNT_ORIGIN;
|
|
22
|
+
// Desktop composes this before Electron ready; keep OS credential loading lazy.
|
|
23
|
+
this.unsubscribe = deps.account.onStatus(() => {
|
|
24
|
+
this.synchronize();
|
|
25
|
+
this.publish();
|
|
26
|
+
if (!this.disposed)
|
|
27
|
+
void this.refresh();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
synchronize() {
|
|
31
|
+
const status = this.deps.account.status();
|
|
32
|
+
const origin = status.origin ?? this.origin;
|
|
33
|
+
const subject = JSON.stringify([origin, status.account?.id, status.state]);
|
|
34
|
+
if (subject !== this.subject) {
|
|
35
|
+
++this.generation;
|
|
36
|
+
this.controller?.abort();
|
|
37
|
+
this.inFlight = undefined;
|
|
38
|
+
if (origin !== this.origin)
|
|
39
|
+
this.deployment = undefined;
|
|
40
|
+
this.origin = origin;
|
|
41
|
+
this.subject = subject;
|
|
42
|
+
this.account = undefined;
|
|
43
|
+
this.reachable = false;
|
|
44
|
+
this.lastAttempt = 0;
|
|
45
|
+
}
|
|
46
|
+
return status;
|
|
47
|
+
}
|
|
48
|
+
snapshot() {
|
|
49
|
+
if (this.disposed)
|
|
50
|
+
return resolveAccountFeatures({ signedIn: false, supported: [] });
|
|
51
|
+
const status = this.synchronize();
|
|
52
|
+
return this.resolve(status, this.deps.local?.(status));
|
|
53
|
+
}
|
|
54
|
+
resolve(status, local) {
|
|
55
|
+
return resolveAccountFeatures({
|
|
56
|
+
deployment: this.deployment,
|
|
57
|
+
account: this.account,
|
|
58
|
+
signedIn: status.state === "signed-in" && !!status.account,
|
|
59
|
+
supported: this.deps.supported,
|
|
60
|
+
reachable: this.reachable,
|
|
61
|
+
storageError: status.state === "storage-error",
|
|
62
|
+
local,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
onChanged(listener) {
|
|
66
|
+
this.listeners.add(listener);
|
|
67
|
+
return () => {
|
|
68
|
+
this.listeners.delete(listener);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
publish() {
|
|
72
|
+
const snapshot = this.snapshot();
|
|
73
|
+
for (const listener of this.listeners) {
|
|
74
|
+
try {
|
|
75
|
+
listener(snapshot);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* One detached host cannot stop others. */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
localChanged() {
|
|
83
|
+
if (!this.disposed)
|
|
84
|
+
this.publish();
|
|
85
|
+
}
|
|
86
|
+
/** Checks remote entitlement before a setup action, independent of its local prerequisite. */
|
|
87
|
+
requireEntitled(id) {
|
|
88
|
+
if (this.disposed)
|
|
89
|
+
throw new Error("账号功能服务已关闭。");
|
|
90
|
+
const status = this.synchronize();
|
|
91
|
+
this.requireView(this.resolve(status)[id].state);
|
|
92
|
+
}
|
|
93
|
+
requireReady(id) {
|
|
94
|
+
this.requireView(this.snapshot()[id].state);
|
|
95
|
+
}
|
|
96
|
+
requireView(state) {
|
|
97
|
+
if (this.disposed)
|
|
98
|
+
throw new Error("账号功能服务已关闭。");
|
|
99
|
+
if (state === "sign-in")
|
|
100
|
+
throw new Error("请先登录 CodeShell 账号。");
|
|
101
|
+
if (state === "pending")
|
|
102
|
+
throw new Error("账号功能尚未准备好,请检查连接后重试。");
|
|
103
|
+
if (state !== "ready")
|
|
104
|
+
throw new Error("当前账号服务未开放此功能。");
|
|
105
|
+
}
|
|
106
|
+
refresh(origin, minIntervalMs = 0) {
|
|
107
|
+
if (this.disposed)
|
|
108
|
+
return Promise.resolve(this.snapshot());
|
|
109
|
+
let status = this.synchronize();
|
|
110
|
+
if (origin !== undefined) {
|
|
111
|
+
if (!accountPublicOrigin(origin))
|
|
112
|
+
return Promise.reject(new Error("无效的账号服务地址。"));
|
|
113
|
+
if (status.origin && status.state === "signed-in" && origin !== status.origin)
|
|
114
|
+
return Promise.reject(new Error("请先退出当前账号,再切换账号服务。"));
|
|
115
|
+
if (origin !== this.origin) {
|
|
116
|
+
this.origin = origin;
|
|
117
|
+
this.deployment = undefined;
|
|
118
|
+
this.subject = "";
|
|
119
|
+
status = this.synchronize();
|
|
120
|
+
this.publish();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (this.inFlight)
|
|
124
|
+
return this.inFlight;
|
|
125
|
+
const now = this.deps.now?.() ?? Date.now();
|
|
126
|
+
if (minIntervalMs > 0 && this.lastAttempt && now - this.lastAttempt < minIntervalMs)
|
|
127
|
+
return Promise.resolve(this.snapshot());
|
|
128
|
+
this.lastAttempt = now;
|
|
129
|
+
if (!this.timer) {
|
|
130
|
+
this.timer = setInterval(() => {
|
|
131
|
+
void this.refresh();
|
|
132
|
+
}, 600_000);
|
|
133
|
+
this.timer.unref?.();
|
|
134
|
+
}
|
|
135
|
+
const generation = this.generation;
|
|
136
|
+
const targetOrigin = this.origin;
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
this.controller = controller;
|
|
139
|
+
const current = () => !this.disposed && !controller.signal.aborted && generation === this.generation;
|
|
140
|
+
const pending = Promise.resolve().then(async () => {
|
|
141
|
+
try {
|
|
142
|
+
const publicValue = await this.deps.account.capabilities(targetOrigin, controller.signal);
|
|
143
|
+
if (!current())
|
|
144
|
+
return this.snapshot();
|
|
145
|
+
const deployment = readAccountFeatures(publicValue);
|
|
146
|
+
this.deployment = deployment;
|
|
147
|
+
if (status.state === "signed-in" && status.account) {
|
|
148
|
+
const value = await this.deps.account.overview({ origin: targetOrigin, accountId: status.account.id }, current, controller.signal);
|
|
149
|
+
if (!current())
|
|
150
|
+
return this.snapshot();
|
|
151
|
+
this.account = readAccountFeatures(value);
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
this.account = undefined;
|
|
155
|
+
this.reachable = true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
if (current())
|
|
159
|
+
this.reachable = false;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
if (current()) {
|
|
163
|
+
this.inFlight = undefined;
|
|
164
|
+
this.publish();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return this.snapshot();
|
|
168
|
+
});
|
|
169
|
+
this.inFlight = pending;
|
|
170
|
+
return pending;
|
|
171
|
+
}
|
|
172
|
+
dispose() {
|
|
173
|
+
this.disposed = true;
|
|
174
|
+
this.reachable = false;
|
|
175
|
+
this.account = undefined;
|
|
176
|
+
this.deployment = undefined;
|
|
177
|
+
this.inFlight = undefined;
|
|
178
|
+
++this.generation;
|
|
179
|
+
this.controller?.abort();
|
|
180
|
+
clearInterval(this.timer);
|
|
181
|
+
this.unsubscribe();
|
|
182
|
+
this.listeners.clear();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConnectionOptions } from "node:tls";
|
|
2
|
+
export declare class CloudAccountHttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code?: string;
|
|
5
|
+
constructor(status: number, path?: string, code?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export interface CloudAccountRequest {
|
|
8
|
+
origin: string;
|
|
9
|
+
path: string;
|
|
10
|
+
method?: "GET" | "POST" | "DELETE" | "PATCH";
|
|
11
|
+
body?: unknown;
|
|
12
|
+
token?: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
export type CloudAccountTransport = (input: CloudAccountRequest) => Promise<unknown>;
|
|
16
|
+
/** Exact-origin HTTPS, bounded JSON, no redirect/cookie inheritance or raw errors. */
|
|
17
|
+
export declare function createCloudAccountTransport(ca?: ConnectionOptions["ca"]): CloudAccountTransport;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { request } from "node:https";
|
|
2
|
+
import { isRelayOrigin } from "../remote-relay/protocol.js";
|
|
3
|
+
function accountErrorMessage(status, path, code) {
|
|
4
|
+
if (status === 403 && code === "feature_not_entitled")
|
|
5
|
+
return "此账号功能当前不可用,请刷新功能状态。";
|
|
6
|
+
if (path === "/api/v1/account/profile") {
|
|
7
|
+
if (status === 400 && code === "invalid_display_name")
|
|
8
|
+
return "昵称需为 1–64 个字符,不能包含控制字符。";
|
|
9
|
+
if (status === 400 && code === "invalid_username")
|
|
10
|
+
return "用户名需为 3–64 位字母、数字、点、下划线或连字符,并以字母或数字开头。";
|
|
11
|
+
if (status === 409 && code === "username_unavailable")
|
|
12
|
+
return "此用户名已被使用,请换一个用户名。";
|
|
13
|
+
if (status === 404)
|
|
14
|
+
return "此云服务尚未启用账号资料编辑。";
|
|
15
|
+
}
|
|
16
|
+
if (path === "/api/v1/account/links" || path?.startsWith("/api/v1/account/links/")) {
|
|
17
|
+
if (status === 503 && code === "link_management_disabled")
|
|
18
|
+
return "此云服务尚未启用连接管理。";
|
|
19
|
+
if (status === 503 && code === "link_management_view_too_large")
|
|
20
|
+
return "连接管理列表超过服务上限,请联系服务管理员。";
|
|
21
|
+
if (status === 404 && code === "link_item_not_found")
|
|
22
|
+
return "此连接或授权不可用,请重新加载列表。";
|
|
23
|
+
if (status === 502)
|
|
24
|
+
return "连接服务暂时不可用,请稍后重新加载。";
|
|
25
|
+
}
|
|
26
|
+
const register = path === "/api/v1/account/register";
|
|
27
|
+
const login = path === "/api/v1/account/login";
|
|
28
|
+
if ((register || login) && status === 400) {
|
|
29
|
+
if (code === "invalid_username")
|
|
30
|
+
return "用户名需为 3–64 位字母、数字、点、下划线或连字符,并以字母或数字开头。";
|
|
31
|
+
if (code === "invalid_password")
|
|
32
|
+
return "密码需为 12–1024 位字符。";
|
|
33
|
+
}
|
|
34
|
+
if (register && status === 409 && code === "username_unavailable")
|
|
35
|
+
return "此用户名已被使用,请换一个用户名。";
|
|
36
|
+
if (login && status === 401 && code === "invalid_credentials")
|
|
37
|
+
return "用户名或密码错误,请重新输入。";
|
|
38
|
+
if ((register || path === "/api/v1/account/github/poll") &&
|
|
39
|
+
status === 403 &&
|
|
40
|
+
code === "registration_disabled")
|
|
41
|
+
return "此云服务暂未开放注册,请登录已有账号。";
|
|
42
|
+
const github = ["status", "start", "poll", "cancel"].some((action) => path === `/api/v1/account/github/${action}`);
|
|
43
|
+
if (github && status === 503) {
|
|
44
|
+
if (code === "github_disabled")
|
|
45
|
+
return "此云服务尚未配置 GitHub 登录,请使用账号密码。";
|
|
46
|
+
if (code === "github_unavailable")
|
|
47
|
+
return "GitHub 登录暂时不可用,请稍后重试。";
|
|
48
|
+
}
|
|
49
|
+
if (path === "/api/v1/account/github/poll") {
|
|
50
|
+
if (status === 409 && code === "provider_already_linked")
|
|
51
|
+
return "GitHub 关联冲突:当前账号或此 GitHub 已有其他关联。";
|
|
52
|
+
if (status === 401 && code === "github_login_denied")
|
|
53
|
+
return "GitHub 登录未获授权,请重新发起登录。";
|
|
54
|
+
if (status === 401 && code === "invalid_github_receipt")
|
|
55
|
+
return "GitHub 登录请求已过期,请重新发起登录。";
|
|
56
|
+
if (status === 502 && code === "github_login_failed")
|
|
57
|
+
return "GitHub 登录未完成,请重新发起登录。";
|
|
58
|
+
}
|
|
59
|
+
const browser = ["start", "poll", "cancel"].some((action) => path === `/api/v1/account/browser/${action}`);
|
|
60
|
+
if (browser) {
|
|
61
|
+
if (status === 400 &&
|
|
62
|
+
(code === "invalid_browser_login_request" || code === "invalid_device_name"))
|
|
63
|
+
return "网页登录请求无效,请重新发起登录。";
|
|
64
|
+
if (status === 401 && code === "invalid_browser_receipt")
|
|
65
|
+
return "网页登录请求已失效,请重新发起登录。";
|
|
66
|
+
if (status === 429 && code === "browser_login_limit")
|
|
67
|
+
return "网页登录请求过多,请稍后重试。";
|
|
68
|
+
if (path === "/api/v1/account/browser/poll") {
|
|
69
|
+
if (status === 401 && code === "browser_login_expired")
|
|
70
|
+
return "网页账号登录已失效,请在浏览器重新登录后重试。";
|
|
71
|
+
if (status === 403 && code === "browser_login_denied")
|
|
72
|
+
return "已取消此次桌面登录。如需登录,请重新发起。";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return status === 401 || status === 403
|
|
76
|
+
? "云账号授权已失效,请重新登录。"
|
|
77
|
+
: status === 409
|
|
78
|
+
? "账号或关联已存在冲突,请检查当前账号。"
|
|
79
|
+
: status === 429
|
|
80
|
+
? "请求过于频繁,请稍后重试。"
|
|
81
|
+
: "云服务请求未完成,请检查服务地址和网络。";
|
|
82
|
+
}
|
|
83
|
+
export class CloudAccountHttpError extends Error {
|
|
84
|
+
status;
|
|
85
|
+
code;
|
|
86
|
+
constructor(status, path, code) {
|
|
87
|
+
super(accountErrorMessage(status, path, code));
|
|
88
|
+
this.status = status;
|
|
89
|
+
if (code === "feature_not_entitled" || code === "authentication_required")
|
|
90
|
+
this.code = code;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Exact-origin HTTPS, bounded JSON, no redirect/cookie inheritance or raw errors. */
|
|
94
|
+
export function createCloudAccountTransport(ca) {
|
|
95
|
+
return (input) => {
|
|
96
|
+
if (!isRelayOrigin(input.origin) || !/^\/api\/v1\/(?:account|remote-hosts)\//.test(input.path))
|
|
97
|
+
return Promise.reject(new Error("无效的云服务地址。"));
|
|
98
|
+
if (input.method === "PATCH" && input.path !== "/api/v1/account/profile")
|
|
99
|
+
return Promise.reject(new Error("云账号资料请求无效。"));
|
|
100
|
+
if (input.method === "DELETE" &&
|
|
101
|
+
(!/^\/api\/v1\/account\/links\/(?:connections|grants)\/[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(input.path) ||
|
|
102
|
+
input.body !== undefined))
|
|
103
|
+
return Promise.reject(new Error("无效的云账号撤销请求。"));
|
|
104
|
+
const maxResponseBytes = input.method === "GET" && input.path === "/api/v1/account/links" ? 1_048_576 : 16_384;
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const bytes = input.body === undefined ? undefined : Buffer.from(JSON.stringify(input.body));
|
|
107
|
+
const req = request(new URL(input.path, input.origin), {
|
|
108
|
+
method: input.method ?? "POST",
|
|
109
|
+
ca,
|
|
110
|
+
signal: input.signal,
|
|
111
|
+
headers: {
|
|
112
|
+
Origin: input.origin,
|
|
113
|
+
Accept: "application/json",
|
|
114
|
+
...(input.token ? { Authorization: `Bearer ${input.token}` } : {}),
|
|
115
|
+
...(bytes
|
|
116
|
+
? { "Content-Type": "application/json", "Content-Length": bytes.length }
|
|
117
|
+
: {}),
|
|
118
|
+
},
|
|
119
|
+
}, (res) => {
|
|
120
|
+
const chunks = [];
|
|
121
|
+
let size = 0;
|
|
122
|
+
res.on("data", (chunk) => {
|
|
123
|
+
size += chunk.length;
|
|
124
|
+
if (size > maxResponseBytes)
|
|
125
|
+
req.destroy(new Error("response too large"));
|
|
126
|
+
else
|
|
127
|
+
chunks.push(chunk);
|
|
128
|
+
});
|
|
129
|
+
res.on("error", () => reject(new CloudAccountHttpError(0)));
|
|
130
|
+
res.on("end", () => {
|
|
131
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
132
|
+
let code;
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
135
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
136
|
+
code = value.error;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Invalid error bodies retain their HTTP status and safe fallback text.
|
|
140
|
+
}
|
|
141
|
+
reject(new CloudAccountHttpError(res.statusCode ?? 0, input.path, code));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
reject(new CloudAccountHttpError(0));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
const timer = setTimeout(() => req.destroy(new Error("deadline")), 15_000);
|
|
153
|
+
req.on("close", () => clearTimeout(timer));
|
|
154
|
+
req.on("error", () => reject(new CloudAccountHttpError(0)));
|
|
155
|
+
req.end(bytes);
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CloudAccountLinksRequest, CloudAccountLinkView, CloudAccountGitHubRequest } from "./types.js";
|
|
2
|
+
export declare function accountGitHubRequest(value: unknown): CloudAccountGitHubRequest;
|
|
3
|
+
export declare function accountLinksRequest(value: unknown): CloudAccountLinksRequest;
|
|
4
|
+
/** Explicit projection strips unknown fields instead of forwarding server objects to the renderer. */
|
|
5
|
+
export declare function accountLinkView(value: unknown): CloudAccountLinkView;
|
|
6
|
+
export declare function accountLinkRevocation(value: unknown, input: Extract<CloudAccountLinksRequest, {
|
|
7
|
+
action: "revoke";
|
|
8
|
+
}>): {
|
|
9
|
+
action: "revoke";
|
|
10
|
+
revoked: true;
|
|
11
|
+
kind: "connections" | "grants";
|
|
12
|
+
id: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { isRelayOrigin } from "../remote-relay/protocol.js";
|
|
2
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
3
|
+
const invalid = () => new Error("云服务返回了无效的连接管理信息。");
|
|
4
|
+
function object(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6
|
+
throw invalid();
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function text(value, max) {
|
|
10
|
+
if (typeof value !== "string" ||
|
|
11
|
+
!value.length ||
|
|
12
|
+
value.length > max ||
|
|
13
|
+
/[\x00-\x1f\x7f]/.test(value))
|
|
14
|
+
throw invalid();
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function id(value) {
|
|
18
|
+
if (typeof value !== "string" || !uuid.test(value))
|
|
19
|
+
throw invalid();
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function flag(value) {
|
|
23
|
+
if (typeof value !== "boolean")
|
|
24
|
+
throw invalid();
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function strings(value, maxLength) {
|
|
28
|
+
if (!Array.isArray(value) || value.length > 100)
|
|
29
|
+
throw invalid();
|
|
30
|
+
return value.map((item) => text(item, maxLength));
|
|
31
|
+
}
|
|
32
|
+
function keys(value, expected) {
|
|
33
|
+
if (Object.keys(value).length !== expected.length || expected.some((key) => !(key in value)))
|
|
34
|
+
throw new Error("云账号请求无效。");
|
|
35
|
+
}
|
|
36
|
+
export function accountGitHubRequest(value) {
|
|
37
|
+
const input = object(value);
|
|
38
|
+
if (input.mode === "link") {
|
|
39
|
+
keys(input, ["mode"]);
|
|
40
|
+
return { mode: "link" };
|
|
41
|
+
}
|
|
42
|
+
if (input.mode !== "login")
|
|
43
|
+
throw new Error("GitHub 登录请求无效。");
|
|
44
|
+
keys(input, ["mode", "input"]);
|
|
45
|
+
const login = object(input.input);
|
|
46
|
+
if (Object.keys(login).some((key) => !["origin", "deviceName", "connectGitHub"].includes(key)))
|
|
47
|
+
throw new Error("GitHub 登录请求无效。");
|
|
48
|
+
if (!isRelayOrigin(login.origin) ||
|
|
49
|
+
login.origin.length > 2048 ||
|
|
50
|
+
(login.deviceName !== undefined && typeof login.deviceName !== "string") ||
|
|
51
|
+
(login.connectGitHub !== undefined && typeof login.connectGitHub !== "boolean"))
|
|
52
|
+
throw new Error("GitHub 登录请求无效。");
|
|
53
|
+
return {
|
|
54
|
+
mode: "login",
|
|
55
|
+
input: {
|
|
56
|
+
origin: login.origin,
|
|
57
|
+
...(login.deviceName === undefined ? {} : { deviceName: login.deviceName }),
|
|
58
|
+
...(login.connectGitHub === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { connectGitHub: login.connectGitHub }),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function accountLinksRequest(value) {
|
|
65
|
+
const input = object(value);
|
|
66
|
+
if (!isRelayOrigin(input.origin) || input.origin.length > 2048)
|
|
67
|
+
throw new Error("云账号请求无效。");
|
|
68
|
+
const identity = { origin: input.origin, accountId: id(input.accountId) };
|
|
69
|
+
if (input.action === "list") {
|
|
70
|
+
keys(input, ["action", "origin", "accountId"]);
|
|
71
|
+
return { ...identity, action: "list" };
|
|
72
|
+
}
|
|
73
|
+
if (input.action !== "revoke" || !["connections", "grants"].includes(input.kind))
|
|
74
|
+
throw new Error("云账号请求无效。");
|
|
75
|
+
keys(input, ["action", "origin", "accountId", "kind", "id"]);
|
|
76
|
+
return {
|
|
77
|
+
...identity,
|
|
78
|
+
action: "revoke",
|
|
79
|
+
kind: input.kind,
|
|
80
|
+
id: id(input.id),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Explicit projection strips unknown fields instead of forwarding server objects to the renderer. */
|
|
84
|
+
export function accountLinkView(value) {
|
|
85
|
+
const view = object(value);
|
|
86
|
+
if (!isRelayOrigin(view.linkOrigin) ||
|
|
87
|
+
view.linkOrigin.length > 2048 ||
|
|
88
|
+
!Array.isArray(view.connections) ||
|
|
89
|
+
view.connections.length > 512 ||
|
|
90
|
+
!Array.isArray(view.grants) ||
|
|
91
|
+
view.grants.length > 4096)
|
|
92
|
+
throw invalid();
|
|
93
|
+
const connectionIds = new Set();
|
|
94
|
+
const grantIds = new Set();
|
|
95
|
+
const connections = view.connections.map((raw) => {
|
|
96
|
+
const item = object(raw);
|
|
97
|
+
const account = object(item.account);
|
|
98
|
+
const connectionId = id(item.id);
|
|
99
|
+
if (connectionIds.has(connectionId))
|
|
100
|
+
throw invalid();
|
|
101
|
+
connectionIds.add(connectionId);
|
|
102
|
+
return {
|
|
103
|
+
id: connectionId,
|
|
104
|
+
provider: text(item.provider, 64),
|
|
105
|
+
account: {
|
|
106
|
+
id: text(account.id, 256),
|
|
107
|
+
...(account.login === undefined ? {} : { login: text(account.login, 256) }),
|
|
108
|
+
...(account.label === undefined ? {} : { label: text(account.label, 256) }),
|
|
109
|
+
},
|
|
110
|
+
revoked: flag(item.revoked),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
const grants = view.grants.map((raw) => {
|
|
114
|
+
const item = object(raw);
|
|
115
|
+
const client = object(item.client);
|
|
116
|
+
const grantId = id(item.id);
|
|
117
|
+
const connectionId = id(item.connectionId);
|
|
118
|
+
if (grantIds.has(grantId) || !connectionIds.has(connectionId))
|
|
119
|
+
throw invalid();
|
|
120
|
+
grantIds.add(grantId);
|
|
121
|
+
return {
|
|
122
|
+
id: grantId,
|
|
123
|
+
connectionId,
|
|
124
|
+
client: { id: id(client.id), name: text(client.name, 100) },
|
|
125
|
+
scopes: strings(item.scopes, 128),
|
|
126
|
+
resources: strings(item.resources, 2048),
|
|
127
|
+
revoked: flag(item.revoked),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
return { linkOrigin: view.linkOrigin, connections, grants };
|
|
131
|
+
}
|
|
132
|
+
export function accountLinkRevocation(value, input) {
|
|
133
|
+
const result = object(value);
|
|
134
|
+
if (result.revoked !== true || result.kind !== input.kind || result.id !== input.id)
|
|
135
|
+
throw invalid();
|
|
136
|
+
return { action: "revoke", revoked: true, kind: input.kind, id: input.id };
|
|
137
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type CloudManagementTicket } from "./cloud-contract.js";
|
|
2
|
+
import type { CloudAccountCapabilities, CloudAccountBrowserLogin, CloudAccountOverview, CloudAccountGitHubLogin, CloudAccountLogin, CloudAccountStatus, CloudAccountLogoutResult, CloudAccountLinksRequest, CloudAccountLinksResult, CloudAccountProfileRequest } from "./types.js";
|
|
3
|
+
import { CloudAccountStore } from "./store.js";
|
|
4
|
+
import { type CloudAccountTransport } from "./http.js";
|
|
5
|
+
/** Main-only continuation of the independent Link authorization job. */
|
|
6
|
+
export interface PreparedGitHubLink {
|
|
7
|
+
authorizationUrl: string;
|
|
8
|
+
continue(url: string): Promise<"connected" | "cancelled">;
|
|
9
|
+
cancel(): void;
|
|
10
|
+
}
|
|
11
|
+
/** Optional cloud identity. Construction/status never initiate network activity. */
|
|
12
|
+
export declare class CloudAccountManager {
|
|
13
|
+
private readonly deps;
|
|
14
|
+
private loaded;
|
|
15
|
+
private disposed;
|
|
16
|
+
private readonly statusListeners;
|
|
17
|
+
private saved?;
|
|
18
|
+
private state;
|
|
19
|
+
private generation;
|
|
20
|
+
private operation;
|
|
21
|
+
private refresh?;
|
|
22
|
+
private refreshResponse?;
|
|
23
|
+
private readonly revocationResponses;
|
|
24
|
+
private readonly request;
|
|
25
|
+
private githubConnection?;
|
|
26
|
+
private pendingGitHubLink?;
|
|
27
|
+
private profileRevision;
|
|
28
|
+
private profileRequestRevision;
|
|
29
|
+
private profileUpdate?;
|
|
30
|
+
private activation?;
|
|
31
|
+
private expiryTimer?;
|
|
32
|
+
constructor(deps: {
|
|
33
|
+
store: Pick<CloudAccountStore, "preflight" | "load" | "save" | "forget">;
|
|
34
|
+
request?: CloudAccountTransport;
|
|
35
|
+
openExternal: (url: string) => Promise<void>;
|
|
36
|
+
prepareGitHubLink?: (signal: AbortSignal, authorized: () => boolean) => Promise<PreparedGitHubLink>;
|
|
37
|
+
changed: (status: CloudAccountStatus) => void;
|
|
38
|
+
/** Retire only this account's remote access; never local work or LAN. */
|
|
39
|
+
retireRelay?: (identity: {
|
|
40
|
+
origin: string;
|
|
41
|
+
accountId: string;
|
|
42
|
+
}) => Promise<void>;
|
|
43
|
+
/** Explicit desktop login/resume enables account-owned device collaboration. */
|
|
44
|
+
activateRelay?: (identity: {
|
|
45
|
+
origin: string;
|
|
46
|
+
accountId: string;
|
|
47
|
+
}, authorized: () => boolean) => Promise<void>;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
50
|
+
});
|
|
51
|
+
private now;
|
|
52
|
+
private load;
|
|
53
|
+
status(): CloudAccountStatus;
|
|
54
|
+
onStatus(listener: (status: CloudAccountStatus) => void): () => void;
|
|
55
|
+
/** Cancel this host's work without revoking a saved account session. */
|
|
56
|
+
dispose(): void;
|
|
57
|
+
overview(input: {
|
|
58
|
+
origin: string;
|
|
59
|
+
accountId: string;
|
|
60
|
+
}, authorized?: () => boolean, signal?: AbortSignal): Promise<CloudAccountOverview>;
|
|
61
|
+
/** Public service discovery never reads or changes this device's account authority. */
|
|
62
|
+
capabilities(origin: string, signal?: AbortSignal): Promise<CloudAccountCapabilities>;
|
|
63
|
+
private relayCapability;
|
|
64
|
+
private armExpiry;
|
|
65
|
+
private activate;
|
|
66
|
+
/** The application explicitly restores saved authority once its main window is ready. */
|
|
67
|
+
resume(authorized?: () => boolean): Promise<CloudAccountStatus>;
|
|
68
|
+
private publish;
|
|
69
|
+
private invalidate;
|
|
70
|
+
private current;
|
|
71
|
+
private retire;
|
|
72
|
+
private validGrant;
|
|
73
|
+
private complete;
|
|
74
|
+
private revoke;
|
|
75
|
+
private prepare;
|
|
76
|
+
signIn(kind: "login" | "register", input: CloudAccountLogin, authorized?: () => boolean): Promise<CloudAccountStatus>;
|
|
77
|
+
/** The website confirms identity; only this Host can redeem and persist its own session. */
|
|
78
|
+
signInWithBrowser(input: CloudAccountBrowserLogin, authorized?: () => boolean): Promise<CloudAccountStatus>;
|
|
79
|
+
linkGitHub(authorized?: () => boolean): Promise<CloudAccountStatus>;
|
|
80
|
+
/** Every read/revoke is bound to the reviewed identity and the current local account incarnation. */
|
|
81
|
+
manageLinks(raw: CloudAccountLinksRequest, authorized?: () => boolean): Promise<CloudAccountLinksResult>;
|
|
82
|
+
profile(raw: CloudAccountProfileRequest, authorized?: () => boolean): Promise<CloudAccountStatus>;
|
|
83
|
+
signInWithGitHub(input: CloudAccountGitHubLogin, authorized?: () => boolean): Promise<CloudAccountStatus>;
|
|
84
|
+
private github;
|
|
85
|
+
private continueGitHubLink;
|
|
86
|
+
cancelSignIn(): void;
|
|
87
|
+
logout(): Promise<CloudAccountLogoutResult>;
|
|
88
|
+
/** Main-only root authority lease; refresh preserves it, logout/relogin invalidates it. */
|
|
89
|
+
captureRootAccount(origin: string, accountId: string, authorized: () => boolean): {
|
|
90
|
+
signal: AbortSignal;
|
|
91
|
+
current: () => void;
|
|
92
|
+
ticket: (targetOrigin: string, installationId: string) => Promise<CloudManagementTicket>;
|
|
93
|
+
};
|
|
94
|
+
/** Returns credentials only to main callers, for one exact service/account. */
|
|
95
|
+
getCredential(origin: string, accountId?: string, signal?: AbortSignal): Promise<string>;
|
|
96
|
+
}
|