@cjhyy/code-shell-server 0.9.31 → 0.9.32

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.
@@ -1,5 +1,10 @@
1
1
  import type { InstalledPanelApp } from "@cjhyy/code-shell-core";
2
2
  export type PanelDirectoryAuthorizer = (app: InstalledPanelApp, projectPath: string, workspacePath: string) => Promise<void>;
3
+ /** Host-only proof that a workspace still belongs to the expected main project. */
4
+ export declare function createPanelDirectoryProjectScope(workspacePath: string, expectedProjectPath: string): {
5
+ projectPath: string;
6
+ assertCurrent(): void;
7
+ };
3
8
  export declare function desktopPanelDirectoryBookmarks(dataDir: string): PanelAppDirectoryBookmarks;
4
9
  /** The file mutex uses a sibling of its parent; keep both inside the data volume. */
5
10
  export declare function hubPanelDirectoryBookmarks(dataDir: string): PanelAppDirectoryBookmarks;
@@ -9,6 +14,8 @@ export declare class PanelAppDirectoryBookmarks {
9
14
  private readonly options;
10
15
  constructor(file: string, options?: {
11
16
  legacyFiles?: readonly string[];
17
+ /** Supplied only by a trusted Host, never by Panel or browser input. */
18
+ isLegacyProjectScope?: (expectedProjectPath: string, legacyProjectPath: string) => boolean;
12
19
  });
13
20
  remember(appId: string, projectPath: string, path: string): string;
14
21
  restore(appId: string, projectPath: string, id: unknown): string;
@@ -2,9 +2,58 @@ import { randomUUID } from "node:crypto";
2
2
  import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  import { acquireFileLock } from "@cjhyy/code-shell-core/internal";
5
+ import { createHubPanelBinding } from "./hub-binding.js";
6
+ /** Host-only proof that a workspace still belongs to the expected main project. */
7
+ export function createPanelDirectoryProjectScope(workspacePath, expectedProjectPath) {
8
+ for (const path of [workspacePath, expectedProjectPath]) {
9
+ if (!isAbsolute(path) || path.length > 32_768 || path.includes("\0"))
10
+ throw new Error("Invalid Panel directory project scope");
11
+ }
12
+ const binding = createHubPanelBinding(workspacePath);
13
+ binding.assertBinding();
14
+ const projectPath = realpathSync(expectedProjectPath);
15
+ const identity = lstatSync(projectPath);
16
+ if (!identity.isDirectory() ||
17
+ identity.isSymbolicLink() ||
18
+ realpathSync(binding.bindingCwd) !== projectPath)
19
+ throw new Error("Panel directory project scope is unavailable");
20
+ const assertCurrent = () => {
21
+ binding.assertBinding();
22
+ const current = lstatSync(projectPath);
23
+ if (!current.isDirectory() ||
24
+ current.isSymbolicLink() ||
25
+ current.dev !== identity.dev ||
26
+ current.ino !== identity.ino ||
27
+ realpathSync(expectedProjectPath) !== projectPath ||
28
+ realpathSync(binding.bindingCwd) !== projectPath)
29
+ throw new Error("Panel directory project scope changed");
30
+ };
31
+ assertCurrent();
32
+ return { projectPath, assertCurrent };
33
+ }
5
34
  export function desktopPanelDirectoryBookmarks(dataDir) {
35
+ const scopes = new Map();
6
36
  return new PanelAppDirectoryBookmarks(join(dataDir, "panel-app-directory-bookmarks.json"), {
7
37
  legacyFiles: [join(dataDir, "panel-web-directory-bookmarks.json")],
38
+ isLegacyProjectScope(expectedProjectPath, legacyProjectPath) {
39
+ const key = JSON.stringify([expectedProjectPath, legacyProjectPath]);
40
+ try {
41
+ let scope = scopes.get(key);
42
+ if (!scope) {
43
+ scope = createPanelDirectoryProjectScope(legacyProjectPath, expectedProjectPath);
44
+ if (scopes.size >= 64)
45
+ scopes.delete(scopes.keys().next().value);
46
+ scopes.set(key, scope);
47
+ }
48
+ // Keep a failed proof frozen: replacing Git metadata must not silently
49
+ // reauthorize this instance's previously verified worktree association.
50
+ scope.assertCurrent();
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ },
8
57
  });
9
58
  }
10
59
  /** The file mutex uses a sibling of its parent; keep both inside the data volume. */
@@ -156,8 +205,22 @@ export class PanelAppDirectoryBookmarks {
156
205
  }
157
206
  }
158
207
  }
