@cjhyy/code-shell-server 0.9.51 → 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/configuration.d.ts +11 -0
- package/dist/hub/configuration.js +34 -1
- package/dist/hub/model-parameter-metadata.d.ts +26 -0
- package/dist/hub/model-parameter-metadata.js +92 -0
- 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-chat-turn.d.ts +4 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
- 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 +21 -4
- package/dist/mobile-remote/remote-host-manager.js +142 -25
- 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/directory-picker.d.ts +48 -0
- package/dist/panels/directory-picker.js +247 -0
- 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 +148 -30
- 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
package/dist/panels/runtime.js
CHANGED
|
@@ -18,6 +18,7 @@ import { PanelTaskCookieHost } from "./task-cookie-host.js";
|
|
|
18
18
|
import { taskCookieFromInput, taskCookieSelection, } from "./task-cookies.js";
|
|
19
19
|
import { desktopPanelDirectoryBookmarks, hubPanelDirectoryBookmarks, } from "./directory-bookmarks.js";
|
|
20
20
|
import { handlePanelProcessDirectory } from "./process-files.js";
|
|
21
|
+
import { PanelDirectoryPicker, capturePanelWorkspaceDirectory, directoryPickerLifetime, } from "./directory-picker.js";
|
|
21
22
|
import { PanelAppProcessService, panelExecutableDirectories, panelProcessInfo, } from "./process-service.js";
|
|
22
23
|
import { PanelManagementError } from "./management.js";
|
|
23
24
|
import { panelAutomationMethods } from "./automations.js";
|
|
@@ -305,7 +306,7 @@ function bridgeScript(id, origin) {
|
|
|
305
306
|
const requestId = String(++next);
|
|
306
307
|
const timer = setTimeout(() => {
|
|
307
308
|
pending.delete(requestId); reject(new Error("Panel request timed out"));
|
|
308
|
-
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio", "resources.recordVideo"].includes(method) ? 30 * 60 * 1000 : 60000);
|
|
309
|
+
}, ["tasks.start", "tasks.retry", "tasks.queue.set", "resources.recordAudio", "resources.recordVideo"].includes(method) ? 30 * 60 * 1000 : method === "filesystem.pickDirectory" ? 6 * 60 * 1000 : 60000);
|
|
309
310
|
pending.set(requestId, { resolve, reject, timer });
|
|
310
311
|
try {
|
|
311
312
|
parent.postMessage({ type: "codeshell-panel:call", instanceId: id, requestId, method, params }, origin);
|
|
@@ -411,6 +412,14 @@ function injectBridge(html, source) {
|
|
|
411
412
|
const offset = doctype?.[0].length ?? 0;
|
|
412
413
|
return html.slice(0, offset) + script + html.slice(offset);
|
|
413
414
|
}
|
|
415
|
+
// CSP3 and Chromium resolve 'self' in a sandboxed document to its URL's origin,
|
|
416
|
+
// but WebKit (every iOS browser) matches nothing, blocking all of a panel's own
|
|
417
|
+
// scripts and styles. Spell that origin out so every engine enforces one policy.
|
|
418
|
+
function resolveMetaPolicySelf(html, origin) {
|
|
419
|
+
return html.replace(/<meta\b[^>]*>/gi, (tag) => /\shttp-equiv\s*=\s*(["']?)content-security-policy\1/i.test(tag)
|
|
420
|
+
? tag.replaceAll("'self'", origin)
|
|
421
|
+
: tag);
|
|
422
|
+
}
|
|
414
423
|
/** Owner-bound capability grants serve reviewed static bytes to an opaque-origin iframe. */
|
|
415
424
|
export function createPanelRuntime(options) {
|
|
416
425
|
const publicPathPrefix = options.publicPathPrefix ?? "";
|
|
@@ -556,6 +565,9 @@ export function createPanelRuntime(options) {
|
|
|
556
565
|
holder: { kind: "process" },
|
|
557
566
|
}),
|
|
558
567
|
approvalScope: "guest",
|
|
568
|
+
// Match Desktop: installed process permission and the authorized project binding
|
|
569
|
+
// are consent. Every spawn still rechecks owner/revision, handles and fingerprints.
|
|
570
|
+
allowAuthorizedProcessWithoutPrompt: true,
|
|
559
571
|
resolvePackageEntry: async (owner, name) => {
|
|
560
572
|
const taskScope = toolOwners.get(owner.guestId);
|
|
561
573
|
if (taskScope) {
|
|
@@ -582,13 +594,7 @@ export function createPanelRuntime(options) {
|
|
|
582
594
|
const grant = byGuest.get(owner.guestId);
|
|
583
595
|
return !!grant && (await authorized(grant));
|
|
584
596
|
},
|
|
585
|
-
confirmExecution: async (
|
|
586
|
-
if (toolOwners.has(input.guestId))
|
|
587
|
-
return true;
|
|
588
|
-
const grant = byGuest.get(input.guestId);
|
|
589
|
-
return (!!grant &&
|
|
590
|
-
(await confirm(grant, `允许 ${input.appTitle} 运行 ${input.executable}?`, `程序将在服务端运行,可使用当前运行用户的文件与网络权限。\n程序:${input.executablePath}`)));
|
|
591
|
-
},
|
|
597
|
+
confirmExecution: async () => true,
|
|
592
598
|
});
|
|
593
599
|
const resources = new PanelResourceService({
|
|
594
600
|
rootDirectory: join(options.dataDir, "panel-app-media"),
|
|
@@ -911,6 +917,52 @@ export function createPanelRuntime(options) {
|
|
|
911
917
|
});
|
|
912
918
|
return result && (await authorized(grant));
|
|
913
919
|
}
|
|
920
|
+
async function pickDirectory(grant) {
|
|
921
|
+
if (!(await authorized(grant)))
|
|
922
|
+
error(410, "面板授权已失效。");
|
|
923
|
+
if ([...grant.confirmations.values()].some((item) => item.picker && item.result === undefined))
|
|
924
|
+
error(409, "请先完成当前目录选择。");
|
|
925
|
+
for (const [id, item] of grant.confirmations) {
|
|
926
|
+
if (item.result !== undefined)
|
|
927
|
+
grant.confirmations.delete(id);
|
|
928
|
+
}
|
|
929
|
+
if (grant.confirmations.size >= 64)
|
|
930
|
+
error(429, "待确认操作过多。");
|
|
931
|
+
const picker = new PanelDirectoryPicker(options.cwd, now);
|
|
932
|
+
const requestId = randomBytes(18).toString("base64url");
|
|
933
|
+
const pending = { picker };
|
|
934
|
+
const allowed = await new Promise((done) => {
|
|
935
|
+
const timer = setTimeout(() => pending.finish(false), directoryPickerLifetime);
|
|
936
|
+
timer.unref();
|
|
937
|
+
pending.finish = (value) => {
|
|
938
|
+
if (pending.result !== undefined)
|
|
939
|
+
return;
|
|
940
|
+
pending.result = value;
|
|
941
|
+
clearTimeout(timer);
|
|
942
|
+
picker.close();
|
|
943
|
+
done(value);
|
|
944
|
+
};
|
|
945
|
+
grant.confirmations.set(requestId, pending);
|
|
946
|
+
emit(grant, "host.confirm", {
|
|
947
|
+
requestId,
|
|
948
|
+
title: options.host === "desktop" ? "选择电脑上的目录" : "选择服务器上的目录",
|
|
949
|
+
body: "仅可选择当前项目内的目录,确认后授予此面板使用。",
|
|
950
|
+
expiresAt: picker.expiresAt,
|
|
951
|
+
directoryPicker: {
|
|
952
|
+
kind: "workspace-directory",
|
|
953
|
+
rootPath: picker.rootPath,
|
|
954
|
+
rootToken: picker.rootToken,
|
|
955
|
+
initialToken: picker.initialToken,
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
if (!(await authorized(grant)))
|
|
960
|
+
error(410, "面板授权已失效。");
|
|
961
|
+
if (!allowed)
|
|
962
|
+
return undefined;
|
|
963
|
+
pending.selection.assertCurrent();
|
|
964
|
+
return pending.selection;
|
|
965
|
+
}
|
|
914
966
|
function taskScope(grant) {
|
|
915
967
|
return {
|
|
916
968
|
instanceId: grant.id,
|
|
@@ -1166,12 +1218,19 @@ export function createPanelRuntime(options) {
|
|
|
1166
1218
|
consentTimeoutMs: 50000,
|
|
1167
1219
|
},
|
|
1168
1220
|
}),
|
|
1169
|
-
...(app.permissions.includes("resources")
|
|
1221
|
+
...(app.permissions.includes("resources") || app.permissions.includes("process")
|
|
1170
1222
|
? {
|
|
1171
1223
|
methodLimits: {
|
|
1172
|
-
"resources
|
|
1173
|
-
|
|
1224
|
+
...(app.permissions.includes("resources")
|
|
1225
|
+
? {
|
|
1226
|
+
"resources.recordAudio": { timeoutMs: 30 * 60 * 1000 },
|
|
1227
|
+
"resources.recordVideo": { timeoutMs: 30 * 60 * 1000 },
|
|
1228
|
+
}
|
|
1229
|
+
: {}),
|
|
1174
1230
|
...(app.permissions.includes("process")
|
|
1231
|
+
? { "filesystem.pickDirectory": { timeoutMs: 6 * 60_000 } }
|
|
1232
|
+
: {}),
|
|
1233
|
+
...(app.permissions.includes("process") && app.permissions.includes("resources")
|
|
1175
1234
|
? {
|
|
1176
1235
|
"tasks.start": {
|
|
1177
1236
|
maxParamsBytes: toolJobLimits.maxInputBytes,
|
|
@@ -1306,6 +1365,7 @@ export function createPanelRuntime(options) {
|
|
|
1306
1365
|
instanceId: id,
|
|
1307
1366
|
src: ASSETS + asset + "/" + app.entry.split("/").map(encodeURIComponent).join("/"),
|
|
1308
1367
|
expiresAt: grant.expiresAt,
|
|
1368
|
+
serverTime: now(),
|
|
1309
1369
|
context,
|
|
1310
1370
|
limitations: panel.compatibility.reasons,
|
|
1311
1371
|
};
|
|
@@ -1541,9 +1601,8 @@ export function createPanelRuntime(options) {
|
|
|
1541
1601
|
if (method.startsWith("agent.task.")) {
|
|
1542
1602
|
if (!agentTasks)
|
|
1543
1603
|
error(501, "当前宿主未提供独立面板任务。");
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
error(403, "你取消了面板 AI 任务。");
|
|
1604
|
+
// Match Desktop: the installed app's agent.task permission and live project
|
|
1605
|
+
// binding authorize task creation. Worker tool approvals stay independent.
|
|
1547
1606
|
return agentTasks.call(taskScope(grant), method, params);
|
|
1548
1607
|
}
|
|
1549
1608
|
if (method.startsWith("process.") || method.startsWith("filesystem.")) {
|
|
@@ -1583,16 +1642,30 @@ export function createPanelRuntime(options) {
|
|
|
1583
1642
|
if (method === "filesystem.restoreDirectory") {
|
|
1584
1643
|
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1585
1644
|
const saved = directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
const restored = await processes.grantDirectory(owner, saved);
|
|
1645
|
+
const workspaceSelection = !options.authorizePanelDirectory
|
|
1646
|
+
? capturePanelWorkspaceDirectory(options.cwd, saved)
|
|
1647
|
+
: undefined;
|
|
1648
|
+
const restored = await processes.grantDirectory(owner, saved, workspaceSelection);
|
|
1591
1649
|
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1592
1650
|
directoryBookmarks.restore(grant.app.id, options.bindingCwd ?? options.cwd, params?.bookmark);
|
|
1651
|
+
workspaceSelection?.assertCurrent();
|
|
1593
1652
|
return { ...restored, bookmark: params.bookmark };
|
|
1594
1653
|
}
|
|
1595
|
-
|
|
1654
|
+
if (method === "filesystem.pickDirectory") {
|
|
1655
|
+
const selection = await pickDirectory(grant);
|
|
1656
|
+
if (!selection)
|
|
1657
|
+
return { cancelled: true };
|
|
1658
|
+
selection.assertCurrent();
|
|
1659
|
+
const selected = await processes.grantDirectory(owner, selection.path, selection);
|
|
1660
|
+
selection.assertCurrent();
|
|
1661
|
+
if (!(await authorized(grant)))
|
|
1662
|
+
error(410, "面板授权已失效。");
|
|
1663
|
+
selection.assertCurrent();
|
|
1664
|
+
const bookmark = directoryBookmarks.remember(grant.app.id, options.bindingCwd ?? options.cwd, selected.path);
|
|
1665
|
+
processes.directoryPath(owner, selected.handle);
|
|
1666
|
+
return { ...selected, bookmark };
|
|
1667
|
+
}
|
|
1668
|
+
const name = params?.name;
|
|
1596
1669
|
const directory = name === "project"
|
|
1597
1670
|
? options.cwd
|
|
1598
1671
|
: name === "downloads"
|
|
@@ -1604,16 +1677,13 @@ export function createPanelRuntime(options) {
|
|
|
1604
1677
|
: undefined;
|
|
1605
1678
|
if (!directory)
|
|
1606
1679
|
error(400, "不支持这个服务端目录。");
|
|
1607
|
-
if (method === "filesystem.pickDirectory" &&
|
|
1608
|
-
!(await confirm(grant, "使用服务端下载目录?", directory)))
|
|
1609
|
-
return { cancelled: true };
|
|
1610
1680
|
await options.authorizePanelDirectory?.(grant.app, options.bindingCwd ?? options.cwd, options.cwd);
|
|
1611
1681
|
if (name !== "project")
|
|
1612
1682
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1613
1683
|
if (!(await authorized(grant)))
|
|
1614
1684
|
error(410, "面板授权已失效。");
|
|
1615
1685
|
const selected = await processes.grantDirectory(owner, directory);
|
|
1616
|
-
if (
|
|
1686
|
+
if (!["downloads", "project"].includes(String(name)))
|
|
1617
1687
|
return selected;
|
|
1618
1688
|
const bookmark = directoryBookmarks.remember(grant.app.id, options.bindingCwd ?? options.cwd, selected.path);
|
|
1619
1689
|
if (!(await authorized(grant)))
|
|
@@ -1780,7 +1850,7 @@ export function createPanelRuntime(options) {
|
|
|
1780
1850
|
}
|
|
1781
1851
|
const directoryMatch = /^\/api\/v1\/panels\/runtime\/([\w-]+)\/directory\/([a-f0-9-]{36})$/.exec(url.pathname);
|
|
1782
1852
|
const resourceMatch = /^\/api\/v1\/panels\/runtime\/([\w-]+)\/resources\/((?:asset|external)-[a-f0-9]{64})$/.exec(url.pathname);
|
|
1783
|
-
const match = /^\/api\/v1\/panels\/runtime\/([\w-]+)(?:\/(call|renew|events|confirm|tool-results))?$/.exec(url.pathname) ??
|
|
1853
|
+
const match = /^\/api\/v1\/panels\/runtime\/([\w-]+)(?:\/(call|renew|events|confirm|directory-picker|tool-results))?$/.exec(url.pathname) ??
|
|
1784
1854
|
directoryMatch ??
|
|
1785
1855
|
resourceMatch;
|
|
1786
1856
|
const grant = match && grants.get(match[1]);
|
|
@@ -1839,7 +1909,7 @@ export function createPanelRuntime(options) {
|
|
|
1839
1909
|
if (!(await authorized(grant)))
|
|
1840
1910
|
error(410, "面板授权已失效。");
|
|
1841
1911
|
grant.expiresAt = now() + GRANT_TTL;
|
|
1842
|
-
json(response, 200, { expiresAt: grant.expiresAt });
|
|
1912
|
+
json(response, 200, { expiresAt: grant.expiresAt, serverTime: now() });
|
|
1843
1913
|
}
|
|
1844
1914
|
else if (request.method === "GET" && match[2] === "events") {
|
|
1845
1915
|
const after = Number(url.searchParams.get("after") ?? 0);
|
|
@@ -1848,17 +1918,63 @@ export function createPanelRuntime(options) {
|
|
|
1848
1918
|
grant.events = grant.events.filter((entry) => entry.id > after);
|
|
1849
1919
|
grant.eventBytes = grant.events.reduce((total, entry) => total + Buffer.byteLength(JSON.stringify(entry.payload) ?? "null"), 0);
|
|
1850
1920
|
const batch = grant.events.slice(0, 128);
|
|
1851
|
-
json(response, 200, {
|
|
1921
|
+
json(response, 200, {
|
|
1922
|
+
events: batch,
|
|
1923
|
+
cursor: batch.at(-1)?.id ?? after,
|
|
1924
|
+
serverTime: now(),
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
else if (request.method === "POST" && match[2] === "directory-picker") {
|
|
1928
|
+
if (request.headers.origin !== grant.origin)
|
|
1929
|
+
error(403, "目录选择仅供当前宿主页面使用。");
|
|
1930
|
+
const input = await body(request);
|
|
1931
|
+
if (Object.keys(input).some((key) => !["requestId", "directoryToken"].includes(key)))
|
|
1932
|
+
error(400, "目录选择参数无效。");
|
|
1933
|
+
const pending = typeof input.requestId === "string" && grant.confirmations.get(input.requestId);
|
|
1934
|
+
if (!pending || !pending.picker || pending.result !== undefined)
|
|
1935
|
+
error(409, "目录选择已失效,请重新发起。");
|
|
1936
|
+
const assertPending = async () => {
|
|
1937
|
+
if (!(await authorized(grant)))
|
|
1938
|
+
error(410, "面板授权已失效。");
|
|
1939
|
+
if (pending.result !== undefined || now() >= pending.picker.expiresAt) {
|
|
1940
|
+
pending.finish(false);
|
|
1941
|
+
error(409, "目录选择已失效,请重新发起。");
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
await assertPending();
|
|
1945
|
+
const listing = await pending.picker.list(input.directoryToken, assertPending);
|
|
1946
|
+
await assertPending();
|
|
1947
|
+
pending.picker.assertListing(listing);
|
|
1948
|
+
json(response, 200, { requestId: input.requestId, ...listing });
|
|
1852
1949
|
}
|
|
1853
1950
|
else if (request.method === "POST" && match[2] === "confirm") {
|
|
1854
1951
|
const input = await body(request);
|
|
1855
1952
|
const pending = typeof input.requestId === "string" && grant.confirmations.get(input.requestId);
|
|
1856
1953
|
if (!pending ||
|
|
1857
1954
|
typeof input.allowed !== "boolean" ||
|
|
1858
|
-
Object.keys(input).some((key) => !["requestId", "allowed"].includes(key))
|
|
1955
|
+
Object.keys(input).some((key) => !["requestId", "allowed", ...(pending?.picker ? ["directoryToken"] : [])].includes(key)) ||
|
|
1956
|
+
(input.allowed === false && input.directoryToken !== undefined))
|
|
1859
1957
|
error(400, "找不到这个待确认操作。");
|
|
1860
1958
|
if (!(await authorized(grant)))
|
|
1861
1959
|
error(410, "面板授权已失效。");
|
|
1960
|
+
if (pending.picker) {
|
|
1961
|
+
if (request.headers.origin !== grant.origin)
|
|
1962
|
+
error(403, "目录选择仅供当前宿主页面使用。");
|
|
1963
|
+
if (input.allowed) {
|
|
1964
|
+
if (pending.result === undefined) {
|
|
1965
|
+
if (now() >= pending.picker.expiresAt) {
|
|
1966
|
+
pending.finish(false);
|
|
1967
|
+
error(409, "目录选择已失效,请重新发起。");
|
|
1968
|
+
}
|
|
1969
|
+
pending.selection = pending.picker.select(input.directoryToken);
|
|
1970
|
+
pending.directoryToken = input.directoryToken;
|
|
1971
|
+
}
|
|
1972
|
+
else if (pending.result !== true ||
|
|
1973
|
+
input.directoryToken !== pending.directoryToken) {
|
|
1974
|
+
error(409, "目录选择已失效,请重新发起。");
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1862
1978
|
pending.finish(input.allowed);
|
|
1863
1979
|
json(response, 200, { accepted: true, allowed: pending.result });
|
|
1864
1980
|
}
|
|
@@ -1984,12 +2100,14 @@ export function createPanelRuntime(options) {
|
|
|
1984
2100
|
// including when the public site is behind HTTPS termination.
|
|
1985
2101
|
const source = publicPathPrefix + ASSETS + grant.asset + "/";
|
|
1986
2102
|
if (extname(path).toLowerCase() === ".html") {
|
|
1987
|
-
bytes = Buffer.from(injectBridge(bytes.toString("utf8"), source));
|
|
1988
2103
|
const resourceOrigin = grant.origin;
|
|
2104
|
+
bytes = Buffer.from(resolveMetaPolicySelf(injectBridge(bytes.toString("utf8"), source), resourceOrigin));
|
|
1989
2105
|
const assetSource = resourceOrigin + source;
|
|
1990
2106
|
// Panels export their own generated files (for example JSON backups).
|
|
1991
2107
|
// Download permission does not grant origin access, network fetches or popups.
|
|
1992
2108
|
// The embedding iframe must enable the same permission.
|
|
2109
|
+
// Forms need submission events and method=dialog for local UI. The
|
|
2110
|
+
// enforcing form-action policy still blocks every form navigation.
|
|
1993
2111
|
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src " +
|
|
1994
2112
|
assetSource +
|
|
1995
2113
|
" 'wasm-unsafe-eval'; style-src " +
|
|
@@ -2002,7 +2120,7 @@ export function createPanelRuntime(options) {
|
|
|
2002
2120
|
assetSource +
|
|
2003
2121
|
" data:; connect-src " +
|
|
2004
2122
|
assetSource +
|
|
2005
|
-
"; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads; frame-ancestors " +
|
|
2123
|
+
"; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads allow-forms; frame-ancestors " +
|
|
2006
2124
|
resourceOrigin);
|
|
2007
2125
|
response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), display-capture=()");
|
|
2008
2126
|
}
|
|
@@ -5,9 +5,13 @@ export declare class ToolJobStorage {
|
|
|
5
5
|
private root;
|
|
6
6
|
private identity;
|
|
7
7
|
private nonce;
|
|
8
|
+
private ownerClaimFile;
|
|
8
9
|
private readonly directories;
|
|
9
10
|
constructor(configuredRoot: string);
|
|
10
11
|
initialize(): Promise<void>;
|
|
12
|
+
private assertDeadOwner;
|
|
13
|
+
private acquireOwnerClaim;
|
|
14
|
+
private releaseOwnerClaim;
|
|
11
15
|
directory(id?: string, work?: boolean, create?: boolean): Promise<string>;
|
|
12
16
|
read(path: string, limit: number): Promise<unknown>;
|
|
13
17
|
write(id: string, value: unknown, limit: number): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { lstat, mkdir, open, realpath, readdir, rename, rm, rmdir, unlink } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
export function toolJobJson(value, limit) {
|
|
6
6
|
let text;
|
|
@@ -20,6 +20,7 @@ export class ToolJobStorage {
|
|
|
20
20
|
root = "";
|
|
21
21
|
identity = "";
|
|
22
22
|
nonce = "";
|
|
23
|
+
ownerClaimFile = "";
|
|
23
24
|
directories = new Map();
|
|
24
25
|
constructor(configuredRoot) {
|
|
25
26
|
this.configuredRoot = configuredRoot;
|
|
@@ -33,43 +34,114 @@ export class ToolJobStorage {
|
|
|
33
34
|
throw new Error("invalid tool job root");
|
|
34
35
|
this.root = await realpath(this.configuredRoot);
|
|
35
36
|
this.identity = `${initial.dev}:${initial.ino}`;
|
|
36
|
-
|
|
37
|
-
const create = () => open(lockPath, "wx", 0o600);
|
|
38
|
-
let lock;
|
|
37
|
+
this.nonce = randomUUID();
|
|
39
38
|
try {
|
|
40
|
-
|
|
39
|
+
await this.acquireOwnerClaim();
|
|
40
|
+
// Keep the previous file format as an interoperability barrier for older
|
|
41
|
+
// Hosts. All current Hosts hold the atomic directory claim before touching it.
|
|
42
|
+
const lockPath = join(this.root, "owner.lock");
|
|
43
|
+
try {
|
|
44
|
+
const current = (await this.read(lockPath, 1024));
|
|
45
|
+
this.assertDeadOwner(current.pid);
|
|
46
|
+
await rm(lockPath);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== "ENOENT")
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
const lock = await open(lockPath, "wx", 0o600);
|
|
53
|
+
try {
|
|
54
|
+
await lock.writeFile(JSON.stringify({ pid: process.pid, nonce: this.nonce }));
|
|
55
|
+
await lock.sync();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await lock.close();
|
|
59
|
+
}
|
|
41
60
|
}
|
|
42
61
|
catch (error) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
await this.releaseOwnerClaim();
|
|
63
|
+
this.nonce = "";
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
assertDeadOwner(pid) {
|
|
68
|
+
if (!Number.isSafeInteger(pid) || Number(pid) <= 1) {
|
|
69
|
+
throw new Error("invalid tool job ownership lock");
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
process.kill(Number(pid), 0);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === "ESRCH")
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new Error("tool job storage is already owned by a running Host");
|
|
79
|
+
}
|
|
80
|
+
async acquireOwnerClaim() {
|
|
81
|
+
const destination = join(this.root, "owner.claim");
|
|
82
|
+
const temporary = join(this.root, `.owner-${this.nonce}`);
|
|
83
|
+
const marker = `${this.nonce}.json`;
|
|
84
|
+
await mkdir(temporary, { mode: 0o700 });
|
|
85
|
+
try {
|
|
86
|
+
const file = await open(join(temporary, marker), "wx", 0o600);
|
|
87
|
+
try {
|
|
88
|
+
await file.writeFile(JSON.stringify({ pid: process.pid, nonce: this.nonce }));
|
|
89
|
+
await file.sync();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await file.close();
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await rename(temporary, destination);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!["EEXIST", "ENOTEMPTY", "EPERM"].includes(error.code ?? ""))
|
|
99
|
+
throw error;
|
|
100
|
+
const directory = await lstat(destination);
|
|
101
|
+
if (!directory.isDirectory() || directory.isSymbolicLink())
|
|
102
|
+
throw new Error("invalid tool job ownership claim", { cause: error });
|
|
103
|
+
const entries = await readdir(destination);
|
|
104
|
+
if (entries.length > 1 || entries.some((entry) => !/^[a-f0-9-]{36}\.json$/.test(entry))) {
|
|
105
|
+
throw new Error("invalid tool job ownership claim", { cause: error });
|
|
51
106
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
107
|
+
if (entries[0]) {
|
|
108
|
+
const stalePath = join(destination, entries[0]);
|
|
109
|
+
try {
|
|
110
|
+
const current = (await this.read(stalePath, 1024));
|
|
111
|
+
this.assertDeadOwner(current.pid);
|
|
112
|
+
// Delete only the observed owner's unique marker. A delayed
|
|
113
|
+
// reclaimer cannot unlink a new owner's different marker.
|
|
114
|
+
await unlink(stalePath);
|
|
115
|
+
}
|
|
116
|
+
catch (cause) {
|
|
117
|
+
if (cause.code !== "ENOENT")
|
|
118
|
+
throw cause;
|
|
119
|
+
}
|
|
55
120
|
}
|
|
121
|
+
// Never move the existing claim: a replacement may already own it.
|
|
122
|
+
// Some platforms cannot replace even an empty destination directory.
|
|
123
|
+
// rmdir is safe here: it cannot remove a newly published nonempty claim.
|
|
124
|
+
await rmdir(destination).catch((cause) => {
|
|
125
|
+
if (cause.code !== "ENOENT")
|
|
126
|
+
throw cause;
|
|
127
|
+
});
|
|
128
|
+
await rename(temporary, destination);
|
|
56
129
|
}
|
|
57
|
-
|
|
58
|
-
throw new Error("invalid tool job ownership lock", { cause: error });
|
|
59
|
-
if (alive)
|
|
60
|
-
throw new Error("tool job storage is already owned by a running Host", { cause: error });
|
|
61
|
-
await rm(lockPath);
|
|
62
|
-
lock = await create();
|
|
63
|
-
}
|
|
64
|
-
this.nonce = randomUUID();
|
|
65
|
-
try {
|
|
66
|
-
await lock.writeFile(JSON.stringify({ pid: process.pid, nonce: this.nonce }));
|
|
67
|
-
await lock.sync();
|
|
130
|
+
this.ownerClaimFile = join(destination, marker);
|
|
68
131
|
}
|
|
69
132
|
finally {
|
|
70
|
-
await
|
|
133
|
+
await rm(temporary, { recursive: true, force: true });
|
|
71
134
|
}
|
|
72
135
|
}
|
|
136
|
+
async releaseOwnerClaim() {
|
|
137
|
+
if (!this.ownerClaimFile)
|
|
138
|
+
return;
|
|
139
|
+
await unlink(this.ownerClaimFile).catch((error) => {
|
|
140
|
+
if (error.code !== "ENOENT")
|
|
141
|
+
throw error;
|
|
142
|
+
});
|
|
143
|
+
this.ownerClaimFile = "";
|
|
144
|
+
}
|
|
73
145
|
async directory(id, work = false, create = false) {
|
|
74
146
|
const info = await lstat(this.root);
|
|
75
147
|
if (!info.isDirectory() ||
|
|
@@ -186,12 +258,17 @@ export class ToolJobStorage {
|
|
|
186
258
|
async close() {
|
|
187
259
|
if (!this.nonce)
|
|
188
260
|
return;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
261
|
+
try {
|
|
262
|
+
const path = join(await this.directory(), "owner.lock");
|
|
263
|
+
const lock = (await this.read(path, 1024));
|
|
264
|
+
if (lock.nonce === this.nonce)
|
|
265
|
+
await rm(path);
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await this.releaseOwnerClaim();
|
|
269
|
+
this.nonce = "";
|
|
270
|
+
this.directories.clear();
|
|
271
|
+
}
|
|
195
272
|
}
|
|
196
273
|
async remove(id) {
|
|
197
274
|
const path = await this.directory(id);
|
package/dist/panels/types.d.ts
CHANGED
|
@@ -1,97 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export interface PanelCompatibility {
|
|
3
|
-
supported: boolean;
|
|
4
|
-
reasons: string[];
|
|
5
|
-
}
|
|
6
|
-
export interface PanelGitSource {
|
|
7
|
-
kind: "git";
|
|
8
|
-
url: string;
|
|
9
|
-
/** The original branch/tag, retained separately from the pinned archive commit. */
|
|
10
|
-
ref: string;
|
|
11
|
-
subdir?: string;
|
|
12
|
-
commit: string;
|
|
13
|
-
}
|
|
14
|
-
export interface ManagedPanel extends Omit<PanelAppPreview, "reviewToken" | "alreadyInstalled" | "warnings" | "source"> {
|
|
15
|
-
packageDigest?: string;
|
|
16
|
-
revision: string;
|
|
17
|
-
bound: boolean;
|
|
18
|
-
enabled: boolean;
|
|
19
|
-
globalDisabled: boolean;
|
|
20
|
-
updatable: boolean;
|
|
21
|
-
source: {
|
|
22
|
-
kind: "git" | "local";
|
|
23
|
-
label: string;
|
|
24
|
-
url?: string;
|
|
25
|
-
ref?: string;
|
|
26
|
-
commit?: string;
|
|
27
|
-
subdir?: string;
|
|
28
|
-
};
|
|
29
|
-
compatibility: PanelCompatibility;
|
|
30
|
-
}
|
|
31
|
-
export interface PanelPackageIssue {
|
|
32
|
-
id: string;
|
|
33
|
-
revision: string;
|
|
34
|
-
code: "package_unavailable";
|
|
35
|
-
version?: string;
|
|
36
|
-
packageDigest?: string;
|
|
37
|
-
bound: boolean;
|
|
38
|
-
globalDisabled: boolean;
|
|
39
|
-
}
|
|
40
|
-
export interface PanelSnapshot {
|
|
41
|
-
panels: ManagedPanel[];
|
|
42
|
-
issues?: PanelPackageIssue[];
|
|
43
|
-
workspace: string;
|
|
44
|
-
hasProject: boolean;
|
|
45
|
-
canRestorePackages?: boolean;
|
|
46
|
-
}
|
|
47
|
-
export interface PanelDiscovery {
|
|
48
|
-
panels: GitPanelAppDiscoveryCandidate[];
|
|
49
|
-
issues: GitPanelAppDiscoveryIssue[];
|
|
50
|
-
source: PanelGitSource;
|
|
51
|
-
}
|
|
52
|
-
export interface PanelReview {
|
|
53
|
-
reviewToken: string;
|
|
54
|
-
expiresAt: number;
|
|
55
|
-
kind: "install" | "update";
|
|
56
|
-
preview: Omit<PanelAppPreview, "reviewToken">;
|
|
57
|
-
source: PanelGitSource;
|
|
58
|
-
expectedRevision: string | null;
|
|
59
|
-
compatibility: PanelCompatibility;
|
|
60
|
-
/** The installed package selected for this project, when its manifest is available. */
|
|
61
|
-
installedVersion?: string;
|
|
62
|
-
/** Reviewed permissions of that same installed package; an empty array is a known baseline. */
|
|
63
|
-
installedPermissions?: PanelAppPreview["permissions"];
|
|
64
|
-
}
|
|
65
|
-
/** Trusted native hosts may review local sources without exposing paths to Web routes. */
|
|
66
|
-
export type PanelProjectReview = Omit<PanelReview, "source">;
|
|
67
|
-
export interface PanelOperationContext {
|
|
68
|
-
ownerId: string;
|
|
69
|
-
authorize: () => boolean | Promise<boolean>;
|
|
70
|
-
}
|
|
71
|
-
export interface PanelPackageVersion {
|
|
72
|
-
version: string;
|
|
73
|
-
packageDigest: string;
|
|
74
|
-
permissions: PanelAppPreview["permissions"];
|
|
75
|
-
compatibility: PanelCompatibility;
|
|
76
|
-
}
|
|
77
|
-
export interface PanelPackageHistory {
|
|
78
|
-
appId: string;
|
|
79
|
-
title: PanelAppPreview["title"];
|
|
80
|
-
expectedRevision: string;
|
|
81
|
-
current: {
|
|
82
|
-
version: string;
|
|
83
|
-
packageDigest?: string;
|
|
84
|
-
unavailable?: boolean;
|
|
85
|
-
};
|
|
86
|
-
versions: PanelPackageVersion[];
|
|
87
|
-
unavailablePackages: number;
|
|
88
|
-
}
|
|
89
|
-
export interface PanelPackageRestoreReview extends PanelPackageVersion {
|
|
90
|
-
appId: string;
|
|
91
|
-
title: PanelAppPreview["title"];
|
|
92
|
-
current: PanelPackageHistory["current"];
|
|
93
|
-
addedPermissions: PanelAppPreview["permissions"];
|
|
94
|
-
expectedRevision: string;
|
|
95
|
-
reviewToken: string;
|
|
96
|
-
expiresAt: number;
|
|
97
|
-
}
|
|
1
|
+
export type { PanelCompatibility, PanelGitSource, ManagedPanel, PanelPackageIssue, PanelSnapshot, PanelDiscovery, PanelReview, PanelProjectReview, PanelOperationContext, PanelPackageVersion, PanelPackageHistory, PanelPackageRestoreReview, } from "@cjhyy/code-shell-web/panels";
|
|
@@ -19,6 +19,14 @@ export interface DeviceRelayConnectorOptions {
|
|
|
19
19
|
/** Additional trusted CA for private installations; TLS verification stays enabled. */
|
|
20
20
|
ca?: ConnectionOptions["ca"];
|
|
21
21
|
onState?: (state: DeviceRelayState) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Opt-in: lets the directory mint a one-time phone pairing token for its
|
|
24
|
+
* owner. Omitted, the feature is neither advertised nor accepted.
|
|
25
|
+
*/
|
|
26
|
+
issuePairing?: () => {
|
|
27
|
+
token: string;
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
export interface DeviceRelayConnector {
|
|
24
32
|
/** Idempotent while running. Callers must await close before restarting. */
|