@cjhyy/code-shell-server 0.9.7 → 0.9.9

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.
Files changed (109) hide show
  1. package/README.md +85 -23
  2. package/dist/bin/code-shell-project-runtime.d.ts +2 -0
  3. package/dist/bin/code-shell-project-runtime.js +8 -0
  4. package/dist/desktop-web/http-api.d.ts +43 -0
  5. package/dist/desktop-web/http-api.js +376 -0
  6. package/dist/hub/approval-lease.d.ts +30 -0
  7. package/dist/hub/approval-lease.js +89 -0
  8. package/dist/hub/auth-http.d.ts +23 -0
  9. package/dist/hub/auth-http.js +259 -0
  10. package/dist/hub/auth-store.d.ts +52 -0
  11. package/dist/hub/auth-store.js +316 -0
  12. package/dist/hub/configuration.d.ts +64 -0
  13. package/dist/hub/configuration.js +555 -0
  14. package/dist/hub/files.d.ts +9 -0
  15. package/dist/hub/files.js +278 -0
  16. package/dist/hub/mcp-configuration.d.ts +51 -0
  17. package/dist/hub/mcp-configuration.js +558 -0
  18. package/dist/hub/mcp-probe.d.ts +24 -0
  19. package/dist/hub/mcp-probe.js +392 -0
  20. package/dist/hub/model-probe.d.ts +20 -0
  21. package/dist/hub/model-probe.js +193 -0
  22. package/dist/hub/outbound-transport.d.ts +36 -0
  23. package/dist/hub/outbound-transport.js +122 -0
  24. package/dist/hub/run-replay.d.ts +42 -0
  25. package/dist/hub/run-replay.js +89 -0
  26. package/dist/hub/session-management.d.ts +36 -0
  27. package/dist/hub/session-management.js +405 -0
  28. package/dist/hub/skills-management.d.ts +43 -0
  29. package/dist/hub/skills-management.js +487 -0
  30. package/dist/hub/uploads.d.ts +22 -0
  31. package/dist/hub/uploads.js +241 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.desktop-web.d.ts +2 -0
  34. package/dist/index.desktop-web.js +2 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.links.d.ts +6 -0
  37. package/dist/index.links.js +6 -0
  38. package/dist/index.panels.d.ts +9 -0
  39. package/dist/index.panels.js +8 -0
  40. package/dist/index.serve.d.ts +1 -0
  41. package/dist/index.serve.js +1 -0
  42. package/dist/index.storage.d.ts +2 -0
  43. package/dist/index.storage.js +2 -0
  44. package/dist/links/catalog.d.ts +10 -0
  45. package/dist/links/catalog.js +41 -0
  46. package/dist/links/device-oauth.d.ts +52 -0
  47. package/dist/links/device-oauth.js +255 -0
  48. package/dist/links/http.d.ts +34 -0
  49. package/dist/links/http.js +144 -0
  50. package/dist/links/service.d.ts +54 -0
  51. package/dist/links/service.js +578 -0
  52. package/dist/links/types.d.ts +6 -0
  53. package/dist/links/types.js +1 -0
  54. package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
  55. package/dist/mobile-remote/pending-approvals.js +2 -0
  56. package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
  57. package/dist/mobile-remote/remote-host-manager.js +90 -15
  58. package/dist/mobile-remote/room-manager.js +17 -5
  59. package/dist/panels/agent-task-host.d.ts +32 -0
  60. package/dist/panels/agent-task-host.js +457 -0
  61. package/dist/panels/agent-task-models.d.ts +27 -0
  62. package/dist/panels/agent-task-models.js +51 -0
  63. package/dist/panels/agent-task-service.d.ts +105 -0
  64. package/dist/panels/agent-task-service.js +283 -0
  65. package/dist/panels/http.d.ts +34 -0
  66. package/dist/panels/http.js +37 -0
  67. package/dist/panels/hub-binding.d.ts +12 -0
  68. package/dist/panels/hub-binding.js +174 -0
  69. package/dist/panels/management-http.d.ts +29 -0
  70. package/dist/panels/management-http.js +119 -0
  71. package/dist/panels/management.d.ts +39 -0
  72. package/dist/panels/management.js +494 -0
  73. package/dist/panels/process-files.d.ts +14 -0
  74. package/dist/panels/process-files.js +299 -0
  75. package/dist/panels/process-service.d.ts +96 -0
  76. package/dist/panels/process-service.js +501 -0
  77. package/dist/panels/runtime-services.d.ts +24 -0
  78. package/dist/panels/runtime-services.js +462 -0
  79. package/dist/panels/runtime.d.ts +51 -0
  80. package/dist/panels/runtime.js +1063 -0
  81. package/dist/panels/storage-store.d.ts +9 -0
  82. package/dist/panels/storage-store.js +154 -0
  83. package/dist/panels/types.d.ts +52 -0
  84. package/dist/panels/types.js +1 -0
  85. package/dist/project-runtime/control-server.d.ts +19 -0
  86. package/dist/project-runtime/control-server.js +300 -0
  87. package/dist/project-runtime/docker-provider.d.ts +33 -0
  88. package/dist/project-runtime/docker-provider.js +487 -0
  89. package/dist/project-runtime/lock.d.ts +6 -0
  90. package/dist/project-runtime/lock.js +43 -0
  91. package/dist/project-runtime/managed-entry.d.ts +10 -0
  92. package/dist/project-runtime/managed-entry.js +113 -0
  93. package/dist/project-runtime/manager.d.ts +40 -0
  94. package/dist/project-runtime/manager.js +249 -0
  95. package/dist/project-runtime/proxy.d.ts +22 -0
  96. package/dist/project-runtime/proxy.js +706 -0
  97. package/dist/project-runtime/registry.d.ts +54 -0
  98. package/dist/project-runtime/registry.js +286 -0
  99. package/dist/project-runtime/types.d.ts +43 -0
  100. package/dist/project-runtime/types.js +1 -0
  101. package/dist/serve/cli.d.ts +5 -0
  102. package/dist/serve/cli.js +82 -23
  103. package/dist/serve/headless-server.d.ts +11 -0
  104. package/dist/serve/headless-server.js +737 -83
  105. package/dist/session-titles-store.d.ts +17 -0
  106. package/dist/session-titles-store.js +174 -0
  107. package/dist/static-files.js +1 -0
  108. package/dist/worker-bridge-core.js +17 -6
  109. package/package.json +19 -5
