@cjhyy/code-shell-server 0.9.36 → 0.9.38

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 (31) hide show
  1. package/dist/bin/code-shell-cloud-account.d.ts +2 -0
  2. package/dist/bin/code-shell-cloud-account.js +53 -0
  3. package/dist/index.serve.d.ts +4 -0
  4. package/dist/index.serve.js +3 -0
  5. package/dist/panels/management.js +12 -1
  6. package/dist/panels/types.d.ts +5 -3
  7. package/dist/project-runtime/account-adoption.d.ts +34 -0
  8. package/dist/project-runtime/account-adoption.js +280 -0
  9. package/dist/project-runtime/account-binding.d.ts +18 -0
  10. package/dist/project-runtime/account-binding.js +42 -0
  11. package/dist/project-runtime/backup.d.ts +10 -0
  12. package/dist/project-runtime/backup.js +113 -20
  13. package/dist/project-runtime/cloud-account-auth.d.ts +23 -0
  14. package/dist/project-runtime/cloud-account-auth.js +287 -0
  15. package/dist/project-runtime/cloud-browser-auth.d.ts +15 -0
  16. package/dist/project-runtime/cloud-browser-auth.js +352 -0
  17. package/dist/project-runtime/control-authority.d.ts +46 -0
  18. package/dist/project-runtime/control-authority.js +15 -0
  19. package/dist/project-runtime/control-server.d.ts +17 -0
  20. package/dist/project-runtime/control-server.js +94 -26
  21. package/dist/project-runtime/docker-provider.d.ts +6 -0
  22. package/dist/project-runtime/docker-provider.js +10 -5
  23. package/dist/project-runtime/lock.js +181 -12
  24. package/dist/project-runtime/manager.d.ts +16 -8
  25. package/dist/project-runtime/manager.js +89 -15
  26. package/dist/project-runtime/proxy.d.ts +3 -3
  27. package/dist/project-runtime/proxy.js +15 -5
  28. package/dist/project-runtime/registry.d.ts +21 -1
  29. package/dist/project-runtime/registry.js +224 -6
  30. package/dist/serve/headless-server.js +14 -0
  31. package/package.json +7 -6
