@cjhyy/code-shell-server 0.9.52 → 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/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-client-event-validator.js +40 -1
- 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 +19 -4
- package/dist/mobile-remote/remote-host-manager.js +124 -17
- 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/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 +25 -14
- 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,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native credential stores are shared by all TUI/serve processes for this OS
|
|
3
|
+
* user. Lock the whole read/refresh/save/logout lifecycle, including prompts.
|
|
4
|
+
* No age-based recovery: a suspended process may still own a rotating token.
|
|
5
|
+
*/
|
|
6
|
+
export declare function acquireNativeAccountLock(origin: string, directory?: string): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { acquireWorkspaceWriteLock } from "@cjhyy/code-shell-core/internal";
|
|
6
|
+
import { accountRegistrationUrl } from "@cjhyy/code-shell-web/account";
|
|
7
|
+
/**
|
|
8
|
+
* Native credential stores are shared by all TUI/serve processes for this OS
|
|
9
|
+
* user. Lock the whole read/refresh/save/logout lifecycle, including prompts.
|
|
10
|
+
* No age-based recovery: a suspended process may still own a rotating token.
|
|
11
|
+
*/
|
|
12
|
+
export async function acquireNativeAccountLock(origin, directory = join(homedir(), ".code-shell", "accounts", "locks")) {
|
|
13
|
+
accountRegistrationUrl(origin);
|
|
14
|
+
const key = createHash("sha256").update(origin).digest("hex");
|
|
15
|
+
const parent = join(directory, key);
|
|
16
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
17
|
+
try {
|
|
18
|
+
return await acquireWorkspaceWriteLock(parent, "session");
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
if (cause.code !== "ELOCKED")
|
|
22
|
+
throw cause;
|
|
23
|
+
throw Object.assign(new Error("Another native account command owns this account service. Finish that command and retry. " +
|
|
24
|
+
`If it crashed, first stop all account commands for this origin, then remove the empty .lock directory inside ${parent}.`, { cause }), { code: "ELOCKED" });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SavedCloudAccount } from "./store.js";
|
|
2
|
+
export type NativeAccountStoreRunner = (command: string, args: string[], input?: string) => {
|
|
3
|
+
status: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
error?: unknown;
|
|
6
|
+
};
|
|
7
|
+
/** Explicit temporary account authority; lifetime is the owning CLI session. */
|
|
8
|
+
export declare class MemoryAccountStore {
|
|
9
|
+
private saved?;
|
|
10
|
+
preflight(): void;
|
|
11
|
+
load(): SavedCloudAccount | undefined;
|
|
12
|
+
save(value: SavedCloudAccount): void;
|
|
13
|
+
forget(): void;
|
|
14
|
+
}
|
|
15
|
+
/** OS credential storage only. Secrets travel through pipes, never command arguments. */
|
|
16
|
+
export declare class NativeAccountStore {
|
|
17
|
+
private readonly origin;
|
|
18
|
+
private readonly account;
|
|
19
|
+
private readonly platform;
|
|
20
|
+
private readonly run;
|
|
21
|
+
private readonly windows?;
|
|
22
|
+
constructor(origin: string, options?: {
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
run?: NativeAccountStoreRunner;
|
|
25
|
+
dataDir?: string;
|
|
26
|
+
});
|
|
27
|
+
private command;
|
|
28
|
+
preflight(): void;
|
|
29
|
+
load(): SavedCloudAccount | undefined;
|
|
30
|
+
save(saved: SavedCloudAccount): void;
|
|
31
|
+
forget(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { join, win32 } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { CloudAccountStore, validCloudAccountGrant } from "./store.js";
|
|
6
|
+
import { isRelayOrigin } from "../remote-relay/protocol.js";
|
|
7
|
+
const execute = (command, args, input) => {
|
|
8
|
+
const result = spawnSync(command, args, {
|
|
9
|
+
input,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
timeout: 15_000,
|
|
12
|
+
maxBuffer: 65_536,
|
|
13
|
+
windowsHide: true,
|
|
14
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
return { status: result.status, stdout: result.stdout ?? "", error: result.error };
|
|
17
|
+
};
|
|
18
|
+
const unavailable = () => new Error("系统安全存储不可用。可使用 --memory 保持当前进程内的账号会话;不会保存明文凭据。");
|
|
19
|
+
const validate = (saved, origin) => isRelayOrigin(origin) &&
|
|
20
|
+
validCloudAccountGrant(saved) &&
|
|
21
|
+
saved.kind !== "device" &&
|
|
22
|
+
saved.origin === origin &&
|
|
23
|
+
(saved.audience === undefined || saved.audience === origin);
|
|
24
|
+
/** Explicit temporary account authority; lifetime is the owning CLI session. */
|
|
25
|
+
export class MemoryAccountStore {
|
|
26
|
+
saved;
|
|
27
|
+
preflight() { }
|
|
28
|
+
load() {
|
|
29
|
+
return this.saved ? structuredClone(this.saved) : undefined;
|
|
30
|
+
}
|
|
31
|
+
save(value) {
|
|
32
|
+
if (!validate(value, value.origin))
|
|
33
|
+
throw new Error("无效的账号授权。");
|
|
34
|
+
this.saved = structuredClone(value);
|
|
35
|
+
}
|
|
36
|
+
forget() {
|
|
37
|
+
this.saved = undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** OS credential storage only. Secrets travel through pipes, never command arguments. */
|
|
41
|
+
export class NativeAccountStore {
|
|
42
|
+
origin;
|
|
43
|
+
account;
|
|
44
|
+
platform;
|
|
45
|
+
run;
|
|
46
|
+
windows;
|
|
47
|
+
constructor(origin, options = {}) {
|
|
48
|
+
this.origin = origin;
|
|
49
|
+
if (!isRelayOrigin(origin))
|
|
50
|
+
throw new Error("请输入完整的 HTTPS 账号服务地址。");
|
|
51
|
+
this.account = createHash("sha256").update(origin).digest("hex");
|
|
52
|
+
this.platform = options.platform ?? process.platform;
|
|
53
|
+
this.run = options.run ?? execute;
|
|
54
|
+
if (this.platform === "win32") {
|
|
55
|
+
const crypt = (operation, input) => {
|
|
56
|
+
const script = `Add-Type -AssemblyName System.Security; $data=[Convert]::FromBase64String([Console]::In.ReadToEnd()); $result=[Security.Cryptography.ProtectedData]::${operation}($data,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser); [Console]::Out.Write([Convert]::ToBase64String($result))`;
|
|
57
|
+
const result = this.run(win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], input);
|
|
58
|
+
if (result.error || result.status !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(result.stdout))
|
|
59
|
+
throw unavailable();
|
|
60
|
+
return Buffer.from(result.stdout, "base64");
|
|
61
|
+
};
|
|
62
|
+
this.windows = new CloudAccountStore(join(options.dataDir ?? join(homedir(), ".code-shell", "accounts"), `${this.account}.enc`), {
|
|
63
|
+
available: () => true,
|
|
64
|
+
encrypt: (text) => crypt("Protect", Buffer.from(text).toString("base64")),
|
|
65
|
+
decrypt: (bytes) => crypt("Unprotect", bytes.toString("base64")).toString("utf8"),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
command(operation, account, value) {
|
|
70
|
+
let result;
|
|
71
|
+
if (this.platform === "darwin") {
|
|
72
|
+
// security's interactive command parser receives only fixed identifiers + base64.
|
|
73
|
+
// The process argument list contains no password, grant, or token.
|
|
74
|
+
result =
|
|
75
|
+
operation === "write"
|
|
76
|
+
? this.run("/usr/bin/security", ["-i"], `add-generic-password -U -s codeshell-native-account -a ${account} -w ${value}\n`)
|
|
77
|
+
: this.run("/usr/bin/security", [
|
|
78
|
+
operation === "read" ? "find-generic-password" : "delete-generic-password",
|
|
79
|
+
"-s",
|
|
80
|
+
"codeshell-native-account",
|
|
81
|
+
"-a",
|
|
82
|
+
account,
|
|
83
|
+
...(operation === "read" ? ["-w"] : []),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
else if (this.platform === "linux") {
|
|
87
|
+
result = this.run("/usr/bin/secret-tool", operation === "write"
|
|
88
|
+
? [
|
|
89
|
+
"store",
|
|
90
|
+
"--label=CodeShell Account",
|
|
91
|
+
"service",
|
|
92
|
+
"codeshell-native-account",
|
|
93
|
+
"account",
|
|
94
|
+
account,
|
|
95
|
+
]
|
|
96
|
+
: [
|
|
97
|
+
operation === "read" ? "lookup" : "clear",
|
|
98
|
+
"service",
|
|
99
|
+
"codeshell-native-account",
|
|
100
|
+
"account",
|
|
101
|
+
account,
|
|
102
|
+
], value);
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
throw unavailable();
|
|
106
|
+
const missing = this.platform === "darwin" ? 44 : 1;
|
|
107
|
+
if (!result.error && result.status === missing && operation !== "write")
|
|
108
|
+
return;
|
|
109
|
+
if (result.error || result.status !== 0)
|
|
110
|
+
throw unavailable();
|
|
111
|
+
return result.stdout.trim();
|
|
112
|
+
}
|
|
113
|
+
preflight() {
|
|
114
|
+
if (this.windows)
|
|
115
|
+
return this.windows.preflight();
|
|
116
|
+
const account = `probe-${randomUUID()}`;
|
|
117
|
+
const value = Buffer.from(randomUUID()).toString("base64");
|
|
118
|
+
try {
|
|
119
|
+
this.command("write", account, value);
|
|
120
|
+
if (this.command("read", account) !== value)
|
|
121
|
+
throw unavailable();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.command("delete", account);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
load() {
|
|
128
|
+
const bytes = this.windows ? undefined : this.command("read", this.account);
|
|
129
|
+
if (!this.windows && bytes === undefined)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
if (bytes && bytes.length > 32_768)
|
|
133
|
+
throw unavailable();
|
|
134
|
+
const saved = this.windows
|
|
135
|
+
? this.windows.load()
|
|
136
|
+
: JSON.parse(Buffer.from(bytes, "base64").toString("utf8"));
|
|
137
|
+
if (saved === undefined && this.windows)
|
|
138
|
+
return;
|
|
139
|
+
if (!validate(saved, this.origin))
|
|
140
|
+
throw unavailable();
|
|
141
|
+
return saved;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error("账号安全存储读取失败,请退出后重新登录。");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
save(saved) {
|
|
148
|
+
if (!validate(saved, this.origin))
|
|
149
|
+
throw new Error("无效的账号授权。");
|
|
150
|
+
if (this.windows)
|
|
151
|
+
return this.windows.save(saved);
|
|
152
|
+
const encoded = Buffer.from(JSON.stringify(saved)).toString("base64");
|
|
153
|
+
this.command("write", this.account, encoded);
|
|
154
|
+
if (this.command("read", this.account) !== encoded)
|
|
155
|
+
throw unavailable();
|
|
156
|
+
}
|
|
157
|
+
forget() {
|
|
158
|
+
if (this.windows)
|
|
159
|
+
return this.windows.forget();
|
|
160
|
+
this.command("delete", this.account);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CloudAccountIdentity, CloudAccountProfileRequest } from "./types.js";
|
|
2
|
+
export declare function accountProfileRequest(raw: unknown): CloudAccountProfileRequest;
|
|
3
|
+
/** Project only public identity fields, never server authority or unknown data. */
|
|
4
|
+
export declare function accountProfile(raw: unknown, accountId: string): CloudAccountIdentity;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isRelayOrigin } from "../remote-relay/protocol.js";
|
|
2
|
+
import { normalizeCloudDisplayName, normalizeCloudUsername, } from "@cjhyy/code-shell-web/account-profile";
|
|
3
|
+
const uuid = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
|
|
4
|
+
export function accountProfileRequest(raw) {
|
|
5
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
6
|
+
throw new Error("云账号资料请求无效。");
|
|
7
|
+
const input = raw;
|
|
8
|
+
if (!isRelayOrigin(input.origin) ||
|
|
9
|
+
input.origin.length > 2048 ||
|
|
10
|
+
typeof input.accountId !== "string" ||
|
|
11
|
+
!uuid.test(input.accountId))
|
|
12
|
+
throw new Error("云账号资料请求无效。");
|
|
13
|
+
const identity = { origin: input.origin, accountId: input.accountId };
|
|
14
|
+
if (input.action === "read" && Object.keys(input).length === 3)
|
|
15
|
+
return { ...identity, action: "read" };
|
|
16
|
+
if (input.action !== "update" ||
|
|
17
|
+
Object.keys(input).some((key) => !["origin", "accountId", "action", "displayName", "username"].includes(key)) ||
|
|
18
|
+
(input.displayName === undefined && input.username === undefined))
|
|
19
|
+
throw new Error("云账号资料请求无效。");
|
|
20
|
+
const displayName = normalizeCloudDisplayName(input.displayName);
|
|
21
|
+
const username = normalizeCloudUsername(input.username);
|
|
22
|
+
if (input.displayName !== undefined && displayName === undefined)
|
|
23
|
+
throw new Error("昵称需为 1–64 个字符,不能包含控制字符。");
|
|
24
|
+
if (input.username !== undefined && username === undefined)
|
|
25
|
+
throw new Error("用户名需为 3–64 位字母、数字、点、下划线或连字符,并以字母或数字开头。");
|
|
26
|
+
return {
|
|
27
|
+
...identity,
|
|
28
|
+
action: "update",
|
|
29
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
30
|
+
...(username === undefined ? {} : { username }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Project only public identity fields, never server authority or unknown data. */
|
|
34
|
+
export function accountProfile(raw, accountId) {
|
|
35
|
+
const account = raw?.account;
|
|
36
|
+
if (!account || typeof account !== "object" || Array.isArray(account))
|
|
37
|
+
throw new Error("云服务返回了无效的账号资料。");
|
|
38
|
+
const value = account;
|
|
39
|
+
if (value.id !== accountId ||
|
|
40
|
+
normalizeCloudUsername(value.username) !== value.username ||
|
|
41
|
+
typeof value.username !== "string" ||
|
|
42
|
+
typeof value.displayName !== "string" ||
|
|
43
|
+
normalizeCloudDisplayName(value.displayName) !== value.displayName)
|
|
44
|
+
throw new Error("云服务返回了无效的账号资料。");
|
|
45
|
+
return { id: accountId, username: value.username, displayName: value.displayName };
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CloudAccountIdentity } from "./types.js";
|
|
2
|
+
export interface RelaySecretCipher {
|
|
3
|
+
available(): boolean;
|
|
4
|
+
encrypt(value: string): Buffer;
|
|
5
|
+
decrypt(value: Buffer): string;
|
|
6
|
+
}
|
|
7
|
+
/** Main-process-only grants. Never return this type from an IPC handler. */
|
|
8
|
+
export interface CloudAccountGrant {
|
|
9
|
+
accessToken: string;
|
|
10
|
+
refreshToken: string;
|
|
11
|
+
accessTokenExpiresAt: number;
|
|
12
|
+
refreshTokenExpiresAt: number;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
account: CloudAccountIdentity;
|
|
15
|
+
kind?: "account" | "device";
|
|
16
|
+
audience?: string;
|
|
17
|
+
hostId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SavedCloudAccount extends CloudAccountGrant {
|
|
20
|
+
origin: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function validCloudAccountGrant(value: unknown): value is CloudAccountGrant;
|
|
23
|
+
/** Independent from agent credentials; no plaintext fallback or generic enumeration. */
|
|
24
|
+
export declare class CloudAccountStore {
|
|
25
|
+
private readonly path;
|
|
26
|
+
private readonly cipher;
|
|
27
|
+
constructor(path: string, cipher: RelaySecretCipher);
|
|
28
|
+
private assertAvailable;
|
|
29
|
+
private temporary;
|
|
30
|
+
preflight(): void;
|
|
31
|
+
load(): SavedCloudAccount | undefined;
|
|
32
|
+
save(saved: SavedCloudAccount): void;
|
|
33
|
+
forget(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { constants, closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { isRelayHostId, isRelayOrigin, isRelayToken } from "../remote-relay/protocol.js";
|
|
5
|
+
import { normalizeCloudDisplayName } from "@cjhyy/code-shell-web/account-profile";
|
|
6
|
+
export function validCloudAccountGrant(value) {
|
|
7
|
+
if (!value || typeof value !== "object")
|
|
8
|
+
return false;
|
|
9
|
+
const grant = value;
|
|
10
|
+
return (isRelayToken(grant.accessToken) &&
|
|
11
|
+
isRelayToken(grant.refreshToken) &&
|
|
12
|
+
isRelayHostId(grant.sessionId) &&
|
|
13
|
+
Number.isSafeInteger(grant.accessTokenExpiresAt) &&
|
|
14
|
+
grant.accessTokenExpiresAt > 0 &&
|
|
15
|
+
Number.isSafeInteger(grant.refreshTokenExpiresAt) &&
|
|
16
|
+
grant.refreshTokenExpiresAt >= grant.accessTokenExpiresAt &&
|
|
17
|
+
!!grant.account &&
|
|
18
|
+
isRelayHostId(grant.account.id) &&
|
|
19
|
+
typeof grant.account.username === "string" &&
|
|
20
|
+
grant.account.username.length > 0 &&
|
|
21
|
+
grant.account.username.length <= 128 &&
|
|
22
|
+
(grant.account.displayName === undefined ||
|
|
23
|
+
(typeof grant.account.displayName === "string" &&
|
|
24
|
+
normalizeCloudDisplayName(grant.account.displayName) === grant.account.displayName)) &&
|
|
25
|
+
(grant.kind === undefined || grant.kind === "account" || grant.kind === "device") &&
|
|
26
|
+
(grant.audience === undefined || isRelayOrigin(grant.audience)) &&
|
|
27
|
+
(grant.hostId === undefined || isRelayHostId(grant.hostId)));
|
|
28
|
+
}
|
|
29
|
+
function validSaved(value) {
|
|
30
|
+
return (validCloudAccountGrant(value) &&
|
|
31
|
+
value.kind !== "device" &&
|
|
32
|
+
isRelayOrigin(value.origin) &&
|
|
33
|
+
(value.audience === undefined || value.audience === value.origin));
|
|
34
|
+
}
|
|
35
|
+
/** Independent from agent credentials; no plaintext fallback or generic enumeration. */
|
|
36
|
+
export class CloudAccountStore {
|
|
37
|
+
path;
|
|
38
|
+
cipher;
|
|
39
|
+
constructor(path, cipher) {
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.cipher = cipher;
|
|
42
|
+
}
|
|
43
|
+
assertAvailable() {
|
|
44
|
+
if (!this.cipher.available())
|
|
45
|
+
throw new Error("系统安全存储不可用;本地使用不受影响。");
|
|
46
|
+
}
|
|
47
|
+
temporary(bytes) {
|
|
48
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
49
|
+
const temporary = `${this.path}.${randomUUID()}.tmp`;
|
|
50
|
+
const fd = openSync(temporary, "wx", 0o600);
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(fd, bytes);
|
|
53
|
+
fsyncSync(fd);
|
|
54
|
+
return temporary;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
unlinkSync(temporary);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
closeSync(fd);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
preflight() {
|
|
65
|
+
this.assertAvailable();
|
|
66
|
+
const probe = randomUUID();
|
|
67
|
+
const bytes = this.cipher.encrypt(probe);
|
|
68
|
+
if (this.cipher.decrypt(bytes) !== probe)
|
|
69
|
+
throw new Error("系统安全存储校验失败。");
|
|
70
|
+
unlinkSync(this.temporary(bytes));
|
|
71
|
+
}
|
|
72
|
+
load() {
|
|
73
|
+
let metadata;
|
|
74
|
+
try {
|
|
75
|
+
metadata = lstatSync(this.path);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === "ENOENT")
|
|
79
|
+
return;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
this.assertAvailable();
|
|
83
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 16_384)
|
|
84
|
+
throw new Error("云账号登记损坏,请退出后重新登录。");
|
|
85
|
+
const fd = openSync(this.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
86
|
+
try {
|
|
87
|
+
const saved = JSON.parse(this.cipher.decrypt(readFileSync(fd)));
|
|
88
|
+
if (!validSaved(saved))
|
|
89
|
+
throw new Error("云账号登记损坏,请退出后重新登录。");
|
|
90
|
+
return saved;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
closeSync(fd);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
save(saved) {
|
|
97
|
+
this.assertAvailable();
|
|
98
|
+
if (!validSaved(saved))
|
|
99
|
+
throw new Error("无效的云账号授权。");
|
|
100
|
+
const temporary = this.temporary(this.cipher.encrypt(JSON.stringify(saved)));
|
|
101
|
+
try {
|
|
102
|
+
renameSync(temporary, this.path);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
unlinkSync(temporary);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
forget() {
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(this.path);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code !== "ENOENT")
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { AccountFeatureMap } from "@cjhyy/code-shell-web/account";
|
|
2
|
+
/** Public account state. Tokens and OAuth receipts never cross the preload bridge. */
|
|
3
|
+
export declare const DEFAULT_CLOUD_ACCOUNT_ORIGIN = "https://115.159.45.55";
|
|
4
|
+
export interface CloudAccountIdentity {
|
|
5
|
+
id: string;
|
|
6
|
+
username: string;
|
|
7
|
+
/** Optional for grants saved before profile support. */
|
|
8
|
+
displayName?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CloudAccountStatus {
|
|
11
|
+
state: "signed-out" | "signed-in" | "signing-in" | "storage-error";
|
|
12
|
+
origin?: string;
|
|
13
|
+
account?: CloudAccountIdentity;
|
|
14
|
+
/** Data connection progress; independent from the account session and never persisted here. */
|
|
15
|
+
githubConnection?: {
|
|
16
|
+
state: "pending" | "connected" | "failed" | "skipped";
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Local credentials are erased even when the service cannot acknowledge revocation. */
|
|
20
|
+
export interface CloudAccountLogoutResult {
|
|
21
|
+
state: "signed-out";
|
|
22
|
+
revocation: "confirmed" | "unconfirmed";
|
|
23
|
+
}
|
|
24
|
+
export interface CloudAccountLogin {
|
|
25
|
+
origin: string;
|
|
26
|
+
username: string;
|
|
27
|
+
password: string;
|
|
28
|
+
deviceName?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CloudAccountBrowserLogin {
|
|
31
|
+
origin: string;
|
|
32
|
+
deviceName?: string;
|
|
33
|
+
}
|
|
34
|
+
export type CloudAccountManagementSection = "profile" | "security" | "sessions" | "connections";
|
|
35
|
+
export interface CloudAccountCapabilities {
|
|
36
|
+
features: AccountFeatureMap;
|
|
37
|
+
registrationAllowed: boolean;
|
|
38
|
+
githubEnabled: boolean;
|
|
39
|
+
browserLoginEnabled?: boolean;
|
|
40
|
+
githubLinkEnabled?: boolean;
|
|
41
|
+
linkManagementEnabled?: boolean;
|
|
42
|
+
profileEnabled?: boolean;
|
|
43
|
+
relay?: {
|
|
44
|
+
enabled: false;
|
|
45
|
+
} | {
|
|
46
|
+
enabled: true;
|
|
47
|
+
directoryOrigin: string;
|
|
48
|
+
protocolVersion: 1;
|
|
49
|
+
accountHandoffVersion: 1;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export type CloudAccountProfileRequest = {
|
|
53
|
+
origin: string;
|
|
54
|
+
accountId: string;
|
|
55
|
+
} & ({
|
|
56
|
+
action: "read";
|
|
57
|
+
} | {
|
|
58
|
+
action: "update";
|
|
59
|
+
displayName?: string;
|
|
60
|
+
username?: string;
|
|
61
|
+
});
|
|
62
|
+
/** Account-owned metadata only. Provider credentials and global clients are never exposed. */
|
|
63
|
+
export interface CloudAccountLinkView {
|
|
64
|
+
linkOrigin: string;
|
|
65
|
+
connections: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
provider: string;
|
|
68
|
+
account: {
|
|
69
|
+
id: string;
|
|
70
|
+
login?: string;
|
|
71
|
+
label?: string;
|
|
72
|
+
};
|
|
73
|
+
revoked: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
grants: Array<{
|
|
76
|
+
id: string;
|
|
77
|
+
connectionId: string;
|
|
78
|
+
client: {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
};
|
|
82
|
+
scopes: string[];
|
|
83
|
+
resources: string[];
|
|
84
|
+
revoked: boolean;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
export type CloudAccountLinksRequest = {
|
|
88
|
+
origin: string;
|
|
89
|
+
accountId: string;
|
|
90
|
+
} & ({
|
|
91
|
+
action: "list";
|
|
92
|
+
} | {
|
|
93
|
+
action: "revoke";
|
|
94
|
+
kind: "connections" | "grants";
|
|
95
|
+
id: string;
|
|
96
|
+
});
|
|
97
|
+
export type CloudAccountLinksResult = {
|
|
98
|
+
action: "list";
|
|
99
|
+
view: CloudAccountLinkView;
|
|
100
|
+
} | {
|
|
101
|
+
action: "revoke";
|
|
102
|
+
revoked: true;
|
|
103
|
+
kind: "connections" | "grants";
|
|
104
|
+
id: string;
|
|
105
|
+
};
|
|
106
|
+
export type CloudAccountGitHubRequest = {
|
|
107
|
+
mode: "login";
|
|
108
|
+
input: CloudAccountGitHubLogin;
|
|
109
|
+
} | {
|
|
110
|
+
mode: "link";
|
|
111
|
+
};
|
|
112
|
+
export interface CloudAccountGitHubLogin {
|
|
113
|
+
origin: string;
|
|
114
|
+
deviceName?: string;
|
|
115
|
+
connectGitHub?: boolean;
|
|
116
|
+
}
|
|
117
|
+
export interface CloudAccountApi {
|
|
118
|
+
openRegistration(origin: string): Promise<void>;
|
|
119
|
+
openManagement(section: CloudAccountManagementSection): Promise<void>;
|
|
120
|
+
signInWithBrowser(input: CloudAccountBrowserLogin): Promise<CloudAccountStatus>;
|
|
121
|
+
status(): Promise<CloudAccountStatus>;
|
|
122
|
+
resume?(): Promise<CloudAccountStatus>;
|
|
123
|
+
capabilities(origin: string): Promise<CloudAccountCapabilities>;
|
|
124
|
+
login(input: CloudAccountLogin): Promise<CloudAccountStatus>;
|
|
125
|
+
register(input: CloudAccountLogin): Promise<CloudAccountStatus>;
|
|
126
|
+
signInWithGitHub(input: CloudAccountGitHubLogin): Promise<CloudAccountStatus>;
|
|
127
|
+
linkGitHub(): Promise<CloudAccountStatus>;
|
|
128
|
+
manageLinks(input: CloudAccountLinksRequest): Promise<CloudAccountLinksResult>;
|
|
129
|
+
profile(input: CloudAccountProfileRequest): Promise<CloudAccountStatus>;
|
|
130
|
+
cancelSignIn(): Promise<void>;
|
|
131
|
+
logout(): Promise<CloudAccountLogoutResult>;
|
|
132
|
+
onStatus(callback: (status: CloudAccountStatus) => void): () => void;
|
|
133
|
+
}
|
|
134
|
+
/** Bounded public projection; no grants, service secrets, or server-only fields. */
|
|
135
|
+
export interface CloudAccountOverview {
|
|
136
|
+
account: CloudAccountIdentity;
|
|
137
|
+
features: AccountFeatureMap;
|
|
138
|
+
}
|
|
@@ -28,6 +28,8 @@ export interface DesktopWebApiOptions {
|
|
|
28
28
|
/** Release owner-scoped resources contributed by handleExtra (for example Link probes). */
|
|
29
29
|
onSessionRevoked?: (sessionId: string) => void;
|
|
30
30
|
onClose?: () => void | Promise<void>;
|
|
31
|
+
/** The account computer list, while this computer is registered to an account. */
|
|
32
|
+
accountComputers?: () => string | undefined;
|
|
31
33
|
/** Authenticated same-origin composition point, for example the shared Link API. */
|
|
32
34
|
handleExtra?: (request: IncomingMessage, response: ServerResponse, context: DesktopWebRequestContext) => boolean | Promise<boolean>;
|
|
33
35
|
now?: () => number;
|
|
@@ -45,6 +47,8 @@ export interface DesktopWebHttpApi {
|
|
|
45
47
|
baseUrl: string;
|
|
46
48
|
/** Recheck the entrance authorization captured after its access gate succeeded. */
|
|
47
49
|
isAccessAuthorized?: () => boolean;
|
|
50
|
+
/** Account handoffs may authenticate only their own short-lived device. */
|
|
51
|
+
accessDeviceId?: string;
|
|
48
52
|
}) => Promise<boolean>;
|
|
49
53
|
/** Reopen after RemoteHostManager.stop(), without reviving previous cookies. */
|
|
50
54
|
start: () => void;
|
|
@@ -318,7 +318,9 @@ export function createDesktopWebApi(options) {
|
|
|
318
318
|
if (context.isAccessAuthorized?.() === false)
|
|
319
319
|
throw new HubConfigurationError(401, "访问口令已更新,请重新连接桌面。");
|
|
320
320
|
const device = options.devices.authenticate(input.deviceId, input.secretHash);
|
|
321
|
-
if (!device
|
|
321
|
+
if (!device ||
|
|
322
|
+
(context.accessDeviceId && context.accessDeviceId !== device.id) ||
|
|
323
|
+
(options.devices.isEphemeralDevice(device.id) && context.accessDeviceId !== device.id))
|
|
322
324
|
throw new HubConfigurationError(401, "设备未配对或已被撤销,请在桌面重新配对。");
|
|
323
325
|
const existing = principal(token);
|
|
324
326
|
if (token && existing?.session.deviceId === device.id) {
|
|
@@ -347,14 +349,20 @@ export function createDesktopWebApi(options) {
|
|
|
347
349
|
json(response, 200, status(session, device));
|
|
348
350
|
return true;
|
|
349
351
|
}
|
|
350
|
-
const
|
|
352
|
+
const candidate = principal(token);
|
|
353
|
+
const current = (context.accessDeviceId && candidate?.device.id !== context.accessDeviceId) ||
|
|
354
|
+
(candidate &&
|
|
355
|
+
options.devices.isEphemeralDevice(candidate.device.id) &&
|
|
356
|
+
context.accessDeviceId !== candidate.device.id)
|
|
357
|
+
? undefined
|
|
358
|
+
: candidate;
|
|
351
359
|
if (request.method === "GET" &&
|
|
352
360
|
["/api/v1/auth/status", "/api/v1/auth"].includes(url.pathname)) {
|
|
353
361
|
json(response, 200, status(current?.session, current?.device));
|
|
354
362
|
return true;
|
|
355
363
|
}
|
|
356
364
|
if (request.method === "POST" && url.pathname === "/api/v1/auth/logout") {
|
|
357
|
-
if (token)
|
|
365
|
+
if (token && current)
|
|
358
366
|
dropSession(token);
|
|
359
367
|
setCookie(response, "", secure);
|
|
360
368
|
json(response, 200, { ok: true });
|
|
@@ -370,7 +378,7 @@ export function createDesktopWebApi(options) {
|
|
|
370
378
|
context.isAccessAuthorized?.() === false ||
|
|
371
379
|
principal(token)?.session !== current.session)
|
|
372
380
|
throw new HubConfigurationError(401, "设备连接已失效,请重新连接桌面。");
|
|
373
|
-
json(response, 200, describeEnvironment(id, "desktop"));
|
|
381
|
+
json(response, 200, describeEnvironment(id, "desktop", options.accountComputers?.()));
|
|
374
382
|
return true;
|
|
375
383
|
}
|
|
376
384
|
const input = workspaceInput(request, url);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { EnvironmentDescriptor } from "@cjhyy/code-shell-web";
|
|
2
2
|
/** Publish once, atomically. Never silently replace a corrupt identity or adopt a symlink. */
|
|
3
3
|
export declare function environmentIdentity(dataDir: string): Promise<string>;
|
|
4
|
-
export declare function describeEnvironment(id: string, kind: EnvironmentDescriptor["kind"]): EnvironmentDescriptor;
|
|
4
|
+
export declare function describeEnvironment(id: string, kind: EnvironmentDescriptor["kind"], accountComputers?: string): EnvironmentDescriptor;
|
|
@@ -56,12 +56,13 @@ export async function environmentIdentity(dataDir) {
|
|
|
56
56
|
await file.close();
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
export function describeEnvironment(id, kind) {
|
|
59
|
+
export function describeEnvironment(id, kind, accountComputers) {
|
|
60
60
|
return {
|
|
61
61
|
version: 1,
|
|
62
62
|
id,
|
|
63
63
|
name: kind === "desktop" ? "我的电脑" : kind === "project-host" ? "云端项目" : "Hub 工作台",
|
|
64
64
|
kind,
|
|
65
65
|
entryPath: kind === "desktop" ? "/mobile" : "/",
|
|
66
|
+
...(accountComputers ? { accountComputers } : {}),
|
|
66
67
|
};
|
|
67
68
|
}
|