@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,551 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { canonicalNeutralScenarioSourceBytes, canonicalScenarioSourceManifestDigestInputBytes, canonicalScenarioSourceSectionBytes, decodeNeutralScenarioSource, } from "@world-engines/scenario-author-source";
5
+ import { TMW_RUNTIME_GENERATION } from "@world-engines/tmw";
6
+ import { NeutralScenarioSource, ScenarioSourceManifest, ScenarioSourceSection, } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
7
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
8
+ import { decodeScenarioPartWesp } from "@world-engines/project-format";
9
+ import { readFrozenReviewTargetBinding, } from "./frozen-review-local-pipeline.js";
10
+ import { createFrozenReviewWasmEncryptor, } from "./frozen-review-wasm-encryptor.js";
11
+ import { assertSafeExistingProjectPath } from "./safe-project-path.js";
12
+ const SHA256 = /^[0-9a-f]{64}$/u;
13
+ const DATABASE_KEY_PATH = "modules/database/key";
14
+ const LOCAL_WESP_SECTION = "local_project_wesp";
15
+ const LOCAL_WESP_MEDIA_TYPE = "application/vnd.worldengine.scenario-part+wesp";
16
+ const LOCAL_WESP_ENCODING = "wesp-v1";
17
+ const MAX_RAW_CHUNK_BYTES = 4 * 1024 * 1024;
18
+ const MAX_CIPHER_CHUNK_BYTES = 8 * 1024 * 1024;
19
+ const MAX_OBJECT_BYTES = 512 * 1024 * 1024;
20
+ const MAX_CHUNKS = 128;
21
+ const MAX_OBJECTS = 64;
22
+ function fail(code, message) {
23
+ throw new AuthoringBridgeError(code, message);
24
+ }
25
+ function object(value, field) {
26
+ if (typeof value !== "object" || value === null || Array.isArray(value))
27
+ fail("E_VALIDATION_FAILED", `${field} 无效`);
28
+ return value;
29
+ }
30
+ function text(value, field) {
31
+ if (typeof value !== "string" || value.length === 0 || value !== value.trim().normalize("NFC")) {
32
+ fail("E_VALIDATION_FAILED", `${field} 无效`);
33
+ }
34
+ return value;
35
+ }
36
+ function sha(value, field) {
37
+ const result = text(value, field);
38
+ if (!SHA256.test(result))
39
+ fail("E_VALIDATION_FAILED", `${field} digest 无效`);
40
+ return result;
41
+ }
42
+ function positive(value, field) {
43
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
44
+ fail("E_VALIDATION_FAILED", `${field} 无效`);
45
+ return value;
46
+ }
47
+ function nonnegative(value, field) {
48
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
49
+ fail("E_VALIDATION_FAILED", `${field} 无效`);
50
+ return value;
51
+ }
52
+ function nullableSha(value, field) {
53
+ return value === null ? null : sha(value, field);
54
+ }
55
+ function digest(bytes) {
56
+ return createHash("sha256").update(bytes).digest("hex");
57
+ }
58
+ function jsonDigest(value) {
59
+ return digest(new TextEncoder().encode(JSON.stringify(value)));
60
+ }
61
+ function base64(value, field) {
62
+ const encoded = text(value, field);
63
+ if (encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(encoded)
64
+ || encoded.slice(0, -2).includes("=")) {
65
+ fail("E_VALIDATION_FAILED", `${field} base64 无效`);
66
+ }
67
+ const decoded = new Uint8Array(Buffer.from(encoded, "base64"));
68
+ if (Buffer.from(decoded).toString("base64") !== encoded)
69
+ fail("E_VALIDATION_FAILED", `${field} base64 非 canonical`);
70
+ return decoded;
71
+ }
72
+ function parseHead(value, field) {
73
+ const row = object(value, field);
74
+ return Object.freeze({
75
+ owner_id: text(row.owner_id, `${field} owner`),
76
+ scenario_id: text(row.scenario_id, `${field} scenario`),
77
+ source_revision: positive(row.source_revision, `${field} revision`),
78
+ manifest_digest: sha(row.manifest_digest, `${field} manifest`),
79
+ source_digest: sha(row.source_digest, `${field} source`),
80
+ revision_digest: sha(row.revision_digest, `${field} revision`),
81
+ previous_head_digest: nullableSha(row.previous_head_digest, `${field} previous head`),
82
+ head_digest: sha(row.head_digest, `${field} head`),
83
+ });
84
+ }
85
+ function parseRevision(value) {
86
+ const row = object(value, "author-source revision");
87
+ return Object.freeze({
88
+ ...parseHead(row, "author-source revision"),
89
+ revision_size_bytes: positive(row.revision_size_bytes, "revision plaintext size"),
90
+ revision_ciphertext_sha256: sha(row.revision_ciphertext_sha256, "revision ciphertext"),
91
+ revision_ciphertext_size_bytes: positive(row.revision_ciphertext_size_bytes, "revision ciphertext size"),
92
+ });
93
+ }
94
+ function parseWitness(value) {
95
+ const row = object(value, "author-source object witness");
96
+ if (row.object_kind !== "manifest" && row.object_kind !== "revision" && row.object_kind !== "section") {
97
+ fail("E_VALIDATION_FAILED", "author-source object kind 无效");
98
+ }
99
+ const sectionKey = row.section_key === null ? null : text(row.section_key, "section key");
100
+ const mediaType = row.media_type === null ? null : text(row.media_type, "section media type");
101
+ const encoding = row.canonical_encoding === null ? null : text(row.canonical_encoding, "section encoding");
102
+ if ((row.object_kind === "section") !== (sectionKey !== null && mediaType !== null && encoding !== null)) {
103
+ fail("E_VALIDATION_FAILED", "author-source section witness 无效");
104
+ }
105
+ return Object.freeze({
106
+ object_kind: row.object_kind,
107
+ object_key: text(row.object_key, "object key"),
108
+ section_key: sectionKey,
109
+ media_type: mediaType,
110
+ canonical_encoding: encoding,
111
+ plaintext_size_bytes: positive(row.plaintext_size_bytes, "object plaintext size"),
112
+ plaintext_sha256: sha(row.plaintext_sha256, "object plaintext"),
113
+ ciphertext_size_bytes: positive(row.ciphertext_size_bytes, "object ciphertext size"),
114
+ ciphertext_sha256: sha(row.ciphertext_sha256, "object ciphertext"),
115
+ });
116
+ }
117
+ function parseReceipt(value) {
118
+ const row = object(value, "author-source receipt");
119
+ if (!Array.isArray(row.objects))
120
+ fail("E_VALIDATION_FAILED", "author-source receipt objects 无效");
121
+ return Object.freeze({
122
+ operation_id: text(row.operation_id, "receipt operation"),
123
+ request_sha256: sha(row.request_sha256, "receipt request"),
124
+ expected_head_digest: nullableSha(row.expected_head_digest, "receipt expected head"),
125
+ previous_head_digest: nullableSha(row.previous_head_digest, "receipt previous head"),
126
+ next_head_digest: sha(row.next_head_digest, "receipt next head"),
127
+ source_revision: positive(row.source_revision, "receipt source revision"),
128
+ manifest_digest: sha(row.manifest_digest, "receipt manifest"),
129
+ source_digest: sha(row.source_digest, "receipt source"),
130
+ revision_digest: sha(row.revision_digest, "receipt revision"),
131
+ revision_size_bytes: positive(row.revision_size_bytes, "receipt revision size"),
132
+ manifest_size_bytes: positive(row.manifest_size_bytes, "receipt manifest size"),
133
+ objects: Object.freeze(row.objects.map(parseWitness)),
134
+ });
135
+ }
136
+ function parseCipherObjectV2(value, field) {
137
+ const row = object(value, field);
138
+ const length = positive(row.plaintext_byte_length, `${field} plaintext length`);
139
+ if (length > MAX_OBJECT_BYTES || row.cipher_codec !== "scnb-v2-chunkset-v1" || !Array.isArray(row.chunks)
140
+ || row.chunks.length === 0 || row.chunks.length > MAX_CHUNKS) {
141
+ fail("E_VALIDATION_FAILED", `${field} descriptor 超限或 codec 无效`);
142
+ }
143
+ let offset = 0;
144
+ const chunks = row.chunks.map((candidate, index) => {
145
+ const chunk = object(candidate, `${field} chunk ${index}`);
146
+ const plaintextLength = positive(chunk.plaintext_byte_length, `${field} chunk plaintext length`);
147
+ const ciphertextLength = positive(chunk.ciphertext_byte_length, `${field} chunk ciphertext length`);
148
+ if (chunk.index !== index || chunk.plaintext_offset !== offset
149
+ || plaintextLength > MAX_RAW_CHUNK_BYTES || ciphertextLength > MAX_CIPHER_CHUNK_BYTES
150
+ || chunk.cipher_codec !== "scnb-v2-chunk-v1") {
151
+ fail("E_VALIDATION_FAILED", `${field} chunk identity 无效`);
152
+ }
153
+ offset += plaintextLength;
154
+ if (!Number.isSafeInteger(offset) || offset > MAX_OBJECT_BYTES)
155
+ fail("E_VALIDATION_FAILED", `${field} chunk length 溢出`);
156
+ return Object.freeze({
157
+ index,
158
+ plaintext_offset: nonnegative(chunk.plaintext_offset, `${field} chunk offset`),
159
+ plaintext_byte_length: plaintextLength,
160
+ plaintext_sha256: sha(chunk.plaintext_sha256, `${field} chunk plaintext`),
161
+ cipher_codec: "scnb-v2-chunk-v1",
162
+ ciphertext_byte_length: ciphertextLength,
163
+ ciphertext_sha256: sha(chunk.ciphertext_sha256, `${field} chunk ciphertext`),
164
+ object_key: text(chunk.object_key, `${field} chunk object key`),
165
+ });
166
+ });
167
+ if (offset !== length)
168
+ fail("E_VALIDATION_FAILED", `${field} chunk 总长度不匹配`);
169
+ return Object.freeze({
170
+ plaintext_byte_length: length,
171
+ plaintext_sha256: sha(row.plaintext_sha256, `${field} plaintext`),
172
+ cipher_codec: "scnb-v2-chunkset-v1",
173
+ chunks: Object.freeze(chunks),
174
+ });
175
+ }
176
+ function parseRevisionIndexV2(value) {
177
+ const row = object(value, "revision index v2");
178
+ if (row.revision_index_version !== 2 || !Array.isArray(row.sections) || row.sections.length === 0 || row.sections.length + 1 > MAX_OBJECTS) {
179
+ fail("E_VALIDATION_FAILED", "revision index v2 schema 无效");
180
+ }
181
+ const keys = new Set();
182
+ const sections = row.sections.map((candidate, index) => {
183
+ const section = object(candidate, `revision index section ${index}`);
184
+ const sectionKey = text(section.section_key, `revision index section ${index} key`);
185
+ if (keys.has(sectionKey))
186
+ fail("E_VALIDATION_FAILED", "revision index section key 重复");
187
+ keys.add(sectionKey);
188
+ return Object.freeze({
189
+ section_key: sectionKey,
190
+ media_type: text(section.media_type, `revision index section ${index} media type`),
191
+ canonical_encoding: text(section.canonical_encoding, `revision index section ${index} encoding`),
192
+ object: parseCipherObjectV2(section.object, `revision index section ${sectionKey}`),
193
+ });
194
+ });
195
+ return Object.freeze({
196
+ revision_index_version: 2,
197
+ owner_id: text(row.owner_id, "revision index owner"),
198
+ scenario_id: text(row.scenario_id, "revision index scenario"),
199
+ source_revision: positive(row.source_revision, "revision index source revision"),
200
+ manifest_digest: sha(row.manifest_digest, "revision index manifest"),
201
+ source_digest: sha(row.source_digest, "revision index source"),
202
+ source_size_bytes: positive(row.source_size_bytes, "revision index source size"),
203
+ previous_head_digest: nullableSha(row.previous_head_digest, "revision index previous head"),
204
+ manifest: parseCipherObjectV2(row.manifest, "revision index manifest"),
205
+ sections: Object.freeze(sections),
206
+ });
207
+ }
208
+ function parseReceiptV2(value) {
209
+ const row = object(value, "author-source receipt v2");
210
+ if (!Array.isArray(row.objects) || row.objects.length === 0 || row.objects.length > MAX_OBJECTS) {
211
+ fail("E_VALIDATION_FAILED", "author-source receipt v2 objects 无效");
212
+ }
213
+ return Object.freeze({
214
+ operation_id: text(row.operation_id, "receipt operation"),
215
+ request_sha256: sha(row.request_sha256, "receipt request"),
216
+ expected_head_digest: nullableSha(row.expected_head_digest, "receipt expected head"),
217
+ previous_head_digest: nullableSha(row.previous_head_digest, "receipt previous head"),
218
+ next_head_digest: sha(row.next_head_digest, "receipt next head"),
219
+ source_revision: positive(row.source_revision, "receipt source revision"),
220
+ manifest_digest: sha(row.manifest_digest, "receipt manifest"),
221
+ source_digest: sha(row.source_digest, "receipt source"),
222
+ revision_digest: sha(row.revision_digest, "receipt revision"),
223
+ source_size_bytes: positive(row.source_size_bytes, "receipt source size"),
224
+ objects: Object.freeze([...row.objects]),
225
+ });
226
+ }
227
+ function receiptObjectIdV2(receipt, kind, sectionKey) {
228
+ const matches = receipt.objects.map((value) => object(value, "receipt v2 object")).filter((value) => value.object_kind === kind && (kind === "manifest" ? value.section_key === undefined || value.section_key === null : value.section_key === sectionKey));
229
+ if (matches.length !== 1)
230
+ fail("E_VALIDATION_FAILED", `receipt v2 ${kind} object 不唯一`);
231
+ return text(matches[0].object_id, `receipt v2 ${kind} object id`);
232
+ }
233
+ function sameHead(left, right) {
234
+ return left.owner_id === right.owner_id
235
+ && left.scenario_id === right.scenario_id
236
+ && left.source_revision === right.source_revision
237
+ && left.manifest_digest === right.manifest_digest
238
+ && left.source_digest === right.source_digest
239
+ && left.revision_digest === right.revision_digest
240
+ && left.previous_head_digest === right.previous_head_digest
241
+ && left.head_digest === right.head_digest;
242
+ }
243
+ function uniqueWitness(receipt, kind, sectionKey) {
244
+ const matches = receipt.objects.filter((item) => item.object_kind === kind && item.section_key === sectionKey);
245
+ if (matches.length !== 1)
246
+ fail("E_VALIDATION_FAILED", `author-source ${kind} witness 不唯一`);
247
+ return matches[0];
248
+ }
249
+ async function decryptVerified(crypto, key, ciphertext, witness) {
250
+ if (ciphertext.byteLength !== witness.ciphertext_size_bytes || digest(ciphertext) !== witness.ciphertext_sha256) {
251
+ fail("E_OPERATION_ID_CONFLICT", "author-source ciphertext readback 不匹配");
252
+ }
253
+ let plaintext;
254
+ try {
255
+ plaintext = await crypto.decrypt(key, ciphertext);
256
+ }
257
+ catch {
258
+ return fail("E_AUTH_REQUIRED", "author-source scenario key 无法解密 readback");
259
+ }
260
+ if (plaintext.byteLength !== witness.plaintext_size_bytes || digest(plaintext) !== witness.plaintext_sha256) {
261
+ fail("E_OPERATION_ID_CONFLICT", "author-source plaintext readback 不匹配");
262
+ }
263
+ return plaintext;
264
+ }
265
+ function requireSuccess(response, field) {
266
+ if (response.status === 401 || response.status === 403)
267
+ fail("E_AUTH_REQUIRED", `${field} 未获授权`);
268
+ if (response.status < 200 || response.status >= 300)
269
+ fail("E_VALIDATION_FAILED", `${field} 失败:HTTP ${response.status}`);
270
+ return response.body;
271
+ }
272
+ async function readCipherObjectV2(transport, projectId, scenario, revisionDigest, kind, objectId, descriptor, crypto, key) {
273
+ const result = new Uint8Array(descriptor.plaintext_byte_length);
274
+ const objectHash = createHash("sha256");
275
+ for (const chunk of descriptor.chunks) {
276
+ const query = new URLSearchParams({
277
+ revision_digest: revisionDigest,
278
+ ciphertext_sha256: chunk.ciphertext_sha256,
279
+ ciphertext_size_bytes: String(chunk.ciphertext_byte_length),
280
+ });
281
+ const path = `/v1/scenarios/${scenario}/author-source/objects/${kind}/${encodeURIComponent(objectId)}/chunks/${chunk.index}?${query.toString()}`;
282
+ const response = await transport.request({ project_id: projectId, method: "GET", path });
283
+ requireSuccess(response, "author-source chunk readback");
284
+ const wire = object(response.body, "author-source chunk readback");
285
+ const ciphertext = base64(wire.contentBase64, "author-source chunk ciphertext");
286
+ if (ciphertext.byteLength !== chunk.ciphertext_byte_length || digest(ciphertext) !== chunk.ciphertext_sha256
287
+ || response.readback?.byte_length !== chunk.ciphertext_byte_length
288
+ || response.readback.sha256 !== chunk.ciphertext_sha256) {
289
+ fail("E_OPERATION_ID_CONFLICT", "author-source chunk ciphertext readback 不匹配");
290
+ }
291
+ let plaintext;
292
+ try {
293
+ plaintext = await crypto.decrypt(key, ciphertext);
294
+ }
295
+ catch {
296
+ return fail("E_AUTH_REQUIRED", "author-source chunk scenario key 无法解密");
297
+ }
298
+ if (plaintext.byteLength !== chunk.plaintext_byte_length || digest(plaintext) !== chunk.plaintext_sha256) {
299
+ fail("E_OPERATION_ID_CONFLICT", "author-source chunk plaintext readback 不匹配");
300
+ }
301
+ result.set(plaintext, chunk.plaintext_offset);
302
+ objectHash.update(plaintext);
303
+ }
304
+ if (objectHash.digest("hex") !== descriptor.plaintext_sha256) {
305
+ fail("E_OPERATION_ID_CONFLICT", "author-source logical object digest 不匹配");
306
+ }
307
+ return result;
308
+ }
309
+ async function readRevisionV2(sourceBody, input) {
310
+ const head = parseHead(sourceBody.head, "author-source head");
311
+ const revision = object(sourceBody.revision, "author-source revision v2");
312
+ const receipt = parseReceiptV2(sourceBody.receipt);
313
+ const index = parseRevisionIndexV2(sourceBody.revision_index);
314
+ if (head.scenario_id !== input.target_scenario_id || head.source_digest !== input.target_source_digest
315
+ || head.head_digest !== input.target_head_digest || revision.storage_version !== 2
316
+ || index.owner_id !== head.owner_id || index.scenario_id !== head.scenario_id
317
+ || index.source_revision !== head.source_revision || index.manifest_digest !== head.manifest_digest
318
+ || index.source_digest !== head.source_digest || index.previous_head_digest !== head.previous_head_digest
319
+ || receipt.next_head_digest !== head.head_digest || receipt.source_revision !== head.source_revision
320
+ || receipt.manifest_digest !== head.manifest_digest || receipt.source_digest !== head.source_digest
321
+ || receipt.revision_digest !== head.revision_digest || receipt.previous_head_digest !== head.previous_head_digest
322
+ || receipt.source_size_bytes !== index.source_size_bytes) {
323
+ fail("E_OPERATION_ID_CONFLICT", "author-source v2 index/receipt 与 upload binding 不匹配");
324
+ }
325
+ const expectedRevisionDigest = jsonDigest({ owner_id: head.owner_id, scenario_id: head.scenario_id, source_revision: head.source_revision, manifest_digest: head.manifest_digest, source_digest: head.source_digest, previous_head_digest: head.previous_head_digest });
326
+ const expectedHeadDigest = jsonDigest({ owner_id: head.owner_id, scenario_id: head.scenario_id, source_revision: head.source_revision, manifest_digest: head.manifest_digest, source_digest: head.source_digest, revision_digest: head.revision_digest, previous_head_digest: head.previous_head_digest });
327
+ if (expectedRevisionDigest !== head.revision_digest || expectedHeadDigest !== head.head_digest)
328
+ fail("E_OPERATION_ID_CONFLICT", "author-source v2 identity digest 不匹配");
329
+ const manifestBytes = await readCipherObjectV2(input.transport, input.project_id, input.scenario, head.revision_digest, "manifest", receiptObjectIdV2(receipt, "manifest", null), index.manifest, input.crypto, input.key);
330
+ let manifest;
331
+ try {
332
+ manifest = ScenarioSourceManifest.decode(manifestBytes);
333
+ }
334
+ catch {
335
+ return fail("E_VALIDATION_FAILED", "author-source v2 manifest protobuf 无效");
336
+ }
337
+ if (digest(canonicalScenarioSourceManifestDigestInputBytes(manifest)) !== head.manifest_digest)
338
+ fail("E_OPERATION_ID_CONFLICT", "author-source v2 manifest digest 不匹配");
339
+ const manifestIdentities = new Map(manifest.sections.map((section) => [section.section_key, section]));
340
+ if (manifest.scenario_id !== head.scenario_id || manifestIdentities.size !== index.sections.length)
341
+ fail("E_OPERATION_ID_CONFLICT", "author-source v2 manifest sections 不闭合");
342
+ const sectionBytes = [];
343
+ for (const sectionRef of index.sections) {
344
+ const bytes = await readCipherObjectV2(input.transport, input.project_id, input.scenario, head.revision_digest, "section", receiptObjectIdV2(receipt, "section", sectionRef.section_key), sectionRef.object, input.crypto, input.key);
345
+ let section;
346
+ try {
347
+ section = ScenarioSourceSection.decode(bytes);
348
+ }
349
+ catch {
350
+ return fail("E_VALIDATION_FAILED", `author-source v2 section protobuf 无效:${sectionRef.section_key}`);
351
+ }
352
+ const identity = manifestIdentities.get(sectionRef.section_key);
353
+ if (section.section_key !== sectionRef.section_key || section.media_type !== sectionRef.media_type
354
+ || section.canonical_encoding !== sectionRef.canonical_encoding || identity === undefined
355
+ || identity.media_type !== sectionRef.media_type || identity.canonical_encoding !== sectionRef.canonical_encoding
356
+ || Number(identity.byte_length) !== bytes.byteLength || Buffer.from(identity.sha256).toString("hex") !== digest(bytes)) {
357
+ fail("E_OPERATION_ID_CONFLICT", `author-source v2 section identity 不匹配:${sectionRef.section_key}`);
358
+ }
359
+ sectionBytes.push(bytes);
360
+ }
361
+ const source = NeutralScenarioSource.fromPartial({
362
+ source_schema_version: 1,
363
+ scenario_id: head.scenario_id,
364
+ manifest,
365
+ sections: sectionBytes.map((bytes) => ScenarioSourceSection.decode(bytes)),
366
+ });
367
+ const revisionBytes = canonicalNeutralScenarioSourceBytes(source);
368
+ if (revisionBytes.byteLength !== index.source_size_bytes || digest(revisionBytes) !== head.source_digest) {
369
+ fail("E_OPERATION_ID_CONFLICT", "author-source v2 canonical source digest 不匹配");
370
+ }
371
+ return Object.freeze({ head, revision_bytes: revisionBytes });
372
+ }
373
+ /**
374
+ * 为 standalone Node review 提供真实 read_source_ready。所有远端读取都经过 project-bound
375
+ * ProjectAuth transport;返回值只含已解密并逐 digest 校验的 canonical Neutral revision。
376
+ */
377
+ export function createFrozenReviewSourceCapture(options) {
378
+ return async () => {
379
+ const transport = options.transport;
380
+ if (transport === undefined)
381
+ fail("E_AUTH_REQUIRED", "Node review 没有 ProjectAuthBroker 受控 transport");
382
+ const target = await readFrozenReviewTargetBinding(options.project_root, options.project_id);
383
+ const scenario = encodeURIComponent(target.scenario_id);
384
+ const localPath = join(options.project_root, "scenario", "source.wes");
385
+ const localWesp = new Uint8Array(await readFile(await assertSafeExistingProjectPath(options.project_root, localPath)));
386
+ await decodeScenarioPartWesp(localWesp);
387
+ const [epochResponse, sourceResponse, keyResponse] = await Promise.all([
388
+ transport.request({ project_id: options.project_id, method: "GET", path: "/v1/system/content-epoch" }),
389
+ transport.request({ project_id: options.project_id, method: "GET", path: `/v1/scenarios/${scenario}/author-source` }),
390
+ transport.request({ project_id: options.project_id, method: "GET", path: `/v1/scenarios/${scenario}/${DATABASE_KEY_PATH}` }),
391
+ ]);
392
+ const epochBody = object(requireSuccess(epochResponse, "content epoch readback"), "content epoch readback");
393
+ const contentEpoch = positive(epochBody.epoch, "content epoch");
394
+ if (epochBody.phase !== "active" || epochBody.runtime_generation !== TMW_RUNTIME_GENERATION) {
395
+ fail("E_VALIDATION_FAILED", "content epoch 当前不可用于 review");
396
+ }
397
+ const keyBody = object(requireSuccess(keyResponse, "scenario key readback"), "scenario key readback");
398
+ const key = text(keyBody.key_b64, "scenario key");
399
+ const derivation = object(keyBody.derivation, "scenario key derivation");
400
+ const keyMaxBytes = positive(keyBody.max_bytes, "scenario key max bytes");
401
+ if (derivation.algo !== "HKDF-SHA256" || derivation.info_v !== 1
402
+ || positive(keyBody.ttl_seconds, "scenario key ttl") < 1) {
403
+ fail("E_VALIDATION_FAILED", "scenario key derivation contract 不匹配");
404
+ }
405
+ const sourceBody = object(requireSuccess(sourceResponse, "author-source readback"), "author-source readback");
406
+ if (sourceBody.storage_version === 2) {
407
+ const crypto = options.crypto ?? createFrozenReviewWasmEncryptor();
408
+ const v2 = await readRevisionV2(sourceBody, {
409
+ transport,
410
+ project_id: options.project_id,
411
+ scenario,
412
+ target_scenario_id: target.scenario_id,
413
+ target_source_digest: target.source_digest,
414
+ target_head_digest: target.source_head_digest,
415
+ crypto,
416
+ key,
417
+ });
418
+ const decoded = await decodeNeutralScenarioSource(v2.revision_bytes);
419
+ const localSection = decoded.sections.find((section) => section.section_key === LOCAL_WESP_SECTION);
420
+ if (localSection === undefined || localSection.media_type !== LOCAL_WESP_MEDIA_TYPE
421
+ || localSection.canonical_encoding !== LOCAL_WESP_ENCODING
422
+ || !Buffer.from(localSection.payload).equals(Buffer.from(localWesp))) {
423
+ fail("E_OPERATION_ID_CONFLICT", "远端 canonical Neutral revision 未绑定当前本地 WESP upload");
424
+ }
425
+ return Object.freeze({ target, content_epoch: contentEpoch, head: Object.freeze({ ...v2.head }), revision_bytes: v2.revision_bytes.slice() });
426
+ }
427
+ if (sourceBody.storage_version !== undefined && sourceBody.storage_version !== 1)
428
+ fail("E_VALIDATION_FAILED", "author-source storage version 不支持");
429
+ const head = parseHead(sourceBody.head, "author-source head");
430
+ const revision = parseRevision(sourceBody.revision);
431
+ const receipt = parseReceipt(sourceBody.receipt);
432
+ const manifestCiphertext = base64(sourceBody.manifest_ciphertext_base64, "manifest ciphertext");
433
+ const revisionCiphertext = base64(sourceBody.revision_ciphertext_base64, "revision ciphertext");
434
+ const manifestWitness = uniqueWitness(receipt, "manifest", null);
435
+ const revisionWitness = uniqueWitness(receipt, "revision", null);
436
+ if (!sameHead(head, revision)
437
+ || head.scenario_id !== target.scenario_id
438
+ || head.source_digest !== target.source_digest
439
+ || head.head_digest !== target.source_head_digest
440
+ || receipt.next_head_digest !== head.head_digest
441
+ || receipt.source_revision !== head.source_revision
442
+ || receipt.previous_head_digest !== head.previous_head_digest
443
+ || receipt.manifest_digest !== head.manifest_digest
444
+ || receipt.source_digest !== head.source_digest
445
+ || receipt.revision_digest !== head.revision_digest
446
+ || receipt.revision_size_bytes !== revision.revision_size_bytes
447
+ || revision.revision_ciphertext_sha256 !== revisionWitness.ciphertext_sha256
448
+ || revision.revision_ciphertext_size_bytes !== revisionWitness.ciphertext_size_bytes) {
449
+ fail("E_OPERATION_ID_CONFLICT", "author-source head/revision/receipt 与 upload binding 不匹配");
450
+ }
451
+ if (manifestWitness.plaintext_size_bytes > keyMaxBytes
452
+ || revisionWitness.plaintext_size_bytes > keyMaxBytes
453
+ || receipt.objects.some((item) => item.plaintext_size_bytes > keyMaxBytes)) {
454
+ fail("E_VALIDATION_FAILED", "author-source object 超过 scenario key max_bytes");
455
+ }
456
+ const expectedRevisionDigest = jsonDigest({
457
+ owner_id: head.owner_id,
458
+ scenario_id: head.scenario_id,
459
+ source_revision: head.source_revision,
460
+ manifest_digest: head.manifest_digest,
461
+ source_digest: head.source_digest,
462
+ previous_head_digest: head.previous_head_digest,
463
+ });
464
+ const expectedHeadDigest = jsonDigest({
465
+ owner_id: head.owner_id,
466
+ scenario_id: head.scenario_id,
467
+ source_revision: head.source_revision,
468
+ manifest_digest: head.manifest_digest,
469
+ source_digest: head.source_digest,
470
+ revision_digest: head.revision_digest,
471
+ previous_head_digest: head.previous_head_digest,
472
+ });
473
+ if (expectedRevisionDigest !== head.revision_digest || expectedHeadDigest !== head.head_digest) {
474
+ fail("E_OPERATION_ID_CONFLICT", "author-source revision/head identity digest 不匹配");
475
+ }
476
+ const expectedRequestDigest = jsonDigest({
477
+ owner_id: head.owner_id,
478
+ scenario_id: head.scenario_id,
479
+ operation_id: receipt.operation_id,
480
+ expected_head_digest: receipt.expected_head_digest,
481
+ source_revision: head.source_revision,
482
+ previous_head_digest: head.previous_head_digest,
483
+ manifest_digest: head.manifest_digest,
484
+ source_digest: head.source_digest,
485
+ revision_digest: head.revision_digest,
486
+ revision_size_bytes: receipt.revision_size_bytes,
487
+ manifest_size_bytes: receipt.manifest_size_bytes,
488
+ objects: receipt.objects,
489
+ });
490
+ if (expectedRequestDigest !== receipt.request_sha256) {
491
+ fail("E_OPERATION_ID_CONFLICT", "author-source request receipt digest 不匹配");
492
+ }
493
+ const crypto = options.crypto ?? createFrozenReviewWasmEncryptor();
494
+ const [manifestBytes, revisionBytes] = await Promise.all([
495
+ decryptVerified(crypto, key, manifestCiphertext, manifestWitness),
496
+ decryptVerified(crypto, key, revisionCiphertext, revisionWitness),
497
+ ]);
498
+ if (manifestBytes.byteLength !== receipt.manifest_size_bytes
499
+ || revisionBytes.byteLength !== receipt.revision_size_bytes
500
+ || digest(manifestBytes) !== head.manifest_digest
501
+ || digest(revisionBytes) !== head.source_digest) {
502
+ fail("E_OPERATION_ID_CONFLICT", "author-source manifest/revision digest 不匹配");
503
+ }
504
+ const source = await decodeNeutralScenarioSource(revisionBytes);
505
+ if (source.scenario_id !== target.scenario_id
506
+ || !Buffer.from(canonicalNeutralScenarioSourceBytes(source)).equals(Buffer.from(revisionBytes))
507
+ || source.manifest === undefined
508
+ || !Buffer.from(canonicalScenarioSourceManifestDigestInputBytes(source.manifest)).equals(Buffer.from(manifestBytes))) {
509
+ fail("E_OPERATION_ID_CONFLICT", "author-source canonical Neutral revision 不匹配");
510
+ }
511
+ for (const section of source.sections) {
512
+ const witness = uniqueWitness(receipt, "section", section.section_key);
513
+ if (witness.media_type !== section.media_type || witness.canonical_encoding !== section.canonical_encoding) {
514
+ fail("E_OPERATION_ID_CONFLICT", `author-source section witness 不匹配:${section.section_key}`);
515
+ }
516
+ const query = new URLSearchParams({
517
+ section_key: section.section_key,
518
+ ciphertext_sha256: witness.ciphertext_sha256,
519
+ ciphertext_size_bytes: String(witness.ciphertext_size_bytes),
520
+ });
521
+ const response = await transport.request({
522
+ project_id: options.project_id,
523
+ method: "GET",
524
+ path: `/v1/scenarios/${scenario}/author-source?${query.toString()}`,
525
+ });
526
+ const body = object(requireSuccess(response, "author-source section readback"), "author-source section readback");
527
+ if (body.section_key !== section.section_key
528
+ || body.ciphertext_sha256 !== witness.ciphertext_sha256
529
+ || body.ciphertext_size_bytes !== witness.ciphertext_size_bytes) {
530
+ fail("E_OPERATION_ID_CONFLICT", `author-source section ciphertext identity 不匹配:${section.section_key}`);
531
+ }
532
+ const plaintext = await decryptVerified(crypto, key, base64(body.ciphertext_base64, "section ciphertext"), witness);
533
+ if (!Buffer.from(plaintext).equals(Buffer.from(canonicalScenarioSourceSectionBytes(section)))) {
534
+ fail("E_OPERATION_ID_CONFLICT", `author-source section plaintext 不匹配:${section.section_key}`);
535
+ }
536
+ }
537
+ const localSection = source.sections.find((section) => section.section_key === LOCAL_WESP_SECTION);
538
+ if (localSection === undefined
539
+ || localSection.media_type !== LOCAL_WESP_MEDIA_TYPE
540
+ || localSection.canonical_encoding !== LOCAL_WESP_ENCODING
541
+ || !Buffer.from(localSection.payload).equals(Buffer.from(localWesp))) {
542
+ fail("E_OPERATION_ID_CONFLICT", "远端 canonical Neutral revision 未绑定当前本地 WESP upload");
543
+ }
544
+ return Object.freeze({
545
+ target,
546
+ content_epoch: contentEpoch,
547
+ head: Object.freeze({ ...head }),
548
+ revision_bytes: revisionBytes.slice(),
549
+ });
550
+ };
551
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Node 提审侧的 SCNB 加密薄适配器。密钥只作为参数交给 Rust/WASM 权威实现;
3
+ * 本模块不读取环境密钥,也不提供 JS 加密回退。
4
+ */
5
+ export function createFrozenReviewWasmEncryptor(assets?: Readonly<{
6
+ glue_url: URL;
7
+ wasm_url: URL;
8
+ }>): Readonly<{
9
+ pack(input: any): Promise<Readonly<{
10
+ codec: "frozen-database-v1";
11
+ vector_dtype: "f32-le" | "f64-le";
12
+ bytes: Uint8Array;
13
+ }>>;
14
+ encrypt(key_b64: any, plaintext: any): Promise<any>;
15
+ decrypt(key_b64: any, ciphertext: any): Promise<any>;
16
+ }>;
@@ -0,0 +1,72 @@
1
+ import { readFile } from "node:fs/promises";
2
+ const initializationByGlue = new Map();
3
+ function packagedAssets() {
4
+ return Object.freeze({
5
+ glue_url: new URL("./vendor/wasm-core/wasm_core.js", import.meta.url),
6
+ wasm_url: new URL("./vendor/wasm-core/wasm_core_bg.wasm", import.meta.url),
7
+ });
8
+ }
9
+ async function initialize(assets) {
10
+ const identity = assets.glue_url.href;
11
+ const current = initializationByGlue.get(identity);
12
+ if (current !== undefined) {
13
+ if (current.wasm_url !== assets.wasm_url.href) {
14
+ throw new Error("同一 WASM glue 不能绑定不同 wasm 资产");
15
+ }
16
+ return await current.promise;
17
+ }
18
+ const pending = (async () => {
19
+ const [glue, wasmBytes] = await Promise.all([
20
+ import(assets.glue_url.href),
21
+ readFile(assets.wasm_url),
22
+ ]);
23
+ glue.initSync({ module: wasmBytes });
24
+ return Object.freeze({
25
+ encrypt: glue.ext_encrypt_scenario_bytes,
26
+ decrypt: glue.ext_decrypt_scenario_bytes,
27
+ pack: glue.ext_pack_frozen_database_v1,
28
+ packValues: glue.ext_pack_frozen_database_values_v1,
29
+ });
30
+ })();
31
+ const initialization = Object.freeze({ wasm_url: assets.wasm_url.href, promise: pending });
32
+ initializationByGlue.set(identity, initialization);
33
+ try {
34
+ return await pending;
35
+ }
36
+ catch (error) {
37
+ if (initializationByGlue.get(identity) === initialization) {
38
+ initializationByGlue.delete(identity);
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ /**
44
+ * Node 提审侧的 SCNB 加密薄适配器。密钥只作为参数交给 Rust/WASM 权威实现;
45
+ * 本模块不读取环境密钥,也不提供 JS 加密回退。
46
+ */
47
+ export function createFrozenReviewWasmEncryptor(assets = packagedAssets()) {
48
+ const frozenAssets = Object.freeze({
49
+ glue_url: new URL(assets.glue_url.href),
50
+ wasm_url: new URL(assets.wasm_url.href),
51
+ });
52
+ return Object.freeze({
53
+ async pack(input) {
54
+ const [dtype, bytes] = (await initialize(frozenAssets)).packValues(input.graph_v5_json, input.embedding_index_v1_json, input.vector_values, input.sidecar_v5_json);
55
+ if ((dtype !== 1 && dtype !== 2) || !(bytes instanceof Uint8Array) || bytes.byteLength === 0) {
56
+ throw new Error("frozen_database_pack_result_invalid");
57
+ }
58
+ return Object.freeze({
59
+ codec: "frozen-database-v1",
60
+ vector_dtype: dtype === 1 ? "f32-le" : "f64-le",
61
+ bytes,
62
+ });
63
+ },
64
+ async encrypt(key_b64, plaintext) {
65
+ return (await initialize(frozenAssets)).encrypt(key_b64, plaintext);
66
+ },
67
+ async decrypt(key_b64, ciphertext) {
68
+ return (await initialize(frozenAssets)).decrypt(key_b64, ciphertext);
69
+ },
70
+ });
71
+ }
72
+