159
- if (!bookmark || bookmark.appId !== appId || bookmark.projectPath !== projectPath)
208
+ if (!bookmark || bookmark.appId !== appId)
160
209
  throw new Error("Saved directory is unavailable; choose it again");
210
+ const savedProjectPath = bookmark.projectPath;
211
+ const assertProjectScope = () => {
212
+ if (savedProjectPath === projectPath)
213
+ return;
214
+ try {
215
+ if (this.options.isLegacyProjectScope?.(projectPath, savedProjectPath) === true)
216
+ return;
217
+ }
218
+ catch {
219
+ // The Host proof is optional; a failed proof never relaxes exact scope.
220
+ }
221
+ throw new Error("Saved directory is unavailable; choose it again");
222
+ };
223
+ assertProjectScope();
161
224
  try {
162
225
  const info = lstatSync(bookmark.path);
163
226
  if (!info.isDirectory() ||
@@ -195,6 +258,7 @@ export class PanelAppDirectoryBookmarks {
195
258
  release();
196
259
  }
197
260
  }
261
+ assertProjectScope();
198
262
  return bookmark.path;
199
263
  }
200
264
  }
@@ -84,7 +84,9 @@ export declare class PanelAppProcessService {
84
84
  name: string;
85
85
  handle?: string;
86
86
  }>;
87
- grantDirectory(owner: PanelProcessOwner, path: string): Promise<{
87
+ grantDirectory(owner: PanelProcessOwner, path: string, options?: {
88
+ assertCurrent?: () => void;
89
+ }): Promise<{
88
90
  handle: string;
89
91
  path: string;
90
92
  name: string;
@@ -221,14 +221,20 @@ export class PanelAppProcessService {
221
221
  });
222
222
  return { available: true, name, handle };
223
223
  }
224
- async grantDirectory(owner, path) {
224
+ async grantDirectory(owner, path, options = {}) {
225
225
  owner = { ...owner };
226
226
  const epoch = this.epoch(owner);
227
+ options.assertCurrent?.();
227
228
  await this.authorize(owner, epoch);
229
+ options.assertCurrent?.();
228
230
  const resolved = await realpath(path);
231
+ options.assertCurrent?.();
229
232
  await this.authorize(owner, epoch);
233
+ options.assertCurrent?.();
230
234
  const info = await stat(resolved);
235
+ options.assertCurrent?.();
231
236
  await this.authorize(owner, epoch);
237
+ options.assertCurrent?.();
232
238
  this.assertLive(owner, epoch);
233
239
  if (!info.isDirectory())
234
240
  throw new Error("selected process directory is not a directory");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.31",
3
+ "version": "0.9.32",
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",
@@ -60,13 +60,13 @@
60
60
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
61
61
  },
62
62
  "dependencies": {
63
- "@cjhyy/code-shell-capability-coding": "0.9.31",
64
- "@cjhyy/code-shell-core": "0.9.31",
65
- "@cjhyy/code-shell-web": "0.9.31",
63
+ "@cjhyy/code-shell-capability-coding": "0.9.32",
64
+ "@cjhyy/code-shell-core": "0.9.32",
65
+ "@cjhyy/code-shell-web": "0.9.32",
66
66
  "@modelcontextprotocol/sdk": "^1.31.0",
67
67
  "@types/ws": "^8.18.1",
68
68
  "ws": "^8.21.0",
69
- "@cjhyy/code-shell-link": "0.9.31"
69
+ "@cjhyy/code-shell-link": "0.9.32"
70
70
  },
71
71
  "engines": {
72
72
  "node": ">=20.10"