@@ -0,0 +1,316 @@
1
+ import { createHash, randomBytes, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
2
+ import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { acquireFileLock } from "@cjhyy/code-shell-core/internal";
5
+ export const HUB_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1_000;
6
+ const MAX_SESSIONS = 64;
7
+ const MAX_STORE_BYTES = 128 * 1024;
8
+ const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
9
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
10
+ const SCRYPT_OPTIONS = { N: 32_768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
11
+ export class HubAuthError extends Error {
12
+ statusCode;
13
+ constructor(statusCode, message) {
14
+ super(message);
15
+ this.statusCode = statusCode;
16
+ }
17
+ }
18
+ export class HubAuthStore {
19
+ filePath;
20
+ sessionTtlMs;
21
+ now;
22
+ maxSessions;
23
+ constructor(options) {
24
+ this.filePath = join(options.dataDir, "hub", "auth.json");
25
+ this.now = options.now ?? Date.now;
26
+ this.sessionTtlMs = options.sessionTtlMs ?? HUB_SESSION_TTL_MS;
27
+ this.maxSessions = options.maxSessions ?? MAX_SESSIONS;
28
+ if (!Number.isSafeInteger(this.sessionTtlMs) || this.sessionTtlMs < 1_000) {
29
+ throw new Error("Hub session lifetime must be at least 1,000 milliseconds");
30
+ }
31
+ if (!Number.isSafeInteger(this.maxSessions) ||
32
+ this.maxSessions < 1 ||
33
+ this.maxSessions > MAX_SESSIONS) {
34
+ throw new Error(`Hub session limit must be between 1 and ${MAX_SESSIONS}`);
35
+ }
36
+ }
37
+ /** Only the process that creates the store receives the one-time secret. */
38
+ initialize() {
39
+ this.ensureDirectory();
40
+ const release = acquireFileLock(this.filePath);
41
+ try {
42
+ const current = this.read(true);
43
+ if (current) {
44
+ chmodSync(this.filePath, 0o600);
45
+ return undefined;
46
+ }
47
+ const token = randomToken();
48
+ this.write({ version: 1, bootstrapTokenHash: hashToken(token), account: null, sessions: [] });
49
+ return token;
50
+ }
51
+ finally {
52
+ release();
53
+ }
54
+ }
55
+ isInitialized() {
56
+ return this.read().account !== null;
57
+ }
58
+ async setup(input) {
59
+ const normalized = validateLoginInput(input);
60
+ this.checkBootstrap(this.read(), input.token);
61
+ const salt = randomBytes(16).toString("hex");
62
+ // Never await with the synchronous cross-process lock held.
63
+ const passwordHash = await hashPassword(input.password, salt);
64
+ return this.mutate((record) => {
65
+ this.checkBootstrap(record, input.token);
66
+ record.account = {
67
+ username: normalized.username,
68
+ passwordHash,
69
+ passwordSalt: salt,
70
+ createdAt: this.now(),
71
+ };
72
+ record.bootstrapTokenHash = null;
73
+ return { result: this.grant(record, normalized.deviceName), changed: true };
74
+ });
75
+ }
76
+ async login(input, replaceToken) {
77
+ const normalized = validateLoginInput(input);
78
+ const account = this.read().account;
79
+ if (!account)
80
+ throw new HubAuthError(401, "Invalid username or password");
81
+ const candidate = await hashPassword(input.password, account.passwordSalt);
82
+ if (!safeHashEqual(candidate, account.passwordHash) ||
83
+ normalized.username !== account.username) {
84
+ throw new HubAuthError(401, "Invalid username or password");
85
+ }
86
+ return this.mutate((record) => {
87
+ if (!record.account || !safeHashEqual(record.account.passwordHash, account.passwordHash)) {
88
+ throw new HubAuthError(401, "Invalid username or password");
89
+ }
90
+ return { result: this.grant(record, normalized.deviceName, replaceToken), changed: true };
91
+ });
92
+ }
93
+ authenticate(token) {
94
+ if (!token || !TOKEN_PATTERN.test(token))
95
+ return null;
96
+ return this.mutate((record) => {
97
+ const now = this.now();
98
+ const tokenHash = hashToken(token);
99
+ const session = record.sessions.find((row) => safeHashEqual(row.tokenHash, tokenHash));
100
+ if (!record.account || !session || session.expiresAt <= now || session.createdAt > now) {
101
+ return { result: null, changed: false };
102
+ }
103
+ // Persist activity at most once per minute; validation always reads disk.
104
+ const changed = now - session.lastSeenAt >= 60_000;
105
+ if (changed)
106
+ session.lastSeenAt = now;
107
+ return { result: this.toPublic(record, session), changed };
108
+ });
109
+ }
110
+ listSessions() {
111
+ const record = this.read();
112
+ const now = this.now();
113
+ return record.sessions
114
+ .filter((session) => session.expiresAt > now && session.createdAt <= now)
115
+ .map((session) => this.toPublic(record, session))
116
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt);
117
+ }
118
+ revoke(id) {
119
+ return this.mutate((record) => {
120
+ const index = record.sessions.findIndex((session) => session.id === id);
121
+ if (index < 0)
122
+ return { result: false, changed: false };
123
+ record.sessions.splice(index, 1);
124
+ return { result: true, changed: true };
125
+ });
126
+ }
127
+ checkBootstrap(record, token) {
128
+ if (record.account)
129
+ throw new HubAuthError(409, "Hub has already been initialized");
130
+ if (typeof token !== "string" ||
131
+ !TOKEN_PATTERN.test(token) ||
132
+ !safeHashEqual(hashToken(token), record.bootstrapTokenHash)) {
133
+ throw new HubAuthError(401, "Invalid initialization token");
134
+ }
135
+ }
136
+ grant(record, deviceName, replaceToken) {
137
+ const now = this.now();
138
+ const replacedHash = replaceToken && TOKEN_PATTERN.test(replaceToken) ? hashToken(replaceToken) : null;
139
+ const revokedSessionIds = [];
140
+ record.sessions = record.sessions.filter((row) => {
141
+ const remove = row.expiresAt <= now ||
142
+ (replacedHash !== null && safeHashEqual(row.tokenHash, replacedHash));
143
+ if (remove)
144
+ revokedSessionIds.push(row.id);
145
+ return !remove;
146
+ });
147
+ if (record.sessions.length >= this.maxSessions) {
148
+ record.sessions.sort((a, b) => a.lastSeenAt - b.lastSeenAt);
149
+ revokedSessionIds.push(record.sessions.shift().id);
150
+ }
151
+ const token = randomToken();
152
+ const session = {
153
+ id: randomUUID(),
154
+ deviceName,
155
+ tokenHash: hashToken(token),
156
+ createdAt: now,
157
+ lastSeenAt: now,
158
+ expiresAt: now + this.sessionTtlMs,
159
+ };
160
+ record.sessions.push(session);
161
+ return { token, session: this.toPublic(record, session), revokedSessionIds };
162
+ }
163
+ toPublic(record, session) {
164
+ return {
165
+ id: session.id,
166
+ username: record.account.username,
167
+ deviceName: session.deviceName,
168
+ createdAt: session.createdAt,
169
+ lastSeenAt: session.lastSeenAt,
170
+ expiresAt: session.expiresAt,
171
+ };
172
+ }
173
+ mutate(mutation) {
174
+ this.ensureDirectory();
175
+ const release = acquireFileLock(this.filePath);
176
+ try {
177
+ const record = this.read();
178
+ const { result, changed } = mutation(record);
179
+ if (changed)
180
+ this.write(record);
181
+ return result;
182
+ }
183
+ finally {
184
+ release();
185
+ }
186
+ }
187
+ ensureDirectory() {
188
+ const parent = dirname(this.filePath);
189
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
190
+ const metadata = lstatSync(parent);
191
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
192
+ throw new Error("Invalid Hub auth directory");
193
+ chmodSync(parent, 0o700);
194
+ }
195
+ read(allowMissing = false) {
196
+ let descriptor;
197
+ try {
198
+ const metadata = lstatSync(this.filePath);
199
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_STORE_BYTES) {
200
+ throw new Error("Invalid Hub auth file");
201
+ }
202
+ descriptor = openSync(this.filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
203
+ const opened = fstatSync(descriptor);
204
+ if (!opened.isFile() || opened.size > MAX_STORE_BYTES)
205
+ throw new Error("Invalid Hub auth file");
206
+ const record = JSON.parse(readFileSync(descriptor, "utf8"));
207
+ if (!isAuthRecord(record))
208
+ throw new Error("Invalid Hub auth record");
209
+ return record;
210
+ }
211
+ catch (error) {
212
+ if (allowMissing && error.code === "ENOENT")
213
+ return undefined;
214
+ // A malformed or missing established store must never reopen bootstrap.
215
+ throw new Error("Hub authentication store is unavailable or corrupt", { cause: error });
216
+ }
217
+ finally {
218
+ if (descriptor !== undefined)
219
+ closeSync(descriptor);
220
+ }
221
+ }
222
+ write(record) {
223
+ const serialized = JSON.stringify(record, null, 2);
224
+ if (Buffer.byteLength(serialized) > MAX_STORE_BYTES)
225
+ throw new Error("Hub auth store limit exceeded");
226
+ const temp = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
227
+ try {
228
+ writeFileSync(temp, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
229
+ renameSync(temp, this.filePath);
230
+ }
231
+ finally {
232
+ rmSync(temp, { force: true });
233
+ }
234
+ }
235
+ }
236
+ function validateLoginInput(input) {
237
+ if (!isLabel(input.username, 64) ||
238
+ typeof input.password !== "string" ||
239
+ input.password.length < 12 ||
240
+ input.password.length > 256) {
241
+ throw new HubAuthError(400, "Use a username of 1–64 characters and a password of 12–256 characters");
242
+ }
243
+ if (input.deviceName !== undefined && !isLabel(input.deviceName, 100)) {
244
+ throw new HubAuthError(400, "Device name must contain 1–100 characters");
245
+ }
246
+ return { username: input.username.trim(), deviceName: input.deviceName?.trim() ?? "Browser" };
247
+ }
248
+ function randomToken() {
249
+ return randomBytes(32).toString("base64url");
250
+ }
251
+ function hashToken(token) {
252
+ return createHash("sha256").update(token).digest("hex");
253
+ }
254
+ function safeHashEqual(a, b) {
255
+ return (HASH_PATTERN.test(a) &&
256
+ HASH_PATTERN.test(b) &&
257
+ timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex")));
258
+ }
259
+ function hashPassword(password, salt) {
260
+ return new Promise((resolve, reject) => {
261
+ scrypt(password, salt, 32, SCRYPT_OPTIONS, (error, key) => {
262
+ if (error)
263
+ reject(error);
264
+ else
265
+ resolve(key.toString("hex"));
266
+ });
267
+ });
268
+ }
269
+ function isLabel(value, max) {
270
+ return (typeof value === "string" &&
271
+ value.trim().length > 0 &&
272
+ value.length <= max &&
273
+ !/[\u0000-\u001f\u007f]/.test(value));
274
+ }
275
+ function isTimestamp(value) {
276
+ return Number.isSafeInteger(value) && value >= 0;
277
+ }
278
+ function isAuthRecord(value) {
279
+ if (!value ||
280
+ value.version !== 1 ||
281
+ !Array.isArray(value.sessions) ||
282
+ value.sessions.length > MAX_SESSIONS)
283
+ return false;
284
+ if (value.account === null) {
285
+ return (typeof value.bootstrapTokenHash === "string" &&
286
+ HASH_PATTERN.test(value.bootstrapTokenHash) &&
287
+ value.sessions.length === 0);
288
+ }
289
+ const account = value.account;
290
+ if (!account ||
291
+ value.bootstrapTokenHash !== null ||
292
+ !isLabel(account.username, 64) ||
293
+ !HASH_PATTERN.test(account.passwordHash) ||
294
+ !/^[a-f0-9]{32}$/.test(account.passwordSalt) ||
295
+ !isTimestamp(account.createdAt))
296
+ return false;
297
+ const ids = new Set();
298
+ const hashes = new Set();
299
+ return value.sessions.every((session) => {
300
+ if (!session ||
301
+ !/^[a-f0-9-]{36}$/.test(session.id) ||
302
+ !isLabel(session.deviceName, 100) ||
303
+ !HASH_PATTERN.test(session.tokenHash) ||
304
+ !isTimestamp(session.createdAt) ||
305
+ !isTimestamp(session.lastSeenAt) ||
306
+ !isTimestamp(session.expiresAt) ||
307
+ session.expiresAt <= session.createdAt ||
308
+ session.lastSeenAt < session.createdAt ||
309
+ ids.has(session.id) ||
310
+ hashes.has(session.tokenHash))
311
+ return false;
312
+ ids.add(session.id);
313
+ hashes.add(session.tokenHash);
314
+ return true;
315
+ });
316
+ }
@@ -0,0 +1,64 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { probeConfiguredModel } from "./model-probe.js";
3
+ export declare class HubConfigurationError extends Error {
4
+ readonly status: number;
5
+ constructor(status: number, message: string);
6
+ }
7
+ export interface HubConfigurationOptions {
8
+ cwd: string;
9
+ /** Host serializes changes against active/staged runs and refreshes its idle worker. */
10
+ withMutation?: <T>(write: () => Promise<T>) => Promise<T>;
11
+ /** Recheck the device after reading its request body, before writing settings. */
12
+ isAuthorized?: (request: IncomingMessage) => Promise<boolean>;
13
+ ownerId?: (request: IncomingMessage) => Promise<string | undefined>;
14
+ /** Test seam; production always uses the shared Core client. */
15
+ probe?: typeof probeConfiguredModel;
16
+ }
17
+ /** The caller must authenticate and check Origin before dispatching any request here. */
18
+ export declare function createHubConfiguration(options: HubConfigurationOptions): {
19
+ handle: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
20
+ snapshot: () => {
21
+ workspace: {
22
+ path: string;
23
+ settingsScope: "local";
24
+ skillDirectories: string[];
25
+ };
26
+ defaults: {
27
+ auxText?: string | undefined;
28
+ text?: string | undefined;
29
+ };
30
+ connections: {
31
+ id: string;
32
+ revision: string;
33
+ catalogId: string;
34
+ tag: "image" | "text" | "video" | "audio" | "speech";
35
+ model: string;
36
+ baseUrl: string;
37
+ hasApiKey: boolean;
38
+ needsKey: boolean;
39
+ }[];
40
+ catalog: {
41
+ id: string;
42
+ displayName: string;
43
+ adapterKind: string;
44
+ defaultBaseUrl: string;
45
+ needsKey: boolean;
46
+ }[];
47
+ skills: {
48
+ disabledReason?: string | undefined;
49
+ name: string;
50
+ description: string;
51
+ source: "user" | "project" | "plugin" | "panel-app";
52
+ enabled: boolean;
53
+ }[];
54
+ mcpServers: {
55
+ name: string;
56
+ transport: string;
57
+ enabled: boolean;
58
+ }[];
59
+ restartRequired: boolean;
60
+ };
61
+ cancelOwner: (owner: string) => void;
62
+ close: () => void;
63
+ };
64
+ export type HubConfigurationSnapshot = ReturnType<ReturnType<typeof createHubConfiguration>["snapshot"]>;