@@ -2,6 +2,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { constants, chmodSync, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { acquireProjectControllerLock } from "./lock.js";
5
+ import { CLOUD_ADOPTION_PENDING, isCloudAccountBinding, isCloudAccountOrigin, isCloudMultiAccountConfiguration, } from "./account-binding.js";
5
6
  const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
6
7
  const TOKEN = /^[A-Za-z0-9_-]{43}$/;
7
8
  const STATUSES = new Set(["stopped", "starting", "running", "stopping", "error"]);
@@ -58,9 +59,16 @@ export class ProjectRegistry {
58
59
  controllerLock;
59
60
  diskDigest;
60
61
  markerEstablished = false;
62
+ storageFailed = false;
61
63
  constructor(dataDir, now = Date.now, io = {}) {
62
64
  this.now = now;
63
65
  this.io = io;
66
+ if (this.io.multiAccountOrigin !== undefined &&
67
+ (!isCloudAccountOrigin(this.io.multiAccountOrigin) || this.io.offlineAccountAdoption))
68
+ throw new Error("Multi-account Cloud requires one explicit canonical Account origin.");
69
+ if (this.io.multiAccountOrigin &&
70
+ lstatSync(join(resolve(dataDir), "hub"), { throwIfNoEntry: false }))
71
+ throw new Error("Multi-account Cloud requires a fresh installation without legacy Hub authority.");
64
72
  this.directory = resolve(dataDir, "project-control");
65
73
  const existed = existsSync(this.directory);
66
74
  mkdirSync(this.directory, { recursive: true, mode: 0o700 });
@@ -72,6 +80,16 @@ export class ProjectRegistry {
72
80
  this.marker = join(this.directory, "initialized");
73
81
  this.controllerLock = acquireProjectControllerLock(this.directory);
74
82
  try {
83
+ if (!this.io.offlineAccountAdoption) {
84
+ try {
85
+ lstatSync(join(this.directory, CLOUD_ADOPTION_PENDING));
86
+ throw new Error("Cloud account adoption is incomplete; retry its explicit offline transaction.");
87
+ }
88
+ catch (error) {
89
+ if (error.code !== "ENOENT")
90
+ throw error;
91
+ }
92
+ }
75
93
  if (existsSync(this.marker)) {
76
94
  if (privateFileBytes(this.marker, 16).toString() !== "1\n")
77
95
  throw new Error("Project initialization marker is corrupt.");
@@ -81,12 +99,26 @@ export class ProjectRegistry {
81
99
  const bytes = privateFileBytes(this.file);
82
100
  this.data = JSON.parse(bytes.toString("utf8"));
83
101
  this.validate();
102
+ this.requireConfiguredMode();
84
103
  this.diskDigest = digest(bytes);
85
104
  }
86
105
  else {
87
106
  if (existsSync(this.marker) || existed)
88
107
  throw new Error("Project registry is missing; restore the existing registry before starting");
89
- this.data = { version: 1, installationId: randomUUID(), owners: [], projects: [] };
108
+ this.data = {
109
+ version: this.io.multiAccountOrigin ? 3 : 1,
110
+ installationId: randomUUID(),
111
+ owners: [],
112
+ projects: [],
113
+ ...(this.io.multiAccountOrigin
114
+ ? {
115
+ multiAccount: {
116
+ accountOrigin: this.io.multiAccountOrigin,
117
+ managementEpoch: randomUUID(),
118
+ },
119
+ }
120
+ : {}),
121
+ };
90
122
  this.write();
91
123
  }
92
124
  if (!this.markerEstablished) {
@@ -109,6 +141,10 @@ export class ProjectRegistry {
109
141
  throw new Error("Stop all projects before restoring an installation.");
110
142
  const next = structuredClone(this.data);
111
143
  next.installationId = randomUUID();
144
+ if (next.accountBinding)
145
+ next.accountBinding.managementEpoch = randomUUID();
146
+ if (next.multiAccount)
147
+ next.multiAccount.managementEpoch = randomUUID();
112
148
  for (const project of next.projects) {
113
149
  if (project.generation > 0)
114
150
  project.generation += 1;
@@ -122,19 +158,105 @@ export class ProjectRegistry {
122
158
  this.assertStorage();
123
159
  return this.data.installationId;
124
160
  }
161
+ get accountBinding() {
162
+ this.assertStorage();
163
+ return this.data.accountBinding && { ...this.data.accountBinding };
164
+ }
165
+ get multiAccount() {
166
+ this.assertStorage();
167
+ return this.data.multiAccount && { ...this.data.multiAccount };
168
+ }
169
+ /** Called only after trusted authority verification, never with browser-selected identity. */
170
+ ensureAccountOwner(accountOrigin, accountId) {
171
+ this.assertStorage();
172
+ if (this.data.version !== 3 ||
173
+ accountOrigin !== this.data.multiAccount?.accountOrigin ||
174
+ typeof accountId !== "string" ||
175
+ !UUID.test(accountId))
176
+ throw new ProjectRegistryError(403, "此账号没有工作台权限。");
177
+ const existing = this.data.owners.find((owner) => "accountId" in owner &&
178
+ owner.accountOrigin === accountOrigin &&
179
+ owner.accountId === accountId);
180
+ if (existing)
181
+ return existing.id;
182
+ if (this.data.owners.length >= 128)
183
+ throw new ProjectRegistryError(429, "工作台账号数量已达到上限。");
184
+ const next = structuredClone(this.data), ownerId = randomUUID();
185
+ next.owners.push({ id: ownerId, accountOrigin, accountId });
186
+ this.write(next);
187
+ this.data = next;
188
+ return ownerId;
189
+ }
190
+ requireConfiguredMode() {
191
+ if (this.data.version === 3) {
192
+ if (this.io.multiAccountOrigin !== this.data.multiAccount?.accountOrigin)
193
+ throw new Error("Multi-account Cloud requires its configured Account origin; automatic mode adoption is refused.");
194
+ }
195
+ else if (this.io.multiAccountOrigin !== undefined) {
196
+ throw new Error("Multi-account Cloud requires a fresh installation; legacy adoption is refused.");
197
+ }
198
+ }
199
+ /** Offline migration only; the caller must durably preserve source evidence first. */
200
+ bindAccount(binding) {
201
+ this.assertStorage();
202
+ if (this.data.version !== 1 ||
203
+ this.data.accountBinding !== undefined ||
204
+ !isCloudAccountBinding(binding) ||
205
+ this.data.owners.length !== 1 ||
206
+ this.data.owners[0].id !== binding.ownerId ||
207
+ this.data.projects.some((project) => project.status !== "stopped"))
208
+ throw new Error("Cloud adoption requires one explicit legacy owner and stopped projects.");
209
+ const next = structuredClone(this.data);
210
+ // The real historical registry rejects version 2. Unknown metadata alone
211
+ // cannot prevent an old controller from continuing its username authority.
212
+ next.version = 2;
213
+ next.accountBinding = { ...binding };
214
+ this.write(next);
215
+ this.data = next;
216
+ }
125
217
  validate(data = this.data) {
126
218
  if (!data ||
127
- data.version !== 1 ||
219
+ ![1, 2, 3].includes(data.version) ||
128
220
  !UUID.test(data.installationId) ||
129
221
  !Array.isArray(data.owners) ||
130
222
  !Array.isArray(data.projects) ||
131
223
  data.owners.length > 128 ||
132
224
  data.projects.length > 128)
133
225
  throw new Error("Project registry is corrupt; restore its backup");
226
+ if ((data.version !== 3 && data.multiAccount !== undefined) ||
227
+ (data.version === 1 && data.accountBinding !== undefined) ||
228
+ (data.version === 2 &&
229
+ (!isCloudAccountBinding(data.accountBinding) ||
230
+ data.owners.length !== 1 ||
231
+ data.owners[0]?.id !== data.accountBinding.ownerId)) ||
232
+ (data.version === 3 &&
233
+ (data.accountBinding !== undefined ||
234
+ !isCloudMultiAccountConfiguration(data.multiAccount) ||
235
+ typeof data.installationId !== "string" ||
236
+ Object.keys(data).sort().join() !==
237
+ "installationId,multiAccount,owners,projects,version")))
238
+ throw new Error("Cloud account ownership is missing or corrupt; restore its backup.");
134
239
  const owners = new Set();
135
240
  const names = new Set();
136
241
  for (const owner of data.owners) {
242
+ if (data.version === 3) {
243
+ if (!owner ||
244
+ typeof owner.id !== "string" ||
245
+ !UUID.test(owner.id) ||
246
+ owners.has(owner.id) ||
247
+ !("accountId" in owner) ||
248
+ Object.keys(owner).sort().join() !== "accountId,accountOrigin,id" ||
249
+ owner.accountOrigin !== data.multiAccount.accountOrigin ||
250
+ typeof owner.accountId !== "string" ||
251
+ !UUID.test(owner.accountId) ||
252
+ names.has(owner.accountId))
253
+ throw new Error("Project account owner registry is corrupt");
254
+ owners.add(owner.id);
255
+ names.add(owner.accountId);
256
+ continue;
257
+ }
137
258
  if (!owner ||
259
+ !("username" in owner) ||
138
260
  !UUID.test(owner.id) ||
139
261
  typeof owner.username !== "string" ||
140
262
  !owner.username.trim() ||
@@ -149,6 +271,27 @@ export class ProjectRegistry {
149
271
  }
150
272
  const ids = new Set();
151
273
  for (const project of data.projects) {
274
+ if (data.version === 3 &&
275
+ (!project ||
276
+ typeof project.id !== "string" ||
277
+ typeof project.ownerId !== "string" ||
278
+ typeof project.runtimePassword !== "string" ||
279
+ Object.keys(project).sort().join() !==
280
+ [
281
+ "id",
282
+ "ownerId",
283
+ "generation",
284
+ "runtimeUsername",
285
+ "runtimePassword",
286
+ "name",
287
+ "status",
288
+ "createdAt",
289
+ "updatedAt",
290
+ ...(project.error === undefined ? [] : ["error"]),
291
+ ]
292
+ .sort()
293
+ .join()))
294
+ throw new Error("Project account runtime registry is corrupt");
152
295
  if (!project ||
153
296
  !UUID.test(project.id) ||
154
297
  ids.has(project.id) ||
@@ -174,6 +317,8 @@ export class ProjectRegistry {
174
317
  }
175
318
  }
176
319
  assertStorage() {
320
+ if (this.storageFailed)
321
+ throw new Error("Project registry durability failed; restart and diagnose before continuing.");
177
322
  this.controllerLock.assertHeld();
178
323
  if (this.markerEstablished && privateFileBytes(this.marker, 16).toString() !== "1\n")
179
324
  throw new Error("Project initialization marker changed.");
@@ -192,15 +337,29 @@ export class ProjectRegistry {
192
337
  throw new Error("Project registry is too large.");
193
338
  const file = join(this.directory, `.registry-${randomUUID()}.tmp`);
194
339
  let fd;
340
+ let committed = false;
195
341
  try {
196
342
  fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
197
343
  writeFileSync(fd, bytes);
198
- fsyncSync(fd);
344
+ (this.io.fsync ?? fsyncSync)(fd);
199
345
  closeSync(fd);
200
346
  fd = undefined;
201
347
  this.assertStorage();
202
348
  (this.io.rename ?? renameSync)(file, this.file);
349
+ committed = true;
203
350
  this.diskDigest = digest(bytes);
351
+ const directory = openSync(this.directory, constants.O_RDONLY);
352
+ try {
353
+ (this.io.fsync ?? fsyncSync)(directory);
354
+ }
355
+ finally {
356
+ closeSync(directory);
357
+ }
358
+ }
359
+ catch (error) {
360
+ if (committed)
361
+ this.storageFailed = true;
362
+ throw error;
204
363
  }
205
364
  finally {
206
365
  if (fd !== undefined)
@@ -214,12 +373,14 @@ export class ProjectRegistry {
214
373
  }
215
374
  list(username) {
216
375
  this.assertStorage();
217
- const owner = this.data.owners.find((item) => item.username === username);
376
+ this.requireLegacy();
377
+ const owner = this.data.owners.find((item) => "username" in item && item.username === username);
218
378
  return this.data.projects.filter((item) => item.ownerId === owner?.id).map(projectView);
219
379
  }
220
380
  get(username, id) {
221
381
  this.assertStorage();
222
- const owner = this.data.owners.find((item) => item.username === username);
382
+ this.requireLegacy();
383
+ const owner = this.data.owners.find((item) => "username" in item && item.username === username);
223
384
  const project = this.data.projects.find((item) => item.id === id && item.ownerId === owner?.id);
224
385
  if (!project)
225
386
  throw new ProjectRegistryError(404, "找不到这个项目。");
@@ -227,6 +388,39 @@ export class ProjectRegistry {
227
388
  }
228
389
  create(username, name) {
229
390
  this.assertStorage();
391
+ this.requireLegacy();
392
+ return this.createForOwner(username, name);
393
+ }
394
+ listAccount(ownerId) {
395
+ this.requireAccountOwner(ownerId);
396
+ return this.data.projects.filter((item) => item.ownerId === ownerId).map(projectView);
397
+ }
398
+ getAccount(ownerId, id) {
399
+ this.requireAccountOwner(ownerId);
400
+ const project = this.data.projects.find((item) => item.id === id && item.ownerId === ownerId);
401
+ if (!project)
402
+ throw new ProjectRegistryError(404, "找不到这个项目。");
403
+ return { ...project };
404
+ }
405
+ createAccount(ownerId, name) {
406
+ this.requireAccountOwner(ownerId);
407
+ if (this.data.version === 3)
408
+ return this.createForAccountOwner(ownerId, name);
409
+ // This username is internal legacy metadata, never the Account display name.
410
+ return this.createForOwner(this.data.owners[0].username, name);
411
+ }
412
+ requireLegacy() {
413
+ if (this.data.version !== 1)
414
+ throw new ProjectRegistryError(403, "此 Cloud 已采用明确的账号归属,请通过云账号管理。");
415
+ }
416
+ requireAccountOwner(ownerId) {
417
+ this.assertStorage();
418
+ if ((this.data.version === 2 && this.data.accountBinding?.ownerId === ownerId) ||
419
+ (this.data.version === 3 && this.data.owners.some((owner) => owner.id === ownerId)))
420
+ return;
421
+ throw new ProjectRegistryError(403, "此账号没有 Cloud 管理权限。");
422
+ }
423
+ createForOwner(username, name) {
230
424
  if (typeof name !== "string" ||
231
425
  !name.trim() ||
232
426
  name.trim().length > 80 ||
@@ -241,7 +435,7 @@ export class ProjectRegistry {
241
435
  if (this.data.projects.length >= 32)
242
436
  throw new ProjectRegistryError(429, "项目数量已达到上限(32 个)。");
243
437
  const next = structuredClone(this.data);
244
- let owner = next.owners.find((item) => item.username === username);
438
+ let owner = next.owners.find((item) => "username" in item && item.username === username);
245
439
  if (!owner) {
246
440
  owner = { id: randomUUID(), username };
247
441
  next.owners.push(owner);
@@ -263,6 +457,30 @@ export class ProjectRegistry {
263
457
  this.data = next;
264
458
  return { ...project };
265
459
  }
460
+ createForAccountOwner(ownerId, name) {
461
+ if (typeof name !== "string" ||
462
+ !name.trim() ||
463
+ name.trim().length > 80 ||
464
+ /[\x00-\x1f\x7f]/.test(name))
465
+ throw new ProjectRegistryError(400, "请输入 1 至 80 个字符的项目名称。");
466
+ if (this.data.projects.length >= 128)
467
+ throw new ProjectRegistryError(429, "工作台项目数量已达到上限(128 个)。");
468
+ const next = structuredClone(this.data), timestamp = this.now(), project = {
469
+ id: randomUUID(),
470
+ ownerId,
471
+ name: name.trim(),
472
+ status: "stopped",
473
+ generation: 0,
474
+ runtimeUsername: "project-runtime",
475
+ runtimePassword: randomBytes(32).toString("base64url"),
476
+ createdAt: timestamp,
477
+ updatedAt: timestamp,
478
+ };
479
+ next.projects.push(project);
480
+ this.write(next);
481
+ this.data = next;
482
+ return { ...project };
483
+ }
266
484
  update(id, patch) {
267
485
  this.assertStorage();
268
486
  const index = this.data.projects.findIndex((item) => item.id === id);
@@ -594,6 +594,20 @@ export async function startHeadlessServer(opts) {
594
594
  hubJson(res, 200, { status: "ok", mode: hubAuth ? "hub" : "passcode" });
595
595
  return;
596
596
  }
597
+ // The public shell probes this before login. A standalone workspace has no
598
+ // Cloud browser authority; an explicit miss selects its existing Hub gate.
599
+ if (pathname === "/api/v1/cloud-browser/capabilities") {
600
+ if (req.url !== pathname) {
601
+ hubJson(res, 400, { error: "request parameters are not supported" });
602
+ }
603
+ else if (req.method !== "GET") {
604
+ hubJson(res, 405, { error: "method not allowed" });
605
+ }
606
+ else {
607
+ hubJson(res, 404, { error: "API endpoint not found" });
608
+ }
609
+ return;
610
+ }
597
611
  if (hubAuth) {
598
612
  if (await links?.handlePublic(req, res))
599
613
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.36",
3
+ "version": "0.9.38",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,8 @@
49
49
  },
50
50
  "bin": {
51
51
  "code-shell-serve": "./dist/bin/code-shell-serve.js",
52
- "code-shell-settings-recovery": "./dist/bin/code-shell-settings-recovery.js"
52
+ "code-shell-settings-recovery": "./dist/bin/code-shell-settings-recovery.js",
53
+ "code-shell-cloud-account": "./dist/bin/code-shell-cloud-account.js"
53
54
  },
54
55
  "files": [
55
56
  "dist"
@@ -60,13 +61,13 @@
60
61
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
61
62
  },
62
63
  "dependencies": {
63
- "@cjhyy/code-shell-capability-coding": "0.9.36",
64
- "@cjhyy/code-shell-core": "0.9.36",
65
- "@cjhyy/code-shell-web": "0.9.36",
64
+ "@cjhyy/code-shell-capability-coding": "0.9.38",
65
+ "@cjhyy/code-shell-core": "0.9.38",
66
+ "@cjhyy/code-shell-web": "0.9.38",
66
67
  "@modelcontextprotocol/sdk": "^1.31.0",
67
68
  "@types/ws": "^8.18.1",
68
69
  "ws": "^8.21.0",
69
- "@cjhyy/code-shell-link": "0.9.36"
70
+ "@cjhyy/code-shell-link": "0.9.38"
70
71
  },
71
72
  "engines": {
72
73
  "node": ">=20.10"