@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.
Files changed (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,598 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import { join, resolve, sep } from "node:path";
4
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
5
+ import { computeCanonicalFileTreeDigest, computeCanonicalProjectDigest, decodeScenarioPartWesp, validateLocalAuthorProjectManifest } from "@world-engines/project-format";
6
+ import { assertSafeExistingProjectPath, assertSafeProjectParent, ensureSafeProjectDirectory, safeProjectPath } from "../safe-project-path.js";
7
+ import { withProjectWriterLease } from "../scenario-writer.js";
8
+ const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
9
+ const VIEW_EXCLUDED = new Set(["node_modules", "dist", ".vite"]);
10
+ const PROJECT_GIT_VIEW_EXCLUDED = new Set(["node_modules", "dist", ".vite", ".git", ".worldengine"]);
11
+ /** Windows checkout 与 strict Git source CAS 共用的大小写无关过滤定义。 */
12
+ export function isProjectGitViewExcludedName(name) {
13
+ return PROJECT_GIT_VIEW_EXCLUDED.has(name.toLowerCase());
14
+ }
15
+ function digest(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
16
+ function operationKey(operationId) { return digest(new TextEncoder().encode(operationId)); }
17
+ function wantsScenario(selection) { return selection === "scenario" || selection === "both"; }
18
+ function wantsView(selection) { return selection === "view" || selection === "both"; }
19
+ function remoteError(status) {
20
+ if (status === 401 || status === 403)
21
+ throw new AuthoringBridgeError("E_AUTH_REQUIRED", "远端 Download 请求未获授权");
22
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", `远端 Download 请求失败:HTTP ${status}`);
23
+ }
24
+ function object(value, message) {
25
+ if (typeof value !== "object" || value === null || Array.isArray(value))
26
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", message);
27
+ return value;
28
+ }
29
+ function string(value, message) {
30
+ if (typeof value !== "string" || value === "")
31
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", message);
32
+ return value;
33
+ }
34
+ function number(value, message) {
35
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
36
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", message);
37
+ return value;
38
+ }
39
+ function remoteObject(value, message) {
40
+ const input = object(value, message);
41
+ const byteLength = input.byteLength ?? input.byte_length;
42
+ const mediaType = input.mediaType ?? input.media_type;
43
+ return { key: string(input.key, message), sha256: string(input.sha256, message), byteLength: number(byteLength, message), mediaType: string(mediaType, message) };
44
+ }
45
+ export function parseRemoteDownloadPlan(value, now = Date.now()) {
46
+ const input = object(value, "远端 download plan 无效");
47
+ const frozen = object(input.frozenRemote, "远端 download plan 缺少 frozen remote");
48
+ if (!Array.isArray(input.parts) || input.parts.length < 1 || input.parts.length > 2 || new Set(input.parts).size !== input.parts.length || input.parts.some((part) => part !== "scenario" && part !== "view")) {
49
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "远端 download plan parts 无效");
50
+ }
51
+ const plan = {
52
+ planId: string(input.planId, "远端 download plan 缺少 planId"),
53
+ planDigest: string(input.planDigest, "远端 download plan 缺少 planDigest"),
54
+ expiresAtMs: number(input.expiresAtMs, "远端 download plan 缺少 expiresAtMs"),
55
+ frozenRemote: {
56
+ scenarioId: string(frozen.scenarioId, "frozen remote scenario 无效"), scenarioHead: string(frozen.scenarioHead, "frozen remote scenario head 无效"),
57
+ viewId: string(frozen.viewId, "frozen remote view 无效"), viewDigest: string(frozen.viewDigest, "frozen remote view digest 无效"), bindingRevision: number(frozen.bindingRevision, "frozen remote binding revision 无效"),
58
+ },
59
+ parts: input.parts,
60
+ ...(input.scenarioObject === undefined ? {} : { scenarioObject: remoteObject(input.scenarioObject, "scenario staging object 无效") }),
61
+ ...(input.viewObject === undefined ? {} : { viewObject: remoteObject(input.viewObject, "view staging object 无效") }),
62
+ };
63
+ if (!/^[a-f0-9]{64}$/.test(plan.planDigest) || !/^[a-f0-9]{64}$/.test(plan.frozenRemote.scenarioHead) || !/^[a-f0-9]{64}$/.test(plan.frozenRemote.viewDigest))
64
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "远端 download plan digest 无效");
65
+ if (plan.expiresAtMs <= now)
66
+ throw new AuthoringBridgeError("E_CONFIRMATION_REQUIRED", "远端 download plan 已过期;请重新计划");
67
+ if (plan.parts.includes("scenario") !== (plan.scenarioObject !== undefined) || plan.parts.includes("view") !== (plan.viewObject !== undefined))
68
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "远端 download plan objects 与 parts 不一致");
69
+ for (const objectRef of [plan.scenarioObject, plan.viewObject])
70
+ if (objectRef !== undefined && (!/^[a-f0-9]{64}$/.test(objectRef.sha256) || objectRef.byteLength <= 0 || objectRef.byteLength > 8 * 1024 * 1024 || !objectRef.key || objectRef.key.length > 256))
71
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "远端 download object authority 无效");
72
+ return plan;
73
+ }
74
+ export function parseRemoteDownloadOperation(value, expected) {
75
+ const input = object(value, "远端 download operation 无效");
76
+ const operation = { operationId: string(input.operationId, "远端 operation 缺少 operationId"), planId: string(input.planId, "远端 operation 缺少 planId"), planDigest: string(input.planDigest, "远端 operation 缺少 planDigest"), state: string(input.state, "远端 operation 缺少 state") };
77
+ if (!/^[a-f0-9]{64}$/.test(operation.planDigest) || operation.operationId.length > 256 || (expected !== undefined && (operation.planId !== expected.planId || operation.planDigest !== expected.planDigest || (expected.operationId !== undefined && operation.operationId !== expected.operationId))))
78
+ throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "远端 operation 未绑定冻结 plan/identity");
79
+ return operation;
80
+ }
81
+ function selection(parts) { return parts.length === 2 ? "both" : parts[0]; }
82
+ function assertPlan(plan) {
83
+ if (plan.schema_version !== 1 || !plan.operation_id || !/^[a-f0-9]{64}$/.test(plan.plan_digest) || !Number.isSafeInteger(plan.expected_project_revision) || plan.expected_project_revision < 0) {
84
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Download Replace plan 无效");
85
+ }
86
+ if (!["scenario", "view", "both"].includes(plan.selection))
87
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Download Replace selection 无效");
88
+ if (wantsScenario(plan.selection) && plan.scenario === undefined)
89
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "Scenario plan 缺失");
90
+ if (wantsView(plan.selection) && plan.view === undefined)
91
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "View plan 缺失");
92
+ if (plan.strict_source_cas !== undefined && (plan.strict_source_cas.schema_version !== 1
93
+ || !/^[a-f0-9]{64}$/.test(plan.strict_source_cas.expected_source_sha256)
94
+ || !/^[a-f0-9]{64}$/.test(plan.strict_source_cas.expected_view_source_tree_sha256))) {
95
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "strict source CAS 无效");
96
+ }
97
+ }
98
+ function relativeFilePath(path) {
99
+ if (typeof path !== "string" || path.trim() === "" || path.includes("\\") || path.split("/").some((part) => part === "" || part === "." || part === "..")) {
100
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "下载文件路径必须是安全相对路径");
101
+ }
102
+ return path;
103
+ }
104
+ async function writeAtomic(root, path, value) {
105
+ const staging = await ensureSafeProjectDirectory(root, ".worldengine/staging");
106
+ const bytes = new TextEncoder().encode(`${JSON.stringify(value, null, 2)}\n`);
107
+ const temp = join(staging, `download-${digest(bytes)}.tmp`);
108
+ await rm(temp, { force: true });
109
+ const handle = await open(temp, "wx");
110
+ try {
111
+ await handle.writeFile(bytes);
112
+ await handle.sync();
113
+ }
114
+ finally {
115
+ await handle.close();
116
+ }
117
+ await assertSafeProjectParent(root, path);
118
+ await rename(temp, path);
119
+ await assertSafeExistingProjectPath(root, path);
120
+ }
121
+ async function readManifest(root) {
122
+ const path = await safeProjectPath(root, "worldengine.project.json");
123
+ return validateLocalAuthorProjectManifest(JSON.parse(await readFile(await assertSafeExistingProjectPath(root, path), "utf8")));
124
+ }
125
+ async function pathExists(path) { try {
126
+ await lstat(path);
127
+ return true;
128
+ }
129
+ catch (e) {
130
+ if (e.code === "ENOENT")
131
+ return false;
132
+ throw e;
133
+ } }
134
+ async function listFiles(root, directory, prefix = "", excluded = (name) => VIEW_EXCLUDED.has(name)) {
135
+ const canonical = await assertSafeExistingProjectPath(root, directory);
136
+ const entries = await readdir(canonical, { withFileTypes: true });
137
+ const output = [];
138
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
139
+ if (excluded(entry.name))
140
+ continue;
141
+ const absolute = join(canonical, entry.name);
142
+ const info = await lstat(absolute);
143
+ if (info.isSymbolicLink())
144
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", `拒绝符号链接:${absolute}`);
145
+ const childPath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
146
+ if (info.isDirectory())
147
+ output.push(...await listFiles(root, absolute, childPath, excluded));
148
+ else if (info.isFile())
149
+ output.push({ path: childPath, bytes: new Uint8Array(await readFile(await assertSafeExistingProjectPath(root, absolute))) });
150
+ }
151
+ return output;
152
+ }
153
+ async function viewDigests(root) {
154
+ const view = await assertSafeExistingProjectPath(root, join(root, "view"));
155
+ const source = await computeCanonicalFileTreeDigest(await listFiles(root, view));
156
+ const lockPath = join(view, "package-lock.json");
157
+ const lock = await pathExists(lockPath) ? digest(new Uint8Array(await readFile(await assertSafeExistingProjectPath(root, lockPath)))) : digest(new Uint8Array());
158
+ return { source_tree_sha256: source, package_lock_sha256: lock };
159
+ }
160
+ async function strictProjectGitViewSourceDigest(root) {
161
+ const view = await assertSafeExistingProjectPath(root, join(root, "view"));
162
+ return computeCanonicalFileTreeDigest(await listFiles(root, view, "", isProjectGitViewExcludedName));
163
+ }
164
+ function dependencyVersions(bytes) {
165
+ if (bytes === undefined)
166
+ return new Map();
167
+ let value;
168
+ try {
169
+ value = JSON.parse(new TextDecoder().decode(bytes));
170
+ }
171
+ catch {
172
+ return new Map();
173
+ }
174
+ if (typeof value !== "object" || value === null || Array.isArray(value))
175
+ return new Map();
176
+ const source = value;
177
+ const output = new Map();
178
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
179
+ const section = source[field];
180
+ if (typeof section !== "object" || section === null || Array.isArray(section))
181
+ continue;
182
+ for (const [name, version] of Object.entries(section))
183
+ if (typeof version === "string")
184
+ output.set(name, version);
185
+ }
186
+ return output;
187
+ }
188
+ function dependencyDiff(local, incoming) {
189
+ const before = dependencyVersions(local["package.json"]);
190
+ const after = dependencyVersions(incoming["package.json"]);
191
+ const added = [...after.keys()].filter((key) => !before.has(key)).sort();
192
+ const removed = [...before.keys()].filter((key) => !after.has(key)).sort();
193
+ const changed = [...after.keys()].filter((key) => before.has(key) && before.get(key) !== after.get(key)).sort();
194
+ const lockfile_changes = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"].some((path) => {
195
+ const beforeBytes = local[path];
196
+ const afterBytes = incoming[path];
197
+ return (beforeBytes === undefined) !== (afterBytes === undefined) || (beforeBytes !== undefined && afterBytes !== undefined && digest(beforeBytes) !== digest(afterBytes));
198
+ });
199
+ return { added, removed, changed, lockfile_changes };
200
+ }
201
+ async function selectedLocalFiles(root, parts) {
202
+ const output = [];
203
+ for (const part of parts) {
204
+ const directory = join(root, part);
205
+ const files = await listFiles(root, directory);
206
+ output.push(...files.map((file) => ({ path: `${part}/${file.path}`, bytes: file.bytes })));
207
+ }
208
+ return output.sort((left, right) => left.path.localeCompare(right.path));
209
+ }
210
+ function incomingViewFiles(plan) {
211
+ const output = {};
212
+ for (const file of plan.view?.files ?? [])
213
+ output[relativeFilePath(file.path)] = new Uint8Array(Buffer.from(file.content_base64, "base64"));
214
+ return output;
215
+ }
216
+ async function inspectLocalDownload(root, plan, remotePlanId = plan.operation_id) {
217
+ assertPlan(plan);
218
+ const manifest = await readManifest(root);
219
+ const parts = plan.selection === "both" ? ["scenario", "view"] : [plan.selection];
220
+ if (plan.scenario !== undefined) {
221
+ const source = new Uint8Array(Buffer.from(plan.scenario.source_base64, "base64"));
222
+ if (digest(source) !== plan.scenario.sha256)
223
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "Scenario hash 与 frozen plan 不一致");
224
+ await decodeScenarioPartWesp(source);
225
+ }
226
+ const incoming = incomingViewFiles(plan);
227
+ for (const [path, bytes] of Object.entries(incoming)) {
228
+ const source = plan.view.files.find((file) => file.path === path);
229
+ if (digest(bytes) !== source.sha256)
230
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", `View hash 不一致:${path}`);
231
+ }
232
+ const files = await selectedLocalFiles(root, parts);
233
+ const localView = {};
234
+ for (const file of files)
235
+ if (file.path.startsWith("view/"))
236
+ localView[file.path.slice("view/".length)] = file.bytes;
237
+ const current_snapshot_digest = await computeCanonicalFileTreeDigest(files);
238
+ const scenarioFiles = files.filter((file) => file.path.startsWith("scenario/"));
239
+ const viewFiles = files.filter((file) => file.path.startsWith("view/"));
240
+ const local_overwrite_summary = {
241
+ scenario_file_count: String(scenarioFiles.length),
242
+ scenario_bytes: String(scenarioFiles.reduce((total, file) => total + file.bytes.length, 0)),
243
+ view_file_count: String(viewFiles.length),
244
+ view_bytes: String(viewFiles.reduce((total, file) => total + file.bytes.length, 0)),
245
+ current_snapshot_digest,
246
+ };
247
+ const object_digests = {};
248
+ if (plan.scenario !== undefined)
249
+ object_digests.scenario = plan.scenario.sha256;
250
+ if (plan.view !== undefined)
251
+ object_digests.view = digest(new TextEncoder().encode(JSON.stringify(plan.view.files.map((file) => [file.path, file.sha256]))));
252
+ const facts = { project_id: manifest.project_id, remote_plan_id: remotePlanId, remote_plan_digest: plan.plan_digest, expected_project_revision: manifest.project_revision, parts, object_digests, local_overwrite_summary, dependency_diff: dependencyDiff(localView, incoming) };
253
+ return { schema_version: 1, ...facts, local_inspection_digest: digest(new TextEncoder().encode(JSON.stringify(facts))) };
254
+ }
255
+ export class DownloadReplaceCoordinator {
256
+ root;
257
+ #dependencies;
258
+ constructor(root, dependencies) { this.root = root; this.#dependencies = dependencies; }
259
+ static async open(root, dependencies = {}) {
260
+ return new DownloadReplaceCoordinator(await assertSafeExistingProjectPath(root, root), dependencies);
261
+ }
262
+ /** 深模块 seam:调用方只提交冻结内容,所有本地树、依赖与 digest 由 host 自己读取。 */
263
+ async inspect(plan, remotePlanId = plan.operation_id) {
264
+ return inspectLocalDownload(this.root, plan, remotePlanId);
265
+ }
266
+ async prepare(plan) {
267
+ assertPlan(plan);
268
+ const key = operationKey(plan.operation_id);
269
+ const stage = await ensureSafeProjectDirectory(this.root, `.worldengine/download-staging/${key}`);
270
+ const incoming = join(stage, "incoming");
271
+ await ensureSafeProjectDirectory(this.root, ".worldengine/download-operations");
272
+ const ledgerPath = await safeProjectPath(this.root, `.worldengine/download-operations/${key}.json`);
273
+ let existing;
274
+ if (await pathExists(ledgerPath)) {
275
+ const ledger = JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, ledgerPath), "utf8"));
276
+ if (ledger.plan.plan_digest !== plan.plan_digest)
277
+ throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "operation_id 已绑定另一份 Download plan");
278
+ if (ledger.state === "completed")
279
+ return;
280
+ if (ledger.state === "prepared" && await pathExists(incoming))
281
+ return;
282
+ // restore/crash 后 incoming 已被 swap 消费。必须从同一冻结 plan 重建 staging,
283
+ // 不能创建新 operation,也不能把旧 preimage 当作新输入。
284
+ existing = ledger;
285
+ }
286
+ await rm(incoming, { recursive: true, force: true });
287
+ await mkdir(incoming, { recursive: true });
288
+ if (wantsScenario(plan.selection)) {
289
+ const bytes = new Uint8Array(Buffer.from(plan.scenario.source_base64, "base64"));
290
+ if (digest(bytes) !== plan.scenario.sha256)
291
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "Scenario hash 与 frozen plan 不一致");
292
+ await decodeScenarioPartWesp(bytes);
293
+ await mkdir(join(incoming, "scenario"));
294
+ await mkdir(join(incoming, "scenario", "assets"));
295
+ await writeFile(join(incoming, "scenario", "source.wes"), bytes, { flag: "wx" });
296
+ }
297
+ if (wantsView(plan.selection)) {
298
+ const view = join(incoming, "view");
299
+ await mkdir(view);
300
+ const seen = new Set();
301
+ for (const file of plan.view.files) {
302
+ const path = relativeFilePath(file.path);
303
+ if (seen.has(path))
304
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "View plan 包含重复路径");
305
+ seen.add(path);
306
+ const bytes = new Uint8Array(Buffer.from(file.content_base64, "base64"));
307
+ if (digest(bytes) !== file.sha256)
308
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", `View hash 不一致:${path}`);
309
+ const target = resolve(view, ...path.split("/"));
310
+ if (!target.startsWith(`${view}${sep}`))
311
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "View staging 路径越界");
312
+ await mkdir(resolve(target, ".."), { recursive: true });
313
+ await writeFile(target, bytes, { flag: "wx" });
314
+ }
315
+ await listFiles(this.root, view); // tree/reparse 验证
316
+ if (plan.view.install_dependencies) {
317
+ if (this.#dependencies.installViewDependencies === undefined)
318
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "未注入 staging dependency installer");
319
+ await this.#dependencies.installViewDependencies(view);
320
+ }
321
+ }
322
+ await ensureSafeProjectDirectory(this.root, ".worldengine/download-history");
323
+ const history = await safeProjectPath(this.root, `.worldengine/download-history/${key}`);
324
+ await writeAtomic(this.root, ledgerPath, existing === undefined
325
+ ? { schema_version: 1, plan, state: "prepared", history_path: history }
326
+ : { ...existing, state: "prepared", receipt: undefined });
327
+ }
328
+ async apply(plan, confirmation, inspection) {
329
+ if (confirmation?.operation_id !== plan.operation_id || confirmation.plan_digest !== plan.plan_digest)
330
+ throw new AuthoringBridgeError("E_CONFIRMATION_REQUIRED", "Download Replace 必须确认冻结 plan 后才写入本地项目");
331
+ const key = operationKey(plan.operation_id);
332
+ await ensureSafeProjectDirectory(this.root, ".worldengine/download-operations");
333
+ const ledgerPath = await safeProjectPath(this.root, `.worldengine/download-operations/${key}.json`);
334
+ for (;;) {
335
+ const replay = await withProjectWriterLease(this.root, async () => {
336
+ const ledger = await this.#readLedger(ledgerPath);
337
+ if (ledger === undefined)
338
+ return undefined;
339
+ if (ledger.plan.plan_digest !== plan.plan_digest)
340
+ throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "operation ledger 不匹配");
341
+ if (ledger.state === "completed")
342
+ return ledger.receipt;
343
+ if (ledger.state !== "prepared")
344
+ await this.#restoreUnderLease(plan.operation_id);
345
+ return undefined;
346
+ });
347
+ if (replay !== undefined)
348
+ return replay;
349
+ // staging/npm ci 可能很慢,且不触碰已发布 project parts,绝不能占有 writer lease。
350
+ await this.prepare(plan);
351
+ const receipt = await withProjectWriterLease(this.root, async () => {
352
+ const ledger = await this.#readLedger(ledgerPath);
353
+ if (ledger === undefined)
354
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "download operation ledger 丢失");
355
+ if (ledger.plan.plan_digest !== plan.plan_digest)
356
+ throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "operation ledger 不匹配");
357
+ if (ledger.state === "completed")
358
+ return ledger.receipt;
359
+ if (ledger.state !== "prepared") {
360
+ await this.#restoreUnderLease(plan.operation_id);
361
+ return undefined;
362
+ }
363
+ return this.#applyPreparedUnderLease(plan, key, ledgerPath, ledger, inspection);
364
+ });
365
+ if (receipt !== undefined)
366
+ return receipt;
367
+ }
368
+ }
369
+ async #readLedger(ledgerPath) {
370
+ if (!await pathExists(ledgerPath))
371
+ return undefined;
372
+ return JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, ledgerPath), "utf8"));
373
+ }
374
+ async #applyPreparedUnderLease(plan, key, ledgerPath, initialLedger, confirmedInspection) {
375
+ const manifest = await readManifest(this.root);
376
+ if (manifest.project_revision < plan.expected_project_revision)
377
+ throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", `期望 revision 至少 ${plan.expected_project_revision},当前 ${manifest.project_revision}`);
378
+ if (plan.strict_source_cas !== undefined) {
379
+ if (manifest.project_revision !== plan.expected_project_revision)
380
+ throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", `strict source CAS 期望 revision ${plan.expected_project_revision},当前 ${manifest.project_revision}`);
381
+ const currentSource = digest(new Uint8Array(await readFile(await assertSafeExistingProjectPath(this.root, join(this.root, "scenario", "source.wes")))));
382
+ const currentViewSourceDigest = await strictProjectGitViewSourceDigest(this.root);
383
+ if (currentSource !== plan.strict_source_cas.expected_source_sha256 || currentViewSourceDigest !== plan.strict_source_cas.expected_view_source_tree_sha256) {
384
+ throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", "strict source CAS 检测到本地 source 或视图树已改变");
385
+ }
386
+ }
387
+ const currentInspection = await this.inspect(plan, confirmedInspection?.remote_plan_id ?? plan.operation_id);
388
+ if (confirmedInspection !== undefined) {
389
+ if (confirmedInspection.schema_version !== 1 || confirmedInspection.project_id !== manifest.project_id || confirmedInspection.remote_plan_digest !== plan.plan_digest || confirmedInspection.expected_project_revision !== plan.expected_project_revision || JSON.stringify(confirmedInspection.parts) !== JSON.stringify(currentInspection.parts)) {
390
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "download inspection 未绑定当前项目/冻结 plan");
391
+ }
392
+ }
393
+ const inspection_changed_since_confirmation = confirmedInspection !== undefined && confirmedInspection.local_inspection_digest !== currentInspection.local_inspection_digest;
394
+ const history = await ensureSafeProjectDirectory(this.root, `.worldengine/download-history/${key}`);
395
+ await writeAtomic(this.root, join(history, "manifest.json"), manifest);
396
+ await writeAtomic(this.root, join(history, "metadata.json"), { schema_version: 1, operation_id: plan.operation_id, created_at_ms: (this.#dependencies.now ?? Date.now)(), pinned: false, inspection_changed_since_confirmation, local_inspection_digest: currentInspection.local_inspection_digest, local_overwrite_summary: currentInspection.local_overwrite_summary });
397
+ let ledger = { ...initialLedger, state: "preimage_saved" };
398
+ await writeAtomic(this.root, ledgerPath, ledger);
399
+ const incoming = join(this.root, ".worldengine", "download-staging", key, "incoming");
400
+ try {
401
+ if (wantsScenario(plan.selection)) {
402
+ await rename(join(this.root, "scenario"), join(history, "scenario"));
403
+ await rename(join(incoming, "scenario"), join(this.root, "scenario"));
404
+ await this.#dependencies.fault?.("after_scenario_rename");
405
+ }
406
+ if (wantsView(plan.selection)) {
407
+ await rename(join(this.root, "view"), join(history, "view"));
408
+ await rename(join(incoming, "view"), join(this.root, "view"));
409
+ await this.#dependencies.fault?.("after_view_rename");
410
+ }
411
+ ledger = { ...ledger, state: "swapped" };
412
+ await writeAtomic(this.root, ledgerPath, ledger);
413
+ const scenario = wantsScenario(plan.selection) ? digest(new Uint8Array(await readFile(join(this.root, "scenario", "source.wes")))) : manifest.scenario.source_sha256;
414
+ await decodeScenarioPartWesp(new Uint8Array(await readFile(join(this.root, "scenario", "source.wes"))));
415
+ const view = await viewDigests(this.root);
416
+ const next = { ...manifest, project_revision: manifest.project_revision + 1, updated_at_ms: (this.#dependencies.now ?? Date.now)(), scenario: { ...manifest.scenario, source_sha256: scenario }, view: { ...manifest.view, ...view }, snapshot: { ...manifest.snapshot, previous_snapshot_id: manifest.snapshot.snapshot_id, snapshot_id: "snapshot_pending", canonical_digest: "0".repeat(64) } };
417
+ next.snapshot.canonical_digest = await computeCanonicalProjectDigest(next);
418
+ next.snapshot.snapshot_id = `snapshot_${next.snapshot.canonical_digest.slice(0, 24)}`;
419
+ await writeAtomic(this.root, join(this.root, "worldengine.project.json"), next);
420
+ const readback = await readManifest(this.root);
421
+ if (readback.project_revision !== next.project_revision || readback.snapshot.canonical_digest !== next.snapshot.canonical_digest)
422
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "manifest readback 不一致");
423
+ const receipt = { operation_id: plan.operation_id, previous_project_revision: manifest.project_revision, next_project_revision: next.project_revision, selection: plan.selection, readback_verified: true, restored: false, scenario_sha256: scenario, view_source_tree_sha256: view.source_tree_sha256, project_snapshot_digest: next.snapshot.canonical_digest, inspection_changed_since_confirmation, local_inspection_digest: currentInspection.local_inspection_digest, local_overwrite_summary: currentInspection.local_overwrite_summary };
424
+ await this.#dependencies.readProjection?.(receipt);
425
+ ledger = { ...ledger, state: "completed", receipt };
426
+ await writeAtomic(this.root, ledgerPath, ledger);
427
+ return receipt;
428
+ }
429
+ catch (error) {
430
+ await this.#restoreUnderLease(plan.operation_id);
431
+ throw error;
432
+ }
433
+ }
434
+ async restore(operationId) {
435
+ return withProjectWriterLease(this.root, () => this.#restoreUnderLease(operationId));
436
+ }
437
+ async #restoreUnderLease(operationId) {
438
+ const key = operationKey(operationId);
439
+ const ledgerPath = await safeProjectPath(this.root, `.worldengine/download-operations/${key}.json`);
440
+ const ledger = JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, ledgerPath), "utf8"));
441
+ const history = await assertSafeExistingProjectPath(this.root, ledger.history_path);
442
+ const staging = await ensureSafeProjectDirectory(this.root, `.worldengine/download-staging/${key}/replaced`);
443
+ for (const part of ["scenario", "view"]) {
444
+ if (!(part === "scenario" ? wantsScenario(ledger.plan.selection) : wantsView(ledger.plan.selection)))
445
+ continue;
446
+ const original = join(history, part);
447
+ const target = join(this.root, part);
448
+ if (await pathExists(original)) {
449
+ if (await pathExists(target)) {
450
+ const displaced = join(staging, part);
451
+ await rm(displaced, { recursive: true, force: true });
452
+ await rename(target, displaced);
453
+ }
454
+ await rename(original, target);
455
+ }
456
+ }
457
+ const preimage = JSON.parse(await readFile(join(history, "manifest.json"), "utf8"));
458
+ await writeAtomic(this.root, join(this.root, "worldengine.project.json"), preimage);
459
+ await writeAtomic(this.root, ledgerPath, { ...ledger, state: "restored" });
460
+ }
461
+ async setHistoryPinned(operationId, pinned) { const key = operationKey(operationId); const metadata = await safeProjectPath(this.root, `.worldengine/download-history/${key}/metadata.json`); const value = JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, metadata), "utf8")); await writeAtomic(this.root, metadata, { ...value, pinned }); }
462
+ async pruneHistory(now = (this.#dependencies.now ?? Date.now)()) { const root = await ensureSafeProjectDirectory(this.root, ".worldengine/download-history"); for (const entry of await readdir(root, { withFileTypes: true })) {
463
+ if (!entry.isDirectory())
464
+ continue;
465
+ const metadata = join(root, entry.name, "metadata.json");
466
+ try {
467
+ const value = JSON.parse(await readFile(metadata, "utf8"));
468
+ if (!value.pinned && typeof value.created_at_ms === "number" && now - value.created_at_ms > RETENTION_MS)
469
+ await rm(join(root, entry.name), { recursive: true, force: true });
470
+ }
471
+ catch (error) {
472
+ if (error.code !== "ENOENT")
473
+ throw error;
474
+ }
475
+ } }
476
+ }
477
+ /**
478
+ * 将稳定远端路由收敛成可重放的单 target 下载:先由远端冻结对象,再在本地验证
479
+ * staging 内容;本地失败不会新建 operation,而是让同一 operation 走 reconcile。
480
+ */
481
+ export class RemoteDownloadCoordinator {
482
+ #projectId;
483
+ #transport;
484
+ #local;
485
+ constructor(projectId, transport, local) {
486
+ this.#projectId = projectId;
487
+ this.#transport = transport;
488
+ this.#local = local;
489
+ }
490
+ static async open(root, projectId, transport, dependencies = {}) {
491
+ if (!projectId)
492
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "download 缺少 project_id");
493
+ return new RemoteDownloadCoordinator(projectId, transport, await DownloadReplaceCoordinator.open(root, dependencies));
494
+ }
495
+ #transportOrThrow() {
496
+ if (this.#transport === undefined)
497
+ throw new AuthoringBridgeError("E_AUTH_REQUIRED", "CLI 没有 ProjectAuthBroker 受控 transport");
498
+ return this.#transport;
499
+ }
500
+ async plan(request) {
501
+ if (request.project_id !== this.#projectId || !request.target_pair_id || request.parts.length < 1 || request.parts.length > 2 || new Set(request.parts).size !== request.parts.length) {
502
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "download plan 必须是当前项目的单 target 与明确 parts");
503
+ }
504
+ const body = { projectId: request.project_id, targetPairId: request.target_pair_id, parts: [...request.parts] };
505
+ const response = await this.#transportOrThrow().request({ project_id: this.#projectId, method: "POST", path: "/v1/creator/project-transfer/download/plan", body, body_digest: digest(new TextEncoder().encode(JSON.stringify(body))) });
506
+ if (response.status < 200 || response.status >= 300)
507
+ remoteError(response.status);
508
+ return parseRemoteDownloadPlan(response.body);
509
+ }
510
+ async confirm(plan, confirmPlanDigest) {
511
+ if (confirmPlanDigest !== plan.planDigest)
512
+ throw new AuthoringBridgeError("E_CONFIRMATION_REQUIRED", "--confirm-plan 与远端冻结 plan digest 不匹配");
513
+ if (plan.expiresAtMs <= Date.now())
514
+ throw new AuthoringBridgeError("E_CONFIRMATION_REQUIRED", "远端 download plan 已过期;请重新计划");
515
+ const body = { planId: plan.planId, planDigest: plan.planDigest, confirmation: "force_overwrite" };
516
+ const response = await this.#transportOrThrow().request({ project_id: this.#projectId, method: "POST", path: "/v1/creator/project-transfer/download/confirm", body, body_digest: digest(new TextEncoder().encode(JSON.stringify(body))) });
517
+ if (response.status < 200 || response.status >= 300)
518
+ remoteError(response.status);
519
+ return parseRemoteDownloadOperation(response.body, { planId: plan.planId, planDigest: plan.planDigest });
520
+ }
521
+ async operation(operationId, reconcile = false) {
522
+ if (!operationId)
523
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "download operation_id 不能为空");
524
+ const response = await this.#transportOrThrow().request({ project_id: this.#projectId, method: reconcile ? "POST" : "GET", path: `/v1/creator/project-transfer/download/operations/${encodeURIComponent(operationId)}${reconcile ? "/reconcile" : ""}` });
525
+ if (response.status < 200 || response.status >= 300)
526
+ remoteError(response.status);
527
+ return parseRemoteDownloadOperation(response.body, { operationId, planId: string(object(response.body, "远端 operation 无效").planId, "operation planId 无效"), planDigest: string(object(response.body, "远端 operation 无效").planDigest, "operation planDigest 无效") });
528
+ }
529
+ async apply(plan, operation) {
530
+ if (operation.planId !== plan.planId || operation.planDigest !== plan.planDigest)
531
+ throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "同一 download operation 不能绑定另一份 plan");
532
+ const source = plan.scenarioObject === undefined ? undefined : await this.#fetchObject(plan.scenarioObject, "application/vnd.worldengine.wesp");
533
+ const view = plan.viewObject === undefined ? undefined : await this.#fetchObject(plan.viewObject, "application/vnd.worldengine.svp");
534
+ const localPlan = {
535
+ schema_version: 1,
536
+ operation_id: operation.operationId,
537
+ plan_digest: plan.planDigest,
538
+ expected_project_revision: (await readManifest(this.#local.root)).project_revision,
539
+ selection: selection(plan.parts),
540
+ ...(source === undefined ? {} : { scenario: { source_base64: Buffer.from(source).toString("base64"), sha256: digest(source) } }),
541
+ ...(view === undefined ? {} : { view: decodeRemoteViewBundle(view) }),
542
+ };
543
+ try {
544
+ return await this.#local.apply(localPlan, { operation_id: operation.operationId, plan_digest: plan.planDigest });
545
+ }
546
+ catch (error) {
547
+ // 不确定的本地异常只能读取同一 remote identity;不发送新的 confirm。
548
+ await this.operation(operation.operationId, true).catch(() => undefined);
549
+ throw error;
550
+ }
551
+ }
552
+ async restore(operationId) { await this.#local.restore(operationId); }
553
+ async #fetchObject(objectRef, expectedMediaType) {
554
+ if (objectRef.mediaType !== expectedMediaType)
555
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", `远端 staging object media type 无效:${objectRef.mediaType}`);
556
+ const response = await this.#transportOrThrow().request({ project_id: this.#projectId, method: "GET", path: `/v1/creator/project-transfer/download/staging/${encodeURIComponent(objectRef.key)}` });
557
+ if (response.status < 200 || response.status >= 300)
558
+ remoteError(response.status);
559
+ const encoded = object(response.body, "远端 staging object 响应无效").contentBase64;
560
+ const bytes = new Uint8Array(Buffer.from(string(encoded, "远端 staging object 缺少 contentBase64"), "base64"));
561
+ if (bytes.length !== objectRef.byteLength || digest(bytes) !== objectRef.sha256 || (response.readback !== undefined && (response.readback.byte_length !== bytes.length || response.readback.sha256 !== objectRef.sha256))) {
562
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "远端 staging object readback 不匹配");
563
+ }
564
+ return bytes;
565
+ }
566
+ }
567
+ export function decodeRemoteViewBundle(bytes) {
568
+ let value;
569
+ // SVP v1: magic/version/flags + updated_at_ms + payload length。当前 ProjectHost
570
+ // 只接受 upload snapshot 生成的未压缩 flags=0 形式;旧 CLI JSON 仅为兼容已冻结 operation。
571
+ let payload = bytes;
572
+ if (bytes.length >= 17 && bytes[0] === 0x53 && bytes[1] === 0x56 && bytes[2] === 0x50 && bytes[3] === 0x01) {
573
+ if (bytes[4] !== 0)
574
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "远端 SVP 压缩形式不受 ProjectHost 支持");
575
+ const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(13, true);
576
+ if (bytes.length !== 17 + length)
577
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "远端 SVP payload 长度无效");
578
+ payload = bytes.subarray(17);
579
+ }
580
+ try {
581
+ value = JSON.parse(new TextDecoder().decode(payload));
582
+ }
583
+ catch {
584
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "远端 View staging bundle 不是有效 SVP JSON");
585
+ }
586
+ const input = object(value, "远端 View staging bundle 无效");
587
+ if (!Array.isArray(input.files) || (input.installDependencies !== undefined && typeof input.installDependencies !== "boolean"))
588
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "远端 View staging bundle 缺少 files/installDependencies");
589
+ return {
590
+ install_dependencies: input.installDependencies === true,
591
+ files: input.files.map((entry) => {
592
+ const file = object(entry, "远端 View 文件无效");
593
+ const contentBase64 = file.contentBase64 ?? file.content_base64;
594
+ const encoded = string(contentBase64, "远端 View 文件 contentBase64 无效");
595
+ return { path: string(file.path, "远端 View 文件 path 无效"), content_base64: encoded, sha256: file.sha256 === undefined ? digest(new Uint8Array(Buffer.from(encoded, "base64"))) : string(file.sha256, "远端 View 文件 sha256 无效") };
596
+ }),
597
+ };
598
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ProjectAuthBroker 注入的无状态受控 HTTP 出口。它故意不暴露 cookie、token 或
3
+ * WebView profile;调用者只提供 project identity、经过摘要的 body 与路径。
4
+ */
5
+ export interface ProjectTransferTransport {
6
+ request(request: Readonly<{
7
+ project_id: string;
8
+ method: "GET" | "POST" | "PUT" | "HEAD";
9
+ path: string;
10
+ content_type?: "application/x-ndjson; profile=\"chat-artifact-v5-submit-stream-v1\"" | "application/x-git-upload-pack-request" | "application/x-git-receive-pack-request";
11
+ operation_id?: string;
12
+ body_digest?: string;
13
+ body_size?: number;
14
+ raw_body_digest?: string;
15
+ raw_body_size?: number;
16
+ body_codec?: "identity-v1" | "gzip-member-v1";
17
+ body?: unknown | Uint8Array;
18
+ }>): Promise<Readonly<{
19
+ status: number;
20
+ body: unknown;
21
+ readback?: Readonly<{
22
+ sha256: string;
23
+ byte_length: number;
24
+ }>;
25
+ }>>;
26
+ }
@@ -0,0 +1 @@
1
+ export {};