@cjhyy/code-shell-server 0.9.30 → 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.
- package/dist/links/http.js +2 -0
- package/dist/links/service.js +33 -1
- package/dist/panels/directory-bookmarks.d.ts +7 -0
- package/dist/panels/directory-bookmarks.js +65 -1
- package/dist/panels/management.d.ts +4 -2
- package/dist/panels/management.js +6 -4
- package/dist/panels/process-service.d.ts +3 -1
- package/dist/panels/process-service.js +7 -1
- package/package.json +5 -5
package/dist/links/http.js
CHANGED
|
@@ -113,6 +113,7 @@ export function createLinkHttp(options) {
|
|
|
113
113
|
"connectionId",
|
|
114
114
|
"expectedRevision",
|
|
115
115
|
"authModeId",
|
|
116
|
+
"resourceUrl",
|
|
116
117
|
]);
|
|
117
118
|
result = await service.startAuthorization(owner, input, input.authModeId);
|
|
118
119
|
}
|
|
@@ -170,6 +171,7 @@ export function createLinkHttp(options) {
|
|
|
170
171
|
"connectionId",
|
|
171
172
|
"expectedRevision",
|
|
172
173
|
...(token ? ["token"] : []),
|
|
174
|
+
...(url.pathname.endsWith("/remote") ? ["resourceUrl"] : []),
|
|
173
175
|
]);
|
|
174
176
|
result = token
|
|
175
177
|
? await service.connectToken(owner, input)
|
package/dist/links/service.js
CHANGED
|
@@ -186,6 +186,14 @@ export function createLinkService(options = {}) {
|
|
|
186
186
|
id: meta.linkAccountId,
|
|
187
187
|
label: meta.linkAccountLabel,
|
|
188
188
|
resources: meta.linkResourceLabels?.slice(0, 100) ?? [],
|
|
189
|
+
...(remote && meta.linkResourceGroups
|
|
190
|
+
? {
|
|
191
|
+
resourceGroups: meta.linkResourceGroups.map((group) => ({
|
|
192
|
+
id: group.id,
|
|
193
|
+
items: group.items.map((item) => ({ id: item.id, label: item.label })),
|
|
194
|
+
})),
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
189
197
|
},
|
|
190
198
|
capabilityIds: meta.linkCapabilityIds ?? [],
|
|
191
199
|
verifiedAt: meta.linkLastVerifiedAt,
|
|
@@ -664,6 +672,8 @@ export function createLinkService(options = {}) {
|
|
|
664
672
|
async function startAuthorization(context, input, rawAuthModeId) {
|
|
665
673
|
await authorize(context);
|
|
666
674
|
const authModeId = string(rawAuthModeId, 80);
|
|
675
|
+
if (input?.resourceUrl !== undefined && authModeId !== "remote-link")
|
|
676
|
+
throw new LinkServiceError(400, "invalid_request");
|
|
667
677
|
if (authModeId === "remote-link")
|
|
668
678
|
await refreshRemoteCatalog(context);
|
|
669
679
|
const provider = snapshot().providers.find((candidate) => candidate.id === input?.providerId);
|
|
@@ -1029,7 +1039,12 @@ export function createLinkService(options = {}) {
|
|
|
1029
1039
|
checkCapacity();
|
|
1030
1040
|
if (!input ||
|
|
1031
1041
|
input.methodId !== "remote-link" ||
|
|
1032
|
-
!REMOTE_LINK_ADAPTERS.some((adapter) => adapter.id === input.providerId)
|
|
1042
|
+
!REMOTE_LINK_ADAPTERS.some((adapter) => adapter.id === input.providerId) ||
|
|
1043
|
+
(input.resourceUrl !== undefined &&
|
|
1044
|
+
(input.providerId !== "figma" ||
|
|
1045
|
+
!input.connectionId ||
|
|
1046
|
+
typeof input.resourceUrl !== "string" ||
|
|
1047
|
+
!input.resourceUrl)))
|
|
1033
1048
|
throw new LinkServiceError(400, "invalid_request");
|
|
1034
1049
|
const config = options.remoteLink?.();
|
|
1035
1050
|
if (!config)
|
|
@@ -1055,11 +1070,24 @@ export function createLinkService(options = {}) {
|
|
|
1055
1070
|
throw new LinkServiceError(409, "conflict");
|
|
1056
1071
|
}
|
|
1057
1072
|
const prepared = { id, expected, input: { ...input, label: string(input.label) } };
|
|
1073
|
+
if (input.resourceUrl !== undefined && !expected)
|
|
1074
|
+
throw new LinkServiceError(400, "invalid_request");
|
|
1058
1075
|
let attempt;
|
|
1059
1076
|
try {
|
|
1077
|
+
let resourceFiles;
|
|
1078
|
+
if (input.providerId === "figma") {
|
|
1079
|
+
// Recover the old whitelist from Host custody. Browser input can only suggest one new file.
|
|
1080
|
+
const groups = expected?.meta?.linkResourceGroups;
|
|
1081
|
+
if (expected && (!groups || groups.length !== 1 || groups[0]?.id !== "files"))
|
|
1082
|
+
throw new Error("Invalid saved Figma resources");
|
|
1083
|
+
resourceFiles = groups?.[0]?.items.map((item) => item.id) ?? [];
|
|
1084
|
+
if (input.resourceUrl !== undefined)
|
|
1085
|
+
resourceFiles.push(input.resourceUrl);
|
|
1086
|
+
}
|
|
1060
1087
|
attempt = beginRemoteLinkAuthorization(config, now(), {
|
|
1061
1088
|
providerId: input.providerId,
|
|
1062
1089
|
actions: capabilities.actions,
|
|
1090
|
+
...(resourceFiles !== undefined ? { resourceFiles } : {}),
|
|
1063
1091
|
});
|
|
1064
1092
|
}
|
|
1065
1093
|
catch {
|
|
@@ -1123,6 +1151,10 @@ export function createLinkService(options = {}) {
|
|
|
1123
1151
|
},
|
|
1124
1152
|
});
|
|
1125
1153
|
await check();
|
|
1154
|
+
if (job.attempt.providerId === "figma" &&
|
|
1155
|
+
job.prepared.expected &&
|
|
1156
|
+
credential.meta?.linkAccountId !== job.prepared.expected.meta?.linkAccountId)
|
|
1157
|
+
throw new LinkServiceError(409, "conflict");
|
|
1126
1158
|
const guarded = {
|
|
1127
1159
|
ownerId: context.ownerId,
|
|
1128
1160
|
authorize: async () => {
|
|
@@ -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
|
|
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
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type PanelAppPreview, type PanelAppSourceInput } from "@cjhyy/code-shell-core";
|
|
2
|
-
import { getRefCommit } from "@cjhyy/code-shell-core/internal/skills";
|
|
3
2
|
import type { PanelCompatibility, PanelDiscovery, PanelOperationContext, PanelReview, PanelProjectReview, PanelSnapshot, PanelPackageHistory, PanelPackageRestoreReview } from "./types.js";
|
|
4
3
|
export declare class PanelManagementError extends Error {
|
|
5
4
|
readonly status: number;
|
|
@@ -23,7 +22,10 @@ export interface PanelManagementOptions {
|
|
|
23
22
|
compatibility?: (app: Pick<PanelAppPreview, "permissions" | "agent">) => PanelCompatibility;
|
|
24
23
|
now?: () => number;
|
|
25
24
|
/** Network seam only; production still uses the reviewed Core installer. */
|
|
26
|
-
resolveCommit?:
|
|
25
|
+
resolveCommit?: (info: {
|
|
26
|
+
owner: string;
|
|
27
|
+
repo: string;
|
|
28
|
+
}, ref: string) => Promise<string>;
|
|
27
29
|
}
|
|
28
30
|
export declare function createPanelManagement(options: PanelManagementOptions): {
|
|
29
31
|
snapshot: (appId?: string) => Promise<PanelSnapshot>;
|
|
@@ -3,9 +3,9 @@ import { panelPackageMutationMatches } from "./package-mutation.js";
|
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { lstatSync, mkdirSync, statSync } from "node:fs";
|
|
5
5
|
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
|
|
6
|
+
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolveGitHubPanelAppCommit, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
|
|
7
7
|
import { mutateJsonFile } from "@cjhyy/code-shell-core/internal";
|
|
8
|
-
import {
|
|
8
|
+
import { parseGithubUrl } from "@cjhyy/code-shell-core/internal/skills";
|
|
9
9
|
const REVIEW_TTL = 8 * 60_000;
|
|
10
10
|
const MAX_REVIEWS = 8;
|
|
11
11
|
const MAX_NETWORK = 2;
|
|
@@ -47,7 +47,7 @@ export function publicPanelError(error) {
|
|
|
47
47
|
return new PanelManagementError(503, "github_unavailable", "GitHub 暂时不可用,请稍后重试。");
|
|
48
48
|
if (/(?:ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|UND_ERR_SOCKET)/.test(code) ||
|
|
49
49
|
/^(?:fetch failed|Failed to fetch)$/i.test(message) ||
|
|
50
|
-
/GitHub source download failed/i.test(message))
|
|
50
|
+
/GitHub source (?:download|ref lookup) failed/i.test(message))
|
|
51
51
|
return new PanelManagementError(502, "github_network", "服务端无法连接 GitHub,请检查网络、DNS 或代理设置后重试。");
|
|
52
52
|
if (error instanceof PanelAppInstallError)
|
|
53
53
|
return new PanelManagementError(400, "invalid_source", error.message);
|
|
@@ -352,7 +352,9 @@ export function createPanelManagement(options) {
|
|
|
352
352
|
const ref = parsed.source.ref ?? "HEAD";
|
|
353
353
|
const commit = SHA.test(ref)
|
|
354
354
|
? ref.toLowerCase()
|
|
355
|
-
:
|
|
355
|
+
: options.resolveCommit
|
|
356
|
+
? await options.resolveCommit({ owner: parsed.owner, repo: parsed.repo }, ref)
|
|
357
|
+
: await resolveGitHubPanelAppCommit(parsed.source);
|
|
356
358
|
await guard();
|
|
357
359
|
if (!SHA.test(commit))
|
|
358
360
|
throw new PanelManagementError(502, "invalid_source", "GitHub 没有返回有效的提交版本。");
|
|
@@ -84,7 +84,9 @@ export declare class PanelAppProcessService {
|
|
|
84
84
|
name: string;
|
|
85
85
|
handle?: string;
|
|
86
86
|
}>;
|
|
87
|
-
grantDirectory(owner: PanelProcessOwner, path: string
|
|
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.
|
|
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.
|
|
64
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
65
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
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.
|
|
69
|
+
"@cjhyy/code-shell-link": "0.9.32"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=20.10"
|