@world-engines/project-host 0.1.0-alpha.0
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/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { removeLocalPreviewViteConfig, writeLocalPreviewViteConfig } from "./local-preview-vite-config.js";
|
|
6
|
+
import { LOCAL_GUI_HOST, LOCAL_GUI_ORIGIN } from "./local-gui-contract.js";
|
|
7
|
+
/** 浏览器使用独立 hostname,避免 127.0.0.1 的 host-only GUI cookie 随 Preview 请求发送。 */
|
|
8
|
+
export const LOCAL_PREVIEW_HOST = "localhost";
|
|
9
|
+
/** Socket 始终绑定 IPv4 loopback,避免 localhost 的 IPv6 DNS 解析歧义。 */
|
|
10
|
+
export const LOCAL_PREVIEW_BIND_HOST = "127.0.0.1";
|
|
11
|
+
export const MAX_PREVIEW_OUTPUT_BYTES = 32 * 1024;
|
|
12
|
+
export class PreviewRuntimeError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "PreviewRuntimeError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const CAPABILITY_PREFIX = "/_worldengine_preview/";
|
|
21
|
+
const outputTails = new WeakMap();
|
|
22
|
+
function redactOutputChunk(previous, next) {
|
|
23
|
+
const combined = previous + next;
|
|
24
|
+
const complete = combined.replace(/\/_worldengine_preview\/[a-f0-9]{32}\//giu, "/_worldengine_preview/[redacted]/");
|
|
25
|
+
const marker = complete.lastIndexOf(CAPABILITY_PREFIX);
|
|
26
|
+
if (marker >= 0 && !complete.slice(marker).startsWith("/_worldengine_preview/[redacted]/")) {
|
|
27
|
+
return { visible: complete.slice(0, marker), tail: complete.slice(marker) };
|
|
28
|
+
}
|
|
29
|
+
for (let length = Math.min(CAPABILITY_PREFIX.length - 1, complete.length); length > 0; length -= 1) {
|
|
30
|
+
if (complete.endsWith(CAPABILITY_PREFIX.slice(0, length))) {
|
|
31
|
+
return { visible: complete.slice(0, -length), tail: complete.slice(-length) };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { visible: complete, tail: "" };
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function isInside(root, candidate) {
|
|
40
|
+
const path = relative(root, candidate);
|
|
41
|
+
return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
|
|
42
|
+
}
|
|
43
|
+
function copySession(session) {
|
|
44
|
+
return Object.freeze({ ...session });
|
|
45
|
+
}
|
|
46
|
+
function appendOutput(session, field, truncated, chunk) {
|
|
47
|
+
if (session[truncated])
|
|
48
|
+
return;
|
|
49
|
+
const tails = outputTails.get(session) ?? { stdout: "", stderr: "" };
|
|
50
|
+
outputTails.set(session, tails);
|
|
51
|
+
const next = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
52
|
+
const sanitized = redactOutputChunk(tails[field], next);
|
|
53
|
+
const text = sanitized.visible;
|
|
54
|
+
tails[field] = sanitized.tail;
|
|
55
|
+
const remaining = MAX_PREVIEW_OUTPUT_BYTES - Buffer.byteLength(session[field], "utf8");
|
|
56
|
+
if (remaining <= 0) {
|
|
57
|
+
session[truncated] = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const bytes = Buffer.from(text, "utf8");
|
|
61
|
+
if (bytes.byteLength <= remaining) {
|
|
62
|
+
session[field] += text;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
session[field] += bytes.subarray(0, remaining).toString("utf8");
|
|
66
|
+
session[truncated] = true;
|
|
67
|
+
}
|
|
68
|
+
export function previewChildEnvironment(base = process.env, nodeExecutable = process.execPath) {
|
|
69
|
+
const currentPath = base.PATH ?? base.Path ?? base.path ?? "";
|
|
70
|
+
const nodeDirectory = dirname(nodeExecutable);
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
PATH: currentPath.length === 0 ? nodeDirectory : `${nodeDirectory}${delimiter}${currentPath}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function spawnForPreview(command, args, cwd) {
|
|
77
|
+
return spawn(command, args, {
|
|
78
|
+
cwd,
|
|
79
|
+
env: previewChildEnvironment(),
|
|
80
|
+
detached: true,
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
windowsHide: true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function previewNpmInvocation() {
|
|
86
|
+
if (process.platform !== "win32")
|
|
87
|
+
return { command: "npm", argsPrefix: [] };
|
|
88
|
+
// npm.cmd 不能与 detached process group 可靠组合;直接运行同一 Node 安装内的 npm CLI。
|
|
89
|
+
return {
|
|
90
|
+
command: process.execPath,
|
|
91
|
+
argsPrefix: [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function collectProcessOutput(command, args) {
|
|
95
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
96
|
+
const child = spawn(command, args, { windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
97
|
+
let output = "";
|
|
98
|
+
child.stdout.on("data", (chunk) => { output += chunk.toString(); });
|
|
99
|
+
child.once("error", rejectPromise);
|
|
100
|
+
child.once("exit", (code) => {
|
|
101
|
+
if (code === 0 || code === 1)
|
|
102
|
+
resolvePromise(output);
|
|
103
|
+
else
|
|
104
|
+
rejectPromise(new Error(`${command} 退出码 ${code ?? 1}`));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function findListeningPids(port) {
|
|
109
|
+
if (process.platform !== "win32")
|
|
110
|
+
return [];
|
|
111
|
+
const output = await collectProcessOutput("netstat.exe", ["-ano", "-p", "tcp"]);
|
|
112
|
+
const matcher = new RegExp(`^\\s*TCP\\s+127\\.0\\.0\\.1:${port}\\s+\\S+\\s+LISTENING\\s+(\\d+)\\s*$`, "gim");
|
|
113
|
+
const pids = new Set();
|
|
114
|
+
for (const match of output.matchAll(matcher)) {
|
|
115
|
+
const pid = Number(match[1]);
|
|
116
|
+
if (Number.isSafeInteger(pid) && pid > 0)
|
|
117
|
+
pids.add(pid);
|
|
118
|
+
}
|
|
119
|
+
return [...pids];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 先绑定独立 Preview loopback host 的动态端口;调用方在 spawn 前的最后一刻释放它。
|
|
123
|
+
* Vite 仍以 strictPort 启动,随后必须把监听 PID 回读到本 process tree,不能把抢占的端口当作 Preview。
|
|
124
|
+
*/
|
|
125
|
+
async function reserveLoopbackPort() {
|
|
126
|
+
const server = createServer();
|
|
127
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
128
|
+
server.once("error", rejectPromise);
|
|
129
|
+
server.listen({ host: LOCAL_PREVIEW_BIND_HOST, port: 0 }, () => {
|
|
130
|
+
server.off("error", rejectPromise);
|
|
131
|
+
resolvePromise();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
const address = server.address();
|
|
135
|
+
if (address === null || typeof address === "string" || address.address !== LOCAL_PREVIEW_BIND_HOST || address.port <= 0) {
|
|
136
|
+
await new Promise((resolvePromise, rejectPromise) => server.close((error) => error === undefined ? resolvePromise() : rejectPromise(error)));
|
|
137
|
+
throw new PreviewRuntimeError("E_PREVIEW_START_FAILED", "无法保留 loopback Preview 端口");
|
|
138
|
+
}
|
|
139
|
+
let released = false;
|
|
140
|
+
return {
|
|
141
|
+
port: address.port,
|
|
142
|
+
async release() {
|
|
143
|
+
if (released)
|
|
144
|
+
return;
|
|
145
|
+
released = true;
|
|
146
|
+
await new Promise((resolvePromise, rejectPromise) => server.close((error) => error === undefined ? resolvePromise() : rejectPromise(error)));
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function terminateProcessTree(pid) {
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
await collectProcessOutput("taskkill.exe", ["/PID", String(pid), "/T", "/F"]);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
process.kill(-pid, "SIGTERM");
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.code !== "ESRCH")
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function getProcessCommandLine(pid) {
|
|
164
|
+
try {
|
|
165
|
+
if (process.platform === "win32") {
|
|
166
|
+
const output = await collectProcessOutput("powershell.exe", [
|
|
167
|
+
"-NoProfile",
|
|
168
|
+
"-NonInteractive",
|
|
169
|
+
"-Command",
|
|
170
|
+
`Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}' | Select-Object -ExpandProperty CommandLine`,
|
|
171
|
+
]);
|
|
172
|
+
return output.trim() || null;
|
|
173
|
+
}
|
|
174
|
+
if (process.platform === "linux") {
|
|
175
|
+
return (await readFile(`/proc/${pid}/cmdline`, "utf8")).replaceAll("\0", " ").trim() || null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// 无法读到进程身份时必须保持端口占用,不得猜测并终止。
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
async function isProcessInTree(rootPid, pid) {
|
|
184
|
+
if (rootPid === pid)
|
|
185
|
+
return true;
|
|
186
|
+
try {
|
|
187
|
+
if (process.platform === "win32") {
|
|
188
|
+
const output = await collectProcessOutput("powershell.exe", [
|
|
189
|
+
"-NoProfile",
|
|
190
|
+
"-NonInteractive",
|
|
191
|
+
"-Command",
|
|
192
|
+
`$current = ${pid}; while ($current -gt 0) { $process = Get-CimInstance Win32_Process -Filter \"ProcessId = $current\"; if ($null -eq $process) { break }; if ($process.ParentProcessId -eq ${rootPid}) { Write-Output true; exit 0 }; $current = $process.ParentProcessId }; Write-Output false; exit 0`,
|
|
193
|
+
]);
|
|
194
|
+
return output.trim().toLowerCase() === "true";
|
|
195
|
+
}
|
|
196
|
+
if (process.platform === "linux") {
|
|
197
|
+
let current = pid;
|
|
198
|
+
while (current > 0) {
|
|
199
|
+
const fields = (await readFile(`/proc/${current}/stat`, "utf8")).split(" ");
|
|
200
|
+
const parent = Number(fields[3]);
|
|
201
|
+
if (parent === rootPid)
|
|
202
|
+
return true;
|
|
203
|
+
if (!Number.isSafeInteger(parent) || parent <= 0 || parent === current)
|
|
204
|
+
return false;
|
|
205
|
+
current = parent;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// 身份不可验证时必须 fail closed,不能把同端口的其他服务认作 Preview。
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
function ledgerPath(projectRoot) {
|
|
215
|
+
return join(projectRoot, ".worldengine", "preview-session.json");
|
|
216
|
+
}
|
|
217
|
+
function isPreviewSessionLedger(value) {
|
|
218
|
+
return isRecord(value)
|
|
219
|
+
&& value.schema_version === 1
|
|
220
|
+
&& typeof value.session_id === "string"
|
|
221
|
+
&& typeof value.project_root === "string"
|
|
222
|
+
&& typeof value.cwd === "string"
|
|
223
|
+
&& typeof value.port === "number"
|
|
224
|
+
&& Number.isSafeInteger(value.port)
|
|
225
|
+
&& value.port > 0
|
|
226
|
+
&& value.port <= 65535
|
|
227
|
+
&& typeof value.pid === "number"
|
|
228
|
+
&& Number.isSafeInteger(value.pid)
|
|
229
|
+
&& value.pid > 0
|
|
230
|
+
&& (value.url === undefined || typeof value.url === "string");
|
|
231
|
+
}
|
|
232
|
+
async function readPreviewSessionLedger(projectRoot) {
|
|
233
|
+
try {
|
|
234
|
+
const value = JSON.parse(await readFile(ledgerPath(projectRoot), "utf8"));
|
|
235
|
+
return isPreviewSessionLedger(value) ? value : null;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** 跨进程只读 owner 探针;只返回 PID、listener 与 cwd 三者都能验证的 live ledger。 */
|
|
242
|
+
export async function readLivePreviewSessionOwner(projectRoot) {
|
|
243
|
+
const root = await realpath(resolve(projectRoot));
|
|
244
|
+
const ledger = await readPreviewSessionLedger(root);
|
|
245
|
+
if (ledger === null || ledger.project_root !== root || ledger.cwd !== await realpath(join(root, "view")))
|
|
246
|
+
return null;
|
|
247
|
+
if (!(await findListeningPids(ledger.port)).includes(ledger.pid))
|
|
248
|
+
return null;
|
|
249
|
+
const commandLine = await getProcessCommandLine(ledger.pid);
|
|
250
|
+
if (commandLine === null || !commandReferencesPath(commandLine, ledger.cwd))
|
|
251
|
+
return null;
|
|
252
|
+
if (typeof ledger.url !== "string")
|
|
253
|
+
return null;
|
|
254
|
+
let url;
|
|
255
|
+
try {
|
|
256
|
+
url = new URL(ledger.url);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (url.protocol !== "http:" || url.hostname !== LOCAL_PREVIEW_HOST || Number(url.port) !== ledger.port
|
|
262
|
+
|| !/^\/_worldengine_preview\/[a-f0-9]{32}\/$/u.test(url.pathname) || url.search || url.hash)
|
|
263
|
+
return null;
|
|
264
|
+
return Object.freeze({ session_id: ledger.session_id, pid: ledger.pid, port: ledger.port, project_root: root, cwd: ledger.cwd, url: url.href });
|
|
265
|
+
}
|
|
266
|
+
async function writePreviewSessionLedger(session) {
|
|
267
|
+
if (session.pid === null)
|
|
268
|
+
throw new PreviewRuntimeError("E_PREVIEW_START_FAILED", "Preview 进程未返回 PID");
|
|
269
|
+
const path = ledgerPath(session.project_root);
|
|
270
|
+
await mkdir(join(session.project_root, ".worldengine"), { recursive: true });
|
|
271
|
+
await writeFile(path, `${JSON.stringify({
|
|
272
|
+
schema_version: 1,
|
|
273
|
+
session_id: session.session_id,
|
|
274
|
+
project_root: session.project_root,
|
|
275
|
+
cwd: session.cwd,
|
|
276
|
+
port: session.port,
|
|
277
|
+
pid: session.pid,
|
|
278
|
+
url: session.url,
|
|
279
|
+
})}\n`, "utf8");
|
|
280
|
+
}
|
|
281
|
+
async function clearPreviewSessionLedger(session) {
|
|
282
|
+
const ledger = await readPreviewSessionLedger(session.project_root);
|
|
283
|
+
if (ledger?.session_id === session.session_id && ledger.pid === session.pid) {
|
|
284
|
+
await rm(ledgerPath(session.project_root), { force: true });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function commandReferencesPath(commandLine, path) {
|
|
288
|
+
return commandLine.replaceAll("/", "\\").toLowerCase().includes(path.replaceAll("/", "\\").toLowerCase());
|
|
289
|
+
}
|
|
290
|
+
/** ProjectHost 的本地 Vite Preview 生命周期;每个 session 独占一个内核分配的 loopback 端口。 */
|
|
291
|
+
export class LocalPreviewRuntime {
|
|
292
|
+
#now;
|
|
293
|
+
#newSessionId;
|
|
294
|
+
#spawn;
|
|
295
|
+
#findListeningPids;
|
|
296
|
+
#getProcessCommandLine;
|
|
297
|
+
#isProcessInTree;
|
|
298
|
+
#terminateProcessTree;
|
|
299
|
+
#reserveLoopbackPort;
|
|
300
|
+
#session = null;
|
|
301
|
+
#configPath = null;
|
|
302
|
+
#basePath = "/";
|
|
303
|
+
#parentOrigin = LOCAL_GUI_ORIGIN;
|
|
304
|
+
#childClosed = null;
|
|
305
|
+
constructor(dependencies = {}) {
|
|
306
|
+
this.#now = dependencies.now ?? Date.now;
|
|
307
|
+
this.#newSessionId = dependencies.newSessionId ?? (() => crypto.randomUUID());
|
|
308
|
+
this.#spawn = dependencies.spawn ?? spawnForPreview;
|
|
309
|
+
this.#findListeningPids = dependencies.findListeningPids ?? findListeningPids;
|
|
310
|
+
this.#getProcessCommandLine = dependencies.getProcessCommandLine ?? getProcessCommandLine;
|
|
311
|
+
this.#isProcessInTree = dependencies.isProcessInTree ?? isProcessInTree;
|
|
312
|
+
this.#terminateProcessTree = dependencies.terminateProcessTree ?? terminateProcessTree;
|
|
313
|
+
this.#reserveLoopbackPort = dependencies.reserveLoopbackPort ?? reserveLoopbackPort;
|
|
314
|
+
}
|
|
315
|
+
async start(projectRoot, parentOrigin = LOCAL_GUI_ORIGIN) {
|
|
316
|
+
let parent;
|
|
317
|
+
try {
|
|
318
|
+
parent = new URL(parentOrigin);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
throw new PreviewRuntimeError("E_PREVIEW_START_FAILED", "Preview parent origin 无效");
|
|
322
|
+
}
|
|
323
|
+
if (parent.protocol !== "http:" || parent.hostname !== LOCAL_GUI_HOST || parent.port === "" || parent.pathname !== "/" || parent.search !== "" || parent.hash !== "" || parent.username !== "" || parent.password !== "") {
|
|
324
|
+
throw new PreviewRuntimeError("E_PREVIEW_START_FAILED", "Preview parent origin 无效");
|
|
325
|
+
}
|
|
326
|
+
const trustedParentOrigin = parent.origin;
|
|
327
|
+
const root = await realpath(resolve(projectRoot));
|
|
328
|
+
const view = await realpath(join(root, "view"));
|
|
329
|
+
if (!isInside(root, view) || view === root) {
|
|
330
|
+
throw new PreviewRuntimeError("E_PATH_OUTSIDE_PROJECT", "Preview cwd 必须是项目内 view/");
|
|
331
|
+
}
|
|
332
|
+
if (this.#session?.state === "starting" || this.#session?.state === "running") {
|
|
333
|
+
if (this.#session.project_root === root && this.#parentOrigin === trustedParentOrigin)
|
|
334
|
+
return copySession(this.#session);
|
|
335
|
+
await this.stop();
|
|
336
|
+
}
|
|
337
|
+
const ledger = await readPreviewSessionLedger(root);
|
|
338
|
+
const listeningPids = ledger === null ? [] : await this.#findListeningPids(ledger.port);
|
|
339
|
+
if (ledger?.project_root === root && ledger.cwd === view && listeningPids.includes(ledger.pid)) {
|
|
340
|
+
const commandLine = await this.#getProcessCommandLine(ledger.pid);
|
|
341
|
+
if (commandLine !== null && commandReferencesPath(commandLine, view)) {
|
|
342
|
+
await this.#terminateProcessTree(ledger.pid);
|
|
343
|
+
await clearPreviewSessionLedger({ ...ledger, url: `http://${LOCAL_PREVIEW_HOST}:${ledger.port}/`, state: "stopped", started_at_ms: 0, stopped_at_ms: 0, exit_code: null, stdout: "", stderr: "", stdout_truncated: false, stderr_truncated: false });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const reservation = await this.#reserveLoopbackPort();
|
|
347
|
+
const npm = previewNpmInvocation();
|
|
348
|
+
let child;
|
|
349
|
+
try {
|
|
350
|
+
const config = await writeLocalPreviewViteConfig(root, view, reservation.port, trustedParentOrigin);
|
|
351
|
+
this.#configPath = config.path;
|
|
352
|
+
this.#basePath = config.basePath;
|
|
353
|
+
this.#parentOrigin = trustedParentOrigin;
|
|
354
|
+
await reservation.release();
|
|
355
|
+
child = this.#spawn(npm.command, [...npm.argsPrefix, "--prefix", view, "run", "dev", "--", "--config", config.path, "--host", LOCAL_PREVIEW_BIND_HOST, "--port", String(reservation.port), "--strictPort"], view);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
await reservation.release();
|
|
359
|
+
if (this.#configPath !== null)
|
|
360
|
+
await removeLocalPreviewViteConfig(root, this.#configPath);
|
|
361
|
+
this.#configPath = null;
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
const session = {
|
|
365
|
+
session_id: this.#newSessionId(),
|
|
366
|
+
project_root: root,
|
|
367
|
+
cwd: view,
|
|
368
|
+
url: `http://${LOCAL_PREVIEW_HOST}:${reservation.port}${this.#basePath}`,
|
|
369
|
+
port: reservation.port,
|
|
370
|
+
state: "starting",
|
|
371
|
+
pid: child.pid ?? null,
|
|
372
|
+
started_at_ms: this.#now(),
|
|
373
|
+
stopped_at_ms: null,
|
|
374
|
+
exit_code: null,
|
|
375
|
+
stdout: "",
|
|
376
|
+
stderr: "",
|
|
377
|
+
stdout_truncated: false,
|
|
378
|
+
stderr_truncated: false,
|
|
379
|
+
};
|
|
380
|
+
this.#session = session;
|
|
381
|
+
this.#childClosed = new Promise((resolvePromise) => {
|
|
382
|
+
child.once("close", () => resolvePromise());
|
|
383
|
+
});
|
|
384
|
+
try {
|
|
385
|
+
await writePreviewSessionLedger(session);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
this.#session = null;
|
|
389
|
+
if (session.pid !== null)
|
|
390
|
+
await this.#terminateProcessTree(session.pid);
|
|
391
|
+
if (error instanceof PreviewRuntimeError)
|
|
392
|
+
throw error;
|
|
393
|
+
throw new PreviewRuntimeError("E_PREVIEW_START_FAILED", "无法持久化 Preview session ledger");
|
|
394
|
+
}
|
|
395
|
+
child.stdout.on("data", (chunk) => {
|
|
396
|
+
appendOutput(session, "stdout", "stdout_truncated", chunk);
|
|
397
|
+
void this.#verifyReadyListener(session);
|
|
398
|
+
});
|
|
399
|
+
child.stderr.on("data", (chunk) => appendOutput(session, "stderr", "stderr_truncated", chunk));
|
|
400
|
+
child.once("error", (error) => {
|
|
401
|
+
appendOutput(session, "stderr", "stderr_truncated", error.stack ?? error.message);
|
|
402
|
+
session.state = "failed";
|
|
403
|
+
session.stopped_at_ms = this.#now();
|
|
404
|
+
void clearPreviewSessionLedger(session);
|
|
405
|
+
});
|
|
406
|
+
child.once("exit", (code) => {
|
|
407
|
+
session.exit_code = code;
|
|
408
|
+
session.stopped_at_ms = this.#now();
|
|
409
|
+
if (session.state !== "stopped")
|
|
410
|
+
session.state = code === 0 ? "stopped" : "failed";
|
|
411
|
+
void clearPreviewSessionLedger(session);
|
|
412
|
+
});
|
|
413
|
+
child.once("close", () => {
|
|
414
|
+
const configPath = this.#configPath;
|
|
415
|
+
this.#configPath = null;
|
|
416
|
+
this.#childClosed = null;
|
|
417
|
+
if (configPath !== null)
|
|
418
|
+
void removeLocalPreviewViteConfig(session.project_root, configPath);
|
|
419
|
+
});
|
|
420
|
+
return copySession(session);
|
|
421
|
+
}
|
|
422
|
+
async #verifyReadyListener(session) {
|
|
423
|
+
if (session !== this.#session || session.state !== "starting" || session.pid === null)
|
|
424
|
+
return;
|
|
425
|
+
if (!session.stdout.includes(`http://${LOCAL_PREVIEW_BIND_HOST}:${session.port}`))
|
|
426
|
+
return;
|
|
427
|
+
const pids = await this.#findListeningPids(session.port);
|
|
428
|
+
for (const pid of pids) {
|
|
429
|
+
if (await this.#isProcessInTree(session.pid, pid)) {
|
|
430
|
+
session.state = "running";
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
appendOutput(session, "stderr", "stderr_truncated", "Preview ready 后未在 child process tree 中验证 loopback listener PID");
|
|
435
|
+
session.state = "failed";
|
|
436
|
+
session.stopped_at_ms = this.#now();
|
|
437
|
+
await this.#terminateProcessTree(session.pid);
|
|
438
|
+
await clearPreviewSessionLedger(session);
|
|
439
|
+
}
|
|
440
|
+
inspect() {
|
|
441
|
+
return this.#session === null ? null : copySession(this.#session);
|
|
442
|
+
}
|
|
443
|
+
async stop() {
|
|
444
|
+
const session = this.#session;
|
|
445
|
+
if (session === null)
|
|
446
|
+
return null;
|
|
447
|
+
if (session.pid !== null && (session.state === "starting" || session.state === "running")) {
|
|
448
|
+
await this.#terminateProcessTree(session.pid);
|
|
449
|
+
if (this.#childClosed !== null)
|
|
450
|
+
await this.#childClosed;
|
|
451
|
+
}
|
|
452
|
+
if (session.state === "starting" || session.state === "running") {
|
|
453
|
+
session.state = "stopped";
|
|
454
|
+
session.stopped_at_ms = this.#now();
|
|
455
|
+
}
|
|
456
|
+
await clearPreviewSessionLedger(session);
|
|
457
|
+
// 配置只在 child stdio/handle 真正 close 后清理,避免 Windows 下 Vite 仍持有 view/。
|
|
458
|
+
return copySession(session);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProjectTransferTransport } from "./transfer/transport.js";
|
|
2
|
+
/** 与 native broker 一致;覆盖 96 MiB author-source wire 加 JSON response 外壳。 */
|
|
3
|
+
export declare const PROJECT_AUTH_MAX_RESPONSE_BYTES: number;
|
|
4
|
+
/** 与 Edge frozen-artifacts JSON body gate 一致。 */
|
|
5
|
+
export declare const PROJECT_AUTH_MAX_HTTP_REQUEST_BYTES: number;
|
|
6
|
+
/** body_json 作为 pipe 外层 JSON 字符串传输时的统一 envelope gate。 */
|
|
7
|
+
export declare const PROJECT_AUTH_MAX_PIPE_REQUEST_BYTES: number;
|
|
8
|
+
export interface ProjectBoundBrokerCapability {
|
|
9
|
+
readonly pipe_name: string;
|
|
10
|
+
readonly challenge_id: string;
|
|
11
|
+
readonly challenge: string;
|
|
12
|
+
readonly expires_at_ms: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ProjectAuthTransportEnvironment {
|
|
15
|
+
readonly WORLDENGINE_PROJECT_TRANSFER_PIPE?: string;
|
|
16
|
+
readonly WORLDENGINE_PROJECT_TRANSFER_CHALLENGE_ID?: string;
|
|
17
|
+
readonly WORLDENGINE_PROJECT_TRANSFER_CHALLENGE?: string;
|
|
18
|
+
readonly WORLDENGINE_PROJECT_TRANSFER_EXPIRES_AT_MS?: string;
|
|
19
|
+
}
|
|
20
|
+
/** 只供同模块测试验证有界累计;错误不包含 payload、capability 或 secret。 */
|
|
21
|
+
export declare function appendProjectAuthResponseChunk(chunks: Buffer[], total: number, chunk: Buffer): number;
|
|
22
|
+
export declare function assertProjectAuthRequestBudget(bodyByteLength: number, envelopeByteLength: number): void;
|
|
23
|
+
/**
|
|
24
|
+
* CLI production composition:只消费 desktop 注入的短期 named-pipe capability。
|
|
25
|
+
* 环境中没有 URL、cookie 或 header authority;SID/project/challenge 仍由 native broker 复验。
|
|
26
|
+
*/
|
|
27
|
+
export declare function createProjectAuthTransport(projectId: string, environment?: ProjectAuthTransportEnvironment): Promise<ProjectTransferTransport | undefined>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const PIPE_PATTERN = /^\\\\\.\\pipe\\worldengine-project-transfer-auth-[a-f0-9]{64}$/i;
|
|
7
|
+
/** 与 native broker 一致;覆盖 96 MiB author-source wire 加 JSON response 外壳。 */
|
|
8
|
+
export const PROJECT_AUTH_MAX_RESPONSE_BYTES = 128 * 1024 * 1024;
|
|
9
|
+
/** 与 Edge frozen-artifacts JSON body gate 一致。 */
|
|
10
|
+
export const PROJECT_AUTH_MAX_HTTP_REQUEST_BYTES = 96 * 1024 * 1024;
|
|
11
|
+
/** body_json 作为 pipe 外层 JSON 字符串传输时的统一 envelope gate。 */
|
|
12
|
+
export const PROJECT_AUTH_MAX_PIPE_REQUEST_BYTES = 128 * 1024 * 1024;
|
|
13
|
+
function capabilityFromEnvironment(environment) {
|
|
14
|
+
const values = [environment.WORLDENGINE_PROJECT_TRANSFER_PIPE, environment.WORLDENGINE_PROJECT_TRANSFER_CHALLENGE_ID, environment.WORLDENGINE_PROJECT_TRANSFER_CHALLENGE, environment.WORLDENGINE_PROJECT_TRANSFER_EXPIRES_AT_MS];
|
|
15
|
+
if (values.every((value) => value === undefined))
|
|
16
|
+
return undefined;
|
|
17
|
+
const [pipe_name, challenge_id, challenge, expires] = values;
|
|
18
|
+
const expires_at_ms = Number(expires);
|
|
19
|
+
if (pipe_name === undefined || !PIPE_PATTERN.test(pipe_name) || challenge_id === undefined || !/^[0-9a-f-]{36}$/i.test(challenge_id) || challenge === undefined || challenge.length < 32 || challenge.length > 256 || !Number.isSafeInteger(expires_at_ms) || expires_at_ms <= Date.now()) {
|
|
20
|
+
throw new AuthoringBridgeError("E_AUTH_REQUIRED", "ProjectAuthBroker capability 无效或已过期");
|
|
21
|
+
}
|
|
22
|
+
return { pipe_name, challenge_id, challenge, expires_at_ms };
|
|
23
|
+
}
|
|
24
|
+
async function currentWindowsSid() {
|
|
25
|
+
const { stdout } = await execFileAsync("whoami", ["/user", "/fo", "csv", "/nh"], { windowsHide: true });
|
|
26
|
+
const sid = stdout.split(",").map((value) => value.trim().replace(/^"|"$/g, "")).find((value) => /^S-1-[0-9-]+$/i.test(value));
|
|
27
|
+
if (sid === undefined)
|
|
28
|
+
throw new AuthoringBridgeError("E_AUTH_REQUIRED", "Windows SID 无法验证");
|
|
29
|
+
return sid;
|
|
30
|
+
}
|
|
31
|
+
function base64(bytes) { return Buffer.from(bytes).toString("base64"); }
|
|
32
|
+
/** 只供同模块测试验证有界累计;错误不包含 payload、capability 或 secret。 */
|
|
33
|
+
export function appendProjectAuthResponseChunk(chunks, total, chunk) {
|
|
34
|
+
const next = total + chunk.length;
|
|
35
|
+
if (!Number.isSafeInteger(next) || next > PROJECT_AUTH_MAX_RESPONSE_BYTES) {
|
|
36
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "ProjectAuthBroker 响应过大");
|
|
37
|
+
}
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
export function assertProjectAuthRequestBudget(bodyByteLength, envelopeByteLength) {
|
|
42
|
+
if (!Number.isSafeInteger(bodyByteLength)
|
|
43
|
+
|| bodyByteLength < 0
|
|
44
|
+
|| bodyByteLength > PROJECT_AUTH_MAX_HTTP_REQUEST_BYTES
|
|
45
|
+
|| !Number.isSafeInteger(envelopeByteLength)
|
|
46
|
+
|| envelopeByteLength < 0
|
|
47
|
+
|| envelopeByteLength > PROJECT_AUTH_MAX_PIPE_REQUEST_BYTES) {
|
|
48
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "ProjectAuthBroker 请求过大");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function pipeRequest(pipe, payload) {
|
|
52
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
53
|
+
const socket = createConnection(pipe);
|
|
54
|
+
const chunks = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
const fail = (error) => { socket.destroy(); rejectPromise(error); };
|
|
57
|
+
socket.setTimeout(10_000, () => fail(new AuthoringBridgeError("E_AUTH_REQUIRED", "ProjectAuthBroker 响应超时")));
|
|
58
|
+
socket.once("connect", () => socket.write(`${payload}\n`));
|
|
59
|
+
socket.on("data", (chunk) => {
|
|
60
|
+
try {
|
|
61
|
+
total = appendProjectAuthResponseChunk(chunks, total, chunk);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
fail(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
socket.once("error", () => fail(new AuthoringBridgeError("E_AUTH_REQUIRED", "ProjectAuthBroker 不可用")));
|
|
68
|
+
socket.once("end", () => {
|
|
69
|
+
try {
|
|
70
|
+
resolvePromise(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
rejectPromise(new AuthoringBridgeError("E_VALIDATION_FAILED", "ProjectAuthBroker 响应无效"));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* CLI production composition:只消费 desktop 注入的短期 named-pipe capability。
|
|
80
|
+
* 环境中没有 URL、cookie 或 header authority;SID/project/challenge 仍由 native broker 复验。
|
|
81
|
+
*/
|
|
82
|
+
export async function createProjectAuthTransport(projectId, environment = process.env) {
|
|
83
|
+
if (!projectId || projectId.length > 256)
|
|
84
|
+
throw new AuthoringBridgeError("E_SCHEMA_INVALID", "project_id 无效");
|
|
85
|
+
const capability = capabilityFromEnvironment(environment);
|
|
86
|
+
if (capability === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
await currentWindowsSid();
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
async request(request) {
|
|
91
|
+
if (request.project_id !== projectId || capability.expires_at_ms <= Date.now())
|
|
92
|
+
throw new AuthoringBridgeError("E_AUTH_REQUIRED", "ProjectAuthBroker project/capability 已失效");
|
|
93
|
+
const bytes = request.body instanceof Uint8Array ? request.body : undefined;
|
|
94
|
+
const bodyJson = bytes === undefined && request.body !== undefined ? JSON.stringify(request.body) : undefined;
|
|
95
|
+
const payload = JSON.stringify({
|
|
96
|
+
project_id: projectId,
|
|
97
|
+
challenge_id: capability.challenge_id,
|
|
98
|
+
challenge: capability.challenge,
|
|
99
|
+
method: request.method,
|
|
100
|
+
path: request.path,
|
|
101
|
+
...(request.content_type === undefined ? {} : { content_type: request.content_type }),
|
|
102
|
+
...(request.operation_id === undefined ? {} : { operation_id: request.operation_id }),
|
|
103
|
+
...(bodyJson === undefined ? {} : { body_json: bodyJson }),
|
|
104
|
+
...(bytes === undefined ? {} : { body_base64: base64(bytes) }),
|
|
105
|
+
...(request.body_digest === undefined ? {} : { body_digest: request.body_digest }),
|
|
106
|
+
...(request.body_size === undefined ? {} : { body_size: request.body_size }),
|
|
107
|
+
...(request.raw_body_digest === undefined ? {} : { raw_body_digest: request.raw_body_digest }),
|
|
108
|
+
...(request.raw_body_size === undefined ? {} : { raw_body_size: request.raw_body_size }),
|
|
109
|
+
...(request.body_codec === undefined ? {} : { body_codec: request.body_codec }),
|
|
110
|
+
});
|
|
111
|
+
assertProjectAuthRequestBudget(bytes?.byteLength ?? (bodyJson === undefined ? 0 : Buffer.byteLength(bodyJson, "utf8")), Buffer.byteLength(payload, "utf8") + 1);
|
|
112
|
+
const response = await pipeRequest(capability.pipe_name, payload);
|
|
113
|
+
if (typeof response !== "object" || response === null || Array.isArray(response) || "error" in response)
|
|
114
|
+
throw new AuthoringBridgeError("E_AUTH_REQUIRED", "ProjectAuthBroker 请求被拒绝");
|
|
115
|
+
const value = response;
|
|
116
|
+
if (typeof value.status !== "number" || !Number.isInteger(value.status))
|
|
117
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "ProjectAuthBroker 响应 status 无效");
|
|
118
|
+
return { status: value.status, body: value.body, ...(value.readback === undefined ? {} : { readback: value.readback }) };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** 登录状态只描述 broker 是否有可用的外部会话;cookie 永远不离开 WebView。 */
|
|
2
|
+
export type ProjectAuthState = "signed_out" | "challenge_pending" | "signed_in";
|
|
3
|
+
export interface ProjectAuthPaths {
|
|
4
|
+
readonly project_root: string;
|
|
5
|
+
readonly auth_directory: string;
|
|
6
|
+
readonly webview_data_directory: string;
|
|
7
|
+
}
|
|
8
|
+
/** Pipe 身份必须同时绑定当前 Windows identity、项目和一次性 challenge。 */
|
|
9
|
+
export interface ProjectAuthBrokerIdentity {
|
|
10
|
+
readonly windows_user_id: string;
|
|
11
|
+
readonly project_id: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ProjectAuthChallenge {
|
|
14
|
+
readonly challenge_id: string;
|
|
15
|
+
readonly challenge: string;
|
|
16
|
+
readonly expires_at_ms: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 外部 adapter 是唯一可以与认证服务或 WebView cookie jar 交互的边界。
|
|
20
|
+
* 这些接口特意不包含 token/cookie 字段,避免 CLI 或项目日志取得密钥材料。
|
|
21
|
+
*/
|
|
22
|
+
export interface ProjectAuthAdapter {
|
|
23
|
+
beginChallenge(identity: ProjectAuthBrokerIdentity, webviewDataDirectory: string): Promise<ProjectAuthChallenge>;
|
|
24
|
+
completeChallenge(identity: ProjectAuthBrokerIdentity, challenge: ProjectAuthChallenge, webviewDataDirectory: string): Promise<void>;
|
|
25
|
+
logout(identity: ProjectAuthBrokerIdentity, webviewDataDirectory: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** 将任意项目相对位置安全地解析到给定项目根内。 */
|
|
28
|
+
export declare function resolveProjectAuthPath(projectRoot: string, projectRelativePath: string): string;
|
|
29
|
+
/** auth 与 WebView profile 固定在项目根,绝不退回到用户目录或共享 app data。 */
|
|
30
|
+
export declare function resolveProjectAuthPaths(projectRoot: string): ProjectAuthPaths;
|
|
31
|
+
/**
|
|
32
|
+
* 项目级 broker。它不保存会话凭据:没有可用的外部 adapter 时只能保持 signed_out。
|
|
33
|
+
*/
|
|
34
|
+
export declare class ProjectAuthBroker {
|
|
35
|
+
#private;
|
|
36
|
+
readonly identity: ProjectAuthBrokerIdentity;
|
|
37
|
+
readonly paths: ProjectAuthPaths;
|
|
38
|
+
private constructor();
|
|
39
|
+
static open(projectRoot: string, identity: ProjectAuthBrokerIdentity, adapter?: ProjectAuthAdapter): Promise<ProjectAuthBroker>;
|
|
40
|
+
get state(): ProjectAuthState;
|
|
41
|
+
beginLogin(): Promise<ProjectAuthChallenge>;
|
|
42
|
+
completeLogin(challengeId: string): Promise<void>;
|
|
43
|
+
logout(): Promise<void>;
|
|
44
|
+
/** 可安全记录的诊断;故意不包含 challenge、cookie、token 或 WebView 内容。 */
|
|
45
|
+
diagnostic(): Readonly<{
|
|
46
|
+
project_id: string;
|
|
47
|
+
auth_state: ProjectAuthState;
|
|
48
|
+
auth_directory: string;
|
|
49
|
+
webview_data_directory: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|