@aura3d/cli 4.7.18 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/data/aura.exports.json +589 -87
  2. package/dist/admission/artDirection.d.ts.map +1 -1
  3. package/dist/admission/artDirection.js.map +1 -1
  4. package/dist/admission/audio.d.ts.map +1 -1
  5. package/dist/admission/audio.js.map +1 -1
  6. package/dist/admission/builder-patterns.d.ts.map +1 -1
  7. package/dist/admission/builder-patterns.js.map +1 -1
  8. package/dist/admission/cubeLut.d.ts.map +1 -1
  9. package/dist/admission/cubeLut.js.map +1 -1
  10. package/dist/admission/gates.d.ts.map +1 -1
  11. package/dist/admission/gates.js.map +1 -1
  12. package/dist/admission/glb.d.ts.map +1 -1
  13. package/dist/admission/glb.js.map +1 -1
  14. package/dist/admission/index.d.ts.map +1 -1
  15. package/dist/admission/index.js.map +1 -1
  16. package/dist/admission/profiles.d.ts.map +1 -1
  17. package/dist/admission/profiles.js.map +1 -1
  18. package/dist/admission/qrFlags.d.ts.map +1 -1
  19. package/dist/admission/qrFlags.js.map +1 -1
  20. package/dist/admission/routeGates.d.ts.map +1 -1
  21. package/dist/admission/routeGates.js.map +1 -1
  22. package/dist/admission/texel.d.ts.map +1 -1
  23. package/dist/admission/texel.js.map +1 -1
  24. package/dist/admission/textureStats.d.ts.map +1 -1
  25. package/dist/admission/textureStats.js.map +1 -1
  26. package/dist/animation-asset-profiles.d.ts.map +1 -1
  27. package/dist/animation-asset-profiles.js.map +1 -1
  28. package/dist/animation-asset-validator.d.ts.map +1 -1
  29. package/dist/animation-asset-validator.js.map +1 -1
  30. package/dist/asset-constants.d.ts +2 -0
  31. package/dist/asset-constants.d.ts.map +1 -1
  32. package/dist/asset-constants.js +2 -0
  33. package/dist/asset-constants.js.map +1 -1
  34. package/dist/asset-core-types.d.ts +43 -1
  35. package/dist/asset-core-types.d.ts.map +1 -1
  36. package/dist/asset-core-types.js.map +1 -1
  37. package/dist/asset-inspection-types.d.ts.map +1 -1
  38. package/dist/asset-inspection-types.js.map +1 -1
  39. package/dist/asset-intent.d.ts.map +1 -1
  40. package/dist/asset-intent.js.map +1 -1
  41. package/dist/asset-manifest.d.ts +1 -1
  42. package/dist/asset-manifest.d.ts.map +1 -1
  43. package/dist/asset-manifest.js +160 -6
  44. package/dist/asset-manifest.js.map +1 -1
  45. package/dist/asset-readiness-types.d.ts.map +1 -1
  46. package/dist/asset-readiness-types.js.map +1 -1
  47. package/dist/asset-role-admission.d.ts.map +1 -1
  48. package/dist/asset-role-admission.js.map +1 -1
  49. package/dist/asset-route-typegen.d.ts.map +1 -1
  50. package/dist/asset-route-typegen.js +90 -1
  51. package/dist/asset-route-typegen.js.map +1 -1
  52. package/dist/asset-screening-effects.d.ts.map +1 -1
  53. package/dist/asset-screening-effects.js.map +1 -1
  54. package/dist/asset-screening-pipeline.d.ts.map +1 -1
  55. package/dist/asset-screening-pipeline.js.map +1 -1
  56. package/dist/asset-source-ast.d.ts.map +1 -1
  57. package/dist/asset-source-ast.js.map +1 -1
  58. package/dist/asset-source-roles.d.ts.map +1 -1
  59. package/dist/asset-source-roles.js.map +1 -1
  60. package/dist/asset-source-validation.d.ts.map +1 -1
  61. package/dist/asset-source-validation.js.map +1 -1
  62. package/dist/character-assembly-types.d.ts.map +1 -1
  63. package/dist/character-assembly-types.js.map +1 -1
  64. package/dist/cli-help.d.ts.map +1 -1
  65. package/dist/cli-help.js +16 -3
  66. package/dist/cli-help.js.map +1 -1
  67. package/dist/cli-options.d.ts +1 -1
  68. package/dist/cli-options.d.ts.map +1 -1
  69. package/dist/cli-options.js +2 -2
  70. package/dist/cli-options.js.map +1 -1
  71. package/dist/cli.d.ts.map +1 -1
  72. package/dist/cli.js +7 -2
  73. package/dist/cli.js.map +1 -1
  74. package/dist/codemods/camera-cast.d.ts.map +1 -1
  75. package/dist/codemods/camera-cast.js.map +1 -1
  76. package/dist/codemods/core-v2.d.ts.map +1 -1
  77. package/dist/codemods/core-v2.js.map +1 -1
  78. package/dist/codemods/engine-entry-imports.d.ts.map +1 -1
  79. package/dist/codemods/engine-entry-imports.js.map +1 -1
  80. package/dist/codemods/lean-imports.d.ts.map +1 -1
  81. package/dist/codemods/lean-imports.js.map +1 -1
  82. package/dist/codemods/post-v2.d.ts.map +1 -1
  83. package/dist/codemods/post-v2.js.map +1 -1
  84. package/dist/codemods/renderer-imports.d.ts.map +1 -1
  85. package/dist/codemods/renderer-imports.js +49 -4
  86. package/dist/codemods/renderer-imports.js.map +1 -1
  87. package/dist/codemods/renderer-mode.d.ts.map +1 -1
  88. package/dist/codemods/renderer-mode.js.map +1 -1
  89. package/dist/commands/assets/certifyCategories.d.ts.map +1 -1
  90. package/dist/commands/assets/certifyCategories.js.map +1 -1
  91. package/dist/commands/assets/export.d.ts +20 -0
  92. package/dist/commands/assets/export.d.ts.map +1 -0
  93. package/dist/commands/assets/export.js +431 -0
  94. package/dist/commands/assets/export.js.map +1 -0
  95. package/dist/commands/assets/gltfToolkit.d.ts.map +1 -1
  96. package/dist/commands/assets/gltfToolkit.js.map +1 -1
  97. package/dist/commands/assets/importMeshy.d.ts.map +1 -1
  98. package/dist/commands/assets/importMeshy.js.map +1 -1
  99. package/dist/commands/assets/index.d.ts.map +1 -1
  100. package/dist/commands/assets/index.js +8 -0
  101. package/dist/commands/assets/index.js.map +1 -1
  102. package/dist/commands/assets/mergeClips.d.ts.map +1 -1
  103. package/dist/commands/assets/mergeClips.js.map +1 -1
  104. package/dist/commands/assets/optimize.d.ts +9 -1
  105. package/dist/commands/assets/optimize.d.ts.map +1 -1
  106. package/dist/commands/assets/optimize.js +29 -5
  107. package/dist/commands/assets/optimize.js.map +1 -1
  108. package/dist/commands/assets/optimizeVerb.d.ts.map +1 -1
  109. package/dist/commands/assets/optimizeVerb.js +22 -1
  110. package/dist/commands/assets/optimizeVerb.js.map +1 -1
  111. package/dist/commands/evidence/index.d.ts.map +1 -1
  112. package/dist/commands/evidence/index.js.map +1 -1
  113. package/dist/commands/evidence/judge.d.ts.map +1 -1
  114. package/dist/commands/evidence/judge.js.map +1 -1
  115. package/dist/commands/evidence/judgeLib.d.ts.map +1 -1
  116. package/dist/commands/evidence/judgeLib.js.map +1 -1
  117. package/dist/commands/meshyLedger.d.ts.map +1 -1
  118. package/dist/commands/meshyLedger.js.map +1 -1
  119. package/dist/commands/prd01/index.d.ts.map +1 -1
  120. package/dist/commands/prd01/index.js.map +1 -1
  121. package/dist/commands/prd02/environmentsBake.d.ts.map +1 -1
  122. package/dist/commands/prd02/environmentsBake.js +23 -2
  123. package/dist/commands/prd02/environmentsBake.js.map +1 -1
  124. package/dist/commands/prd02/index.d.ts.map +1 -1
  125. package/dist/commands/prd02/index.js +1 -1
  126. package/dist/commands/prd02/index.js.map +1 -1
  127. package/dist/commands/prd02/migrateLighting.d.ts.map +1 -1
  128. package/dist/commands/prd02/migrateLighting.js.map +1 -1
  129. package/dist/commands/prd02/migrateLightingSweep.d.ts.map +1 -1
  130. package/dist/commands/prd02/migrateLightingSweep.js.map +1 -1
  131. package/dist/commands/prd02/probeFormatOptions.d.ts +8 -0
  132. package/dist/commands/prd02/probeFormatOptions.d.ts.map +1 -0
  133. package/dist/commands/prd02/probeFormatOptions.js +19 -0
  134. package/dist/commands/prd02/probeFormatOptions.js.map +1 -0
  135. package/dist/commands/prd03/index.d.ts.map +1 -1
  136. package/dist/commands/prd03/index.js.map +1 -1
  137. package/dist/commands/prd04/index.d.ts.map +1 -1
  138. package/dist/commands/prd04/index.js.map +1 -1
  139. package/dist/commands/prd04/modelTintReport.d.ts.map +1 -1
  140. package/dist/commands/prd04/modelTintReport.js.map +1 -1
  141. package/dist/commands/prd04/pinEmissiveDefaults.d.ts.map +1 -1
  142. package/dist/commands/prd04/pinEmissiveDefaults.js.map +1 -1
  143. package/dist/commands/prd05/admit.d.ts.map +1 -1
  144. package/dist/commands/prd05/admit.js.map +1 -1
  145. package/dist/commands/prd05/codemods/assetsRouteModules.d.ts.map +1 -1
  146. package/dist/commands/prd05/codemods/assetsRouteModules.js.map +1 -1
  147. package/dist/commands/prd05/index.d.ts.map +1 -1
  148. package/dist/commands/prd05/index.js +1 -1
  149. package/dist/commands/prd05/index.js.map +1 -1
  150. package/dist/commands/prd05/library.d.ts.map +1 -1
  151. package/dist/commands/prd05/library.js.map +1 -1
  152. package/dist/commands/prd05/lookdev.d.ts.map +1 -1
  153. package/dist/commands/prd05/lookdev.js.map +1 -1
  154. package/dist/commands/prd05/optimize.d.ts.map +1 -1
  155. package/dist/commands/prd05/optimize.js.map +1 -1
  156. package/dist/commands/prd05/prune.d.ts.map +1 -1
  157. package/dist/commands/prd05/prune.js.map +1 -1
  158. package/dist/commands/prd05/review.d.ts.map +1 -1
  159. package/dist/commands/prd05/review.js.map +1 -1
  160. package/dist/commands/prd06/index.d.ts.map +1 -1
  161. package/dist/commands/prd06/index.js.map +1 -1
  162. package/dist/commands/prd06/inspectAnimationClips.d.ts.map +1 -1
  163. package/dist/commands/prd06/inspectAnimationClips.js.map +1 -1
  164. package/dist/commands/prd06/validateHero.d.ts.map +1 -1
  165. package/dist/commands/prd06/validateHero.js.map +1 -1
  166. package/dist/commands/prd07/codemods.d.ts.map +1 -1
  167. package/dist/commands/prd07/codemods.js.map +1 -1
  168. package/dist/commands/prd07/index.d.ts.map +1 -1
  169. package/dist/commands/prd07/index.js.map +1 -1
  170. package/dist/commands/prd08/feelLint.d.ts.map +1 -1
  171. package/dist/commands/prd08/feelLint.js.map +1 -1
  172. package/dist/commands/prd08/index.d.ts.map +1 -1
  173. package/dist/commands/prd08/index.js.map +1 -1
  174. package/dist/commands/prd08/motionReport.d.ts.map +1 -1
  175. package/dist/commands/prd08/motionReport.js.map +1 -1
  176. package/dist/commands/prd09/codemods/audio-wrapper.d.ts.map +1 -1
  177. package/dist/commands/prd09/codemods/audio-wrapper.js.map +1 -1
  178. package/dist/commands/prd09/codemods/capture-branches.d.ts.map +1 -1
  179. package/dist/commands/prd09/codemods/capture-branches.js.map +1 -1
  180. package/dist/commands/prd09/codemods/evidence-globals.d.ts.map +1 -1
  181. package/dist/commands/prd09/codemods/evidence-globals.js.map +1 -1
  182. package/dist/commands/prd09/codemods/perf-script.d.ts.map +1 -1
  183. package/dist/commands/prd09/codemods/perf-script.js.map +1 -1
  184. package/dist/commands/prd09/index.d.ts.map +1 -1
  185. package/dist/commands/prd09/index.js.map +1 -1
  186. package/dist/commands/prd09/perf-report.d.ts.map +1 -1
  187. package/dist/commands/prd09/perf-report.js.map +1 -1
  188. package/dist/commands/prd09/sfx-admit.d.ts.map +1 -1
  189. package/dist/commands/prd09/sfx-admit.js.map +1 -1
  190. package/dist/commands/prd10/index.d.ts.map +1 -1
  191. package/dist/commands/prd10/index.js.map +1 -1
  192. package/dist/commands/prd11/batchOptout.d.ts.map +1 -1
  193. package/dist/commands/prd11/batchOptout.js.map +1 -1
  194. package/dist/commands/prd11/index.d.ts.map +1 -1
  195. package/dist/commands/prd11/index.js.map +1 -1
  196. package/dist/commands/prd11/perfGate.d.ts.map +1 -1
  197. package/dist/commands/prd11/perfGate.js.map +1 -1
  198. package/dist/commands/prd13/index.d.ts.map +1 -1
  199. package/dist/commands/prd13/index.js.map +1 -1
  200. package/dist/commands/prd13/run.d.ts.map +1 -1
  201. package/dist/commands/prd13/run.js.map +1 -1
  202. package/dist/commands/prd14/index.d.ts.map +1 -1
  203. package/dist/commands/prd14/index.js.map +1 -1
  204. package/dist/commands/prd15/index.d.ts.map +1 -1
  205. package/dist/commands/prd15/index.js.map +1 -1
  206. package/dist/commands/registry.d.ts.map +1 -1
  207. package/dist/commands/registry.js.map +1 -1
  208. package/dist/contracts/assetManifest.d.ts.map +1 -1
  209. package/dist/contracts/assetManifest.js.map +1 -1
  210. package/dist/contracts/commands.d.ts.map +1 -1
  211. package/dist/contracts/commands.js.map +1 -1
  212. package/dist/deploy-browser-check.d.ts.map +1 -1
  213. package/dist/deploy-browser-check.js.map +1 -1
  214. package/dist/game-asset-profiles.d.ts.map +1 -1
  215. package/dist/game-asset-profiles.js.map +1 -1
  216. package/dist/game-asset-validator.d.ts.map +1 -1
  217. package/dist/game-asset-validator.js.map +1 -1
  218. package/dist/glb-dependency-validation.d.ts +41 -0
  219. package/dist/glb-dependency-validation.d.ts.map +1 -0
  220. package/dist/glb-dependency-validation.js +574 -0
  221. package/dist/glb-dependency-validation.js.map +1 -0
  222. package/dist/index.d.ts +4 -1
  223. package/dist/index.d.ts.map +1 -1
  224. package/dist/index.js +1276 -35
  225. package/dist/index.js.map +1 -1
  226. package/dist/library-manifest.d.ts.map +1 -1
  227. package/dist/library-manifest.js.map +1 -1
  228. package/dist/lint/feelSourceScan.d.ts.map +1 -1
  229. package/dist/lint/feelSourceScan.js.map +1 -1
  230. package/dist/look/capture.d.ts.map +1 -1
  231. package/dist/look/capture.js.map +1 -1
  232. package/dist/look/judge.d.ts.map +1 -1
  233. package/dist/look/judge.js.map +1 -1
  234. package/dist/look/lint-static.d.ts.map +1 -1
  235. package/dist/look/lint-static.js.map +1 -1
  236. package/dist/look/rubric.d.ts.map +1 -1
  237. package/dist/look/rubric.js.map +1 -1
  238. package/dist/meshy/admission.d.ts.map +1 -1
  239. package/dist/meshy/admission.js.map +1 -1
  240. package/dist/meshy/evidence.d.ts.map +1 -1
  241. package/dist/meshy/evidence.js.map +1 -1
  242. package/dist/meshy/import.d.ts.map +1 -1
  243. package/dist/meshy/import.js.map +1 -1
  244. package/dist/meshy/index.d.ts.map +1 -1
  245. package/dist/meshy/index.js.map +1 -1
  246. package/dist/meshy/metadata.d.ts.map +1 -1
  247. package/dist/meshy/metadata.js.map +1 -1
  248. package/dist/meshy/provenance.d.ts.map +1 -1
  249. package/dist/meshy/provenance.js.map +1 -1
  250. package/dist/meshy/thumbnail.d.ts.map +1 -1
  251. package/dist/meshy/thumbnail.js.map +1 -1
  252. package/dist/meshy/validation.d.ts.map +1 -1
  253. package/dist/meshy/validation.js.map +1 -1
  254. package/dist/migrate-three/CompatibilityWarnings.d.ts.map +1 -1
  255. package/dist/migrate-three/CompatibilityWarnings.js.map +1 -1
  256. package/dist/migrate-three/ImportMap.d.ts.map +1 -1
  257. package/dist/migrate-three/ImportMap.js.map +1 -1
  258. package/dist/migrate-three/R3fMigration.d.ts.map +1 -1
  259. package/dist/migrate-three/R3fMigration.js.map +1 -1
  260. package/dist/migrate-three/ThreeToA3DAdapter.d.ts.map +1 -1
  261. package/dist/migrate-three/ThreeToA3DAdapter.js.map +1 -1
  262. package/dist/migrate-three/emit.d.ts.map +1 -1
  263. package/dist/migrate-three/emit.js.map +1 -1
  264. package/dist/migrate-three/mappings.d.ts.map +1 -1
  265. package/dist/migrate-three/mappings.js +32 -17
  266. package/dist/migrate-three/mappings.js.map +1 -1
  267. package/dist/migrate-three/parse.d.ts.map +1 -1
  268. package/dist/migrate-three/parse.js.map +1 -1
  269. package/dist/migrate-three/report.d.ts.map +1 -1
  270. package/dist/migrate-three/report.js.map +1 -1
  271. package/dist/pull-bridge/adapters.d.ts.map +1 -1
  272. package/dist/pull-bridge/adapters.js.map +1 -1
  273. package/dist/pull-bridge/download.d.ts.map +1 -1
  274. package/dist/pull-bridge/download.js.map +1 -1
  275. package/dist/pull-bridge/profiles.d.ts.map +1 -1
  276. package/dist/pull-bridge/profiles.js.map +1 -1
  277. package/dist/pull-bridge/provenance.d.ts.map +1 -1
  278. package/dist/pull-bridge/provenance.js.map +1 -1
  279. package/dist/pull-bridge/pullable.d.ts.map +1 -1
  280. package/dist/pull-bridge/pullable.js.map +1 -1
  281. package/dist/pull-bridge/scoring.d.ts.map +1 -1
  282. package/dist/pull-bridge/scoring.js.map +1 -1
  283. package/dist/pull-bridge/search.d.ts.map +1 -1
  284. package/dist/pull-bridge/search.js.map +1 -1
  285. package/dist/pull-bridge/types.d.ts.map +1 -1
  286. package/dist/pull-bridge/types.js.map +1 -1
  287. package/dist/pull-bridge/vector3.d.ts.map +1 -1
  288. package/dist/pull-bridge/vector3.js.map +1 -1
  289. package/dist/pull-bridge.d.ts.map +1 -1
  290. package/dist/pull-bridge.js.map +1 -1
  291. package/dist/resolve-ladder.d.ts.map +1 -1
  292. package/dist/resolve-ladder.js.map +1 -1
  293. package/dist/skin/analysis.d.ts.map +1 -1
  294. package/dist/skin/analysis.js.map +1 -1
  295. package/dist/skin/command.d.ts.map +1 -1
  296. package/dist/skin/command.js.map +1 -1
  297. package/dist/skin/glbEdit.d.ts.map +1 -1
  298. package/dist/skin/glbEdit.js.map +1 -1
  299. package/dist/skin/gltf.d.ts +15 -0
  300. package/dist/skin/gltf.d.ts.map +1 -1
  301. package/dist/skin/gltf.js.map +1 -1
  302. package/dist/skin/rebuildLimbs.d.ts.map +1 -1
  303. package/dist/skin/rebuildLimbs.js.map +1 -1
  304. package/dist/skin/refitSkeleton.d.ts.map +1 -1
  305. package/dist/skin/refitSkeleton.js.map +1 -1
  306. package/dist/skin/repair.d.ts.map +1 -1
  307. package/dist/skin/repair.js.map +1 -1
  308. package/package.json +7 -6
  309. package/skills/aura3d-browser-game/references/kits-and-hooks.md +1 -1
  310. package/skills/aura3d-scene-authoring/SKILL.md +2 -2
  311. package/skills/llms.txt +2 -2
package/dist/index.js CHANGED
@@ -1,20 +1,22 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
1
+ import { closeSync, copyFileSync, existsSync, fstatSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
2
2
  import { readSkinAllowlist, skinReleaseFailures } from "./skin/command.js";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { createHash } from "node:crypto";
5
5
  import { createRequire } from "node:module";
6
6
  import { fileURLToPath } from "node:url";
7
- import { dirname, extname, join, relative, resolve, sep } from "node:path";
7
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { inflateSync } from "node:zlib";
9
9
  import { writeAgentSkills } from "create-aura3d";
10
10
  import { measureLoudness } from "@aura3d/audio";
11
+ import { inspectNRRDDataReference, OBJLoader, parseModel3MF, parsePLY, parseSTL } from "@aura3d/assets";
11
12
  import { admitAssetForRole } from "./asset-role-admission.js";
13
+ import { assertStagedGlbFile, captureGlbSource, checkGlbDependencies, referencedGlbResourcePaths } from "./glb-dependency-validation.js";
12
14
  import { CERTIFY_CATEGORY_RULES } from "./commands/assets/certifyCategories.js";
13
15
  import { hasStylizedFlatApproval } from "./admission/artDirection.js";
14
16
  import { AUDIO_SYNTH_RELEASE_ALLOWLIST, isSynthesizedAudioProvenance } from "./admission/audio.js";
15
17
  import { validateCubeLut } from "./admission/cubeLut.js";
16
18
  import { profileForRole, requiresPbrTextures } from "./admission/profiles.js";
17
- import { DEFAULT_AURA_ASSET_MANIFEST, DEFAULT_AURA_ASSET_OUTPUT_DIR, DEFAULT_AURA_ASSET_PUBLIC_PATH, DEFAULT_AURA_ASSET_TYPEGEN } from "./asset-constants.js";
19
+ import { DEFAULT_AURA_ASSET_MANIFEST, DEFAULT_AURA_ASSET_OUTPUT_DIR, DEFAULT_AURA_ASSET_PUBLIC_PATH, DEFAULT_AURA_ASSET_TYPEGEN, NATIVE_CATALOG_MODEL_FORMATS } from "./asset-constants.js";
18
20
  import { shouldScanSource, validateAssetSource } from "./asset-source-validation.js";
19
21
  import { listAssets, readAssetManifest, writeAssetManifest, writeTypedAssets } from "./asset-manifest.js";
20
22
  export { animationCliAssetProfiles, getAnimationAssetProfileDefinition } from "./animation-asset-profiles.js";
@@ -149,38 +151,367 @@ function measureAudioLoudness(sourcePath) {
149
151
  return undefined;
150
152
  }
151
153
  }
154
+ /** Synchronous admission remains available where inspection is synchronous. */
152
155
  export function addAsset(options) {
156
+ const sourcePath = resolve(options.projectDir ?? process.cwd(), options.file);
157
+ const format = extname(sourcePath).slice(1).toLowerCase();
158
+ if (format === "3mf")
159
+ throw new Error("Aura3D assets add failed: 3MF archive inspection is asynchronous; use addAssetAsync or the assets add command.");
160
+ if (format === "pvr")
161
+ throw new Error("Aura3D assets add failed: PVR texture inspection is asynchronous; use addAssetAsync or the assets add command.");
162
+ if (format === "vox")
163
+ throw new Error("Aura3D assets add failed: VOX model inspection is asynchronous; use addAssetAsync or the assets add command.");
164
+ if (format === "nrrd" || format === "nhdr")
165
+ throw new Error("Aura3D assets add failed: scalar-volume inspection is asynchronous; use addAssetAsync or the assets add command.");
166
+ // Without strict inspection the deprecated legacy path stays available and is
167
+ // recorded as legacy-unverified; parser admission for catalog formats is async.
168
+ if (NATIVE_CATALOG_MODEL_FORMATS.has(format) && options.modelInspection === "strict") {
169
+ throw new Error(`Aura3D assets add failed: strict bounded parser inspection is unavailable synchronously for .${format}; native-format admission is asynchronous, use addAssetAsync or the assets add command.`);
170
+ }
171
+ if (format === "glb") {
172
+ const projectDir = resolve(options.projectDir ?? process.cwd());
173
+ const captured = captureGlbSource(projectDir, sourcePath);
174
+ if (captured.dependencies.length > 0) {
175
+ const inspection = inspectGlb(captured.bytes);
176
+ return withScalarVolumeAdmissionLock(projectDir, () => addAssetPrepared(options, {
177
+ bytes: captured.bytes,
178
+ inspector: "glb",
179
+ dependencies: captured.dependencies,
180
+ inspection: { ...inspection, dependencies: captured.dependencies.map((item) => item.uri) },
181
+ assertSourceUnchanged: captured.assertUnchanged
182
+ }));
183
+ }
184
+ // Keep single-file GLB names and no-copy behavior unchanged, but use the
185
+ // captured bytes so an embedded document cannot turn into an unchecked
186
+ // multi-file document between inspection and copy.
187
+ return addAssetPrepared(options, undefined, captured);
188
+ }
189
+ let native = format === "ply" || format === "stl" ? inspectNativeMeshFile(sourcePath, format) : undefined;
190
+ if (format === "obj" && (options.type === undefined || options.type === "model")) {
191
+ try {
192
+ native = inspectNativeObjFile(sourcePath);
193
+ checkNativeAdmissionDestination(options, native);
194
+ }
195
+ catch (error) {
196
+ if (options.modelInspection === "strict")
197
+ throw error;
198
+ // Existing explicit OBJ admission remains usable during deprecation, but
199
+ // it is explicitly recorded as unverified rather than a parser pass.
200
+ native = undefined;
201
+ }
202
+ }
203
+ return addAssetPrepared(options, native);
204
+ }
205
+ /** Inspect asynchronous formats before publishing any asset, manifest or generated source. */
206
+ export async function addAssetAsync(options) {
207
+ const sourcePath = resolve(options.projectDir ?? process.cwd(), options.file);
208
+ const format = extname(sourcePath).slice(1).toLowerCase();
209
+ if (format === "pvr") {
210
+ // The parser is asynchronous, so retain the admission destination and
211
+ // supplied digest chosen when the operation began.
212
+ const admission = { ...options };
213
+ if (admission.type !== undefined && admission.type !== "texture") {
214
+ throw new Error("Aura3D assets add failed: .pvr must use --type texture.");
215
+ }
216
+ // Keep one bounded snapshot for inspection, hashing, and the staged copy.
217
+ // No GPU formats are claimed at admission time; the parser may use its
218
+ // supported CPU fallback or reject a format needing a device/transcoder.
219
+ const bytes = readBoundedNativeFile(sourcePath);
220
+ const { parseImageFormat } = await import("@aura3d/assets/importers");
221
+ const parsed = await parseImageFormat(bytes, "pvr", {
222
+ maxFileBytes: NATIVE_IMPORT_MAX_BYTES,
223
+ maxPixels: 16 * 1024 * 1024,
224
+ maxDecodedBytes: 64 * 1024 * 1024
225
+ });
226
+ const metadata = parsed.metadata.metadata;
227
+ const pixelFormat = parsed.metadata.pixelFormat;
228
+ const premultiplied = (typeof parsed.metadata.flags === "number" && (parsed.metadata.flags & 2) !== 0)
229
+ || (Array.isArray(pixelFormat) && pixelFormat[1] === 0 && (pixelFormat[0] === 8 || pixelFormat[0] === 10));
230
+ const formatWarnings = [
231
+ "PVR container inspection does not establish renderer, texture-upload, or device support.",
232
+ ...(parsed.levels.some((level) => level.encoding !== "rgba8" && level.encoding !== "rgba16")
233
+ ? ["The parsed PVR pixel encoding may not be supported by the current native texture bridge."] : []),
234
+ ...(metadata instanceof Uint8Array && metadata.byteLength > 0
235
+ ? ["The current native texture bridge does not support PVR orientation or other metadata."] : []),
236
+ ...(premultiplied ? ["Premultiplied PVR conversion supports opaque RGBA8 or UNORM16 pixels and transparent linear RGBA8 pixels; compressed GPU payloads and other cases may be rejected."] : []),
237
+ ...parsed.diagnostics
238
+ ];
239
+ return addAssetPrepared(admission, {
240
+ bytes,
241
+ inspector: "pvr",
242
+ dependencies: [],
243
+ inspection: { ...inspectAssetFile(sourcePath, format), nativeWarnings: formatWarnings }
244
+ });
245
+ }
246
+ if (format === "vox") {
247
+ const admission = {
248
+ ...options,
249
+ ...(options.provenanceEvidence ? { provenanceEvidence: [...options.provenanceEvidence] } : {})
250
+ };
251
+ if (admission.type !== undefined && admission.type !== "model") {
252
+ throw new Error("Aura3D assets add failed: .vox must use --type model.");
253
+ }
254
+ // The parser consumes one bounded snapshot; the same bytes are hashed and
255
+ // staged below even if the source file changes during the dynamic import.
256
+ const bytes = readBoundedNativeFile(sourcePath);
257
+ const { parseVOX } = await import("@aura3d/assets/importers");
258
+ const parsed = parseVOX(bytes, {
259
+ frame: 0,
260
+ maxFileBytes: NATIVE_IMPORT_MAX_BYTES,
261
+ maxOutputBytes: NATIVE_RENDER_BUFFER_MAX_BYTES,
262
+ maxVoxelCount: 1_000_000,
263
+ maxNodes: 100_000,
264
+ maxElements: 2_000_000,
265
+ maxListItems: 12_000_000
266
+ });
267
+ const prepared = prepareNativeMeshData(sourcePath, "vox", bytes, parsed.mesh, [
268
+ "VOX inspection uses frame 0 and flattens visible scene transforms into a Y-up mesh; animation and hierarchy are not retained by this adapter.",
269
+ ...parsed.diagnostics
270
+ ]);
271
+ return addAssetPrepared(admission, { ...prepared, formatMetadata: { version: String(parsed.version) } });
272
+ }
273
+ if (format === "nrrd" || format === "nhdr") {
274
+ const admission = {
275
+ ...options,
276
+ ...(options.provenanceEvidence ? { provenanceEvidence: [...options.provenanceEvidence] } : {})
277
+ };
278
+ if (admission.type !== undefined && admission.type !== "scalar-volume") {
279
+ throw new Error(`Aura3D assets add failed: .${format} must use --type scalar-volume.`);
280
+ }
281
+ // Read and retain the exact root and child snapshots before inspection;
282
+ // neither staged file is reread from the source after the parser yields.
283
+ const bytes = readBoundedNativeFile(sourcePath);
284
+ const reference = inspectNRRDDataReference(bytes, { maxBytes: NATIVE_IMPORT_MAX_BYTES });
285
+ const dependencies = [];
286
+ if (reference !== undefined) {
287
+ const path = safeNRRDDependencyPath(reference, `volume.${format}`);
288
+ const sourceDir = dirname(sourcePath);
289
+ const dependency = resolve(sourceDir, path);
290
+ assertNoNativeSymlinks(sourceDir, dependency);
291
+ const remaining = NATIVE_IMPORT_MAX_BYTES - bytes.byteLength;
292
+ if (remaining <= 0)
293
+ throw new Error("Aura3D assets add failed: NRRD root and dependency exceed the 64 MiB input budget.");
294
+ dependencies.push({ uri: reference, path, bytes: readBoundedNativeFile(dependency, remaining) });
295
+ }
296
+ const { parseNRRD, inspectScalarVolume } = await import("@aura3d/assets/importers");
297
+ const parsed = await parseNRRD(bytes, {
298
+ maxBytes: NATIVE_IMPORT_MAX_BYTES,
299
+ maxElements: 16_777_216,
300
+ maxOutputBytes: 256 * 1024 * 1024,
301
+ ...(dependencies[0] ? { detachedData: dependencies[0].bytes } : {})
302
+ });
303
+ const volume = inspectScalarVolume(parsed, { maxVoxels: 16_777_216 });
304
+ const inspection = inspectScalarVolumeBounds(sourcePath, format, volume.dataToLocal);
305
+ return withScalarVolumeAdmissionLock(resolve(admission.projectDir ?? process.cwd()), () => addAssetPrepared(admission, {
306
+ bytes, inspector: "nrrd", dependencies,
307
+ inspection: { ...inspection, dependencies: reference === undefined ? [] : [reference],
308
+ nativeWarnings: volume.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`) }
309
+ }));
310
+ }
311
+ if (NATIVE_CATALOG_MODEL_FORMATS.has(format)) {
312
+ if (options.type !== undefined && options.type !== "model")
313
+ throw new Error(`Aura3D assets add failed: .${format} must use --type model.`);
314
+ // One bounded snapshot is admitted by the format's real parser (and glTF
315
+ // projection) under the shared catalog limits, then hashed and staged.
316
+ const bytes = readBoundedNativeFile(sourcePath);
317
+ const { admitNativeFormat } = await import("@aura3d/assets/importers");
318
+ let admitted;
319
+ try {
320
+ admitted = await admitNativeFormat(bytes, format, { limits: { maxFileBytes: NATIVE_IMPORT_MAX_BYTES } });
321
+ }
322
+ catch (error) {
323
+ // An explicit legacy request keeps the deprecated, unverified path.
324
+ if (options.modelInspection === "legacy")
325
+ return addAsset(options);
326
+ throw new Error(`Aura3D assets add failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
327
+ }
328
+ const inspection = {
329
+ ...inspectAssetFile(sourcePath, format),
330
+ nativeWarnings: [
331
+ `${admitted.format} admitted by the bounded native parser: ${JSON.stringify(admitted.summary)}.`,
332
+ ...admitted.warnings
333
+ ]
334
+ };
335
+ return addAssetPrepared(options, { bytes, inspection, inspector: "native", dependencies: [] });
336
+ }
337
+ if (format !== "3mf")
338
+ return addAsset(options);
339
+ const bytes = readBoundedNativeFile(sourcePath);
340
+ const parsed = await parseModel3MF(bytes);
341
+ const inspection = {
342
+ ...inspectGltf(parsed.gltf),
343
+ nativeWarnings: parsed.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`)
344
+ };
345
+ return addAssetPrepared(options, { bytes, inspection, inspector: "3mf", dependencies: [] });
346
+ }
347
+ function scalarVolumeNamespace(format, rootHash, dependencies) {
348
+ const members = dependencies.map((item) => [item.uri, createHash("sha256").update(item.bytes).digest("hex")]);
349
+ const identity = createHash("sha256").update(JSON.stringify(["aura3d.scalar-volume/1", format, rootHash, members])).digest("hex");
350
+ return `scalar-volumes/${identity}`;
351
+ }
352
+ function sameScalarVolumeDependencies(previous, format, dependencies) {
353
+ if (previous.type !== "scalar-volume" || previous.format !== format || previous.integrity?.requireDependencies !== true)
354
+ return false;
355
+ const actual = previous.integrity.dependencies ?? {};
356
+ if (Object.keys(actual).length !== dependencies.length)
357
+ return false;
358
+ return dependencies.every((item) => {
359
+ const expected = createHash("sha256").update(item.bytes).digest("hex");
360
+ const found = actual[item.uri];
361
+ return typeof found === "string" && new RegExp(`^(?:sha256-)?${expected}$`, "i").test(found);
362
+ });
363
+ }
364
+ function glbBundleNamespace(rootHash, dependencies) {
365
+ return glbBundleNamespaceFromHashes(rootHash, dependencies.map((item) => ({
366
+ uri: item.uri,
367
+ hash: createHash("sha256").update(item.bytes).digest("hex")
368
+ })));
369
+ }
370
+ function glbBundleNamespaceFromHashes(rootHash, dependencies) {
371
+ const members = dependencies.map((item) => [item.uri, item.hash.replace(/^sha256-/i, "").toLowerCase()])
372
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
373
+ const identity = createHash("sha256").update(JSON.stringify(["aura3d.glb-bundle/1", rootHash.replace(/^sha256-/i, "").toLowerCase(), members])).digest("hex");
374
+ return `glb-bundles/${identity}`;
375
+ }
376
+ function glbBundleDocumentName(dependencies) {
377
+ const paths = dependencies.map((item) => item.path.normalize("NFC").toLowerCase());
378
+ for (let index = 0; index <= paths.length; index++) {
379
+ const candidate = index ? `document-${index}.glb` : "document.glb";
380
+ if (!paths.some((path) => path === candidate || path.startsWith(`${candidate}/`)))
381
+ return candidate;
382
+ }
383
+ throw new Error("Aura3D assets add failed: unable to choose a non-colliding GLB document name.");
384
+ }
385
+ function sameGlbDependencies(previous, dependencies) {
386
+ if (previous.format !== "glb")
387
+ return false;
388
+ const actual = previous.integrity?.dependencies ?? {};
389
+ if (Object.keys(actual).length !== dependencies.length ||
390
+ (dependencies.length > 0 && previous.integrity?.requireDependencies !== true))
391
+ return false;
392
+ return dependencies.every((item) => {
393
+ const expected = createHash("sha256").update(item.bytes).digest("hex");
394
+ const found = actual[item.uri];
395
+ return typeof found === "string" && new RegExp(`^(?:sha256-)?${expected}$`, "i").test(found);
396
+ });
397
+ }
398
+ function addAssetPrepared(options, native, embeddedGlb) {
153
399
  const projectDir = resolve(options.projectDir ?? process.cwd());
154
400
  const sourcePath = resolve(projectDir, options.file);
155
401
  if (!existsSync(sourcePath)) {
156
402
  throw new Error(`Aura3D assets add failed: "${options.file}" does not exist. Suggested fix: pass a real local GLB/glTF/texture path.`);
157
403
  }
158
404
  const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
405
+ // Capture the shared manifest before reading it; do not read through a link
406
+ // or overwrite another writer's changes made during this admission.
407
+ const priorGlbManifest = native?.inspector === "glb" ? captureGlbPublicationTarget(projectDir, manifestPath) : undefined;
159
408
  const current = readAssetManifest(projectDir);
409
+ if (native?.inspector === "glb")
410
+ assertGlbPublicationTarget(projectDir, manifestPath, priorGlbManifest);
160
411
  // Re-adding (e.g. resolve overwriting) must not silently drop hand-authored
161
412
  // provenance the new add does not re-supply (license/attribution/author/etc.).
162
- const existing = current.assets.find((asset) => asset.id === options.name);
413
+ const previous = current.assets.find((asset) => asset.id === options.name);
163
414
  const outputDir = normalizeRelativePath(options.outputDir ?? current.outputDir ?? DEFAULT_AURA_ASSET_OUTPUT_DIR);
164
415
  const publicPath = normalizePublicPath(options.publicPath ?? current.assetBasePath ?? DEFAULT_AURA_ASSET_PUBLIC_PATH);
165
416
  const typegen = normalizeRelativePath(options.typegen ?? current.typegen ?? DEFAULT_AURA_ASSET_TYPEGEN);
166
- const hash = hashFile(sourcePath);
167
417
  const format = extname(sourcePath).slice(1).toLowerCase();
168
418
  const type = options.type ?? inferAssetType(format);
419
+ if (options.modelInspection !== undefined && options.modelInspection !== "legacy" && options.modelInspection !== "strict") {
420
+ throw new Error("Aura3D assets add failed: modelInspection must be legacy or strict.");
421
+ }
422
+ if (native && native.inspector === "pvr" && type !== "texture")
423
+ throw new Error("Aura3D assets add failed: .pvr must use --type texture.");
424
+ if (native && native.inspector === "nrrd" && type !== "scalar-volume")
425
+ throw new Error(`Aura3D assets add failed: .${format} must use --type scalar-volume.`);
426
+ if (native && native.inspector !== "pvr" && native.inspector !== "nrrd" && type !== "model")
427
+ throw new Error(`Aura3D assets add failed: .${format} must use --type model.`);
428
+ if (type === "model" && options.modelInspection === "strict" && (!native || native.inspector === "glb")) {
429
+ throw new Error(`Aura3D assets add failed: strict bounded parser inspection is unavailable for .${format || "unknown"}; glTF currently has metadata-only inspection.`);
430
+ }
431
+ if (native)
432
+ validateNativeAdmissionPaths(projectDir, options.name, outputDir, typegen, publicPath);
433
+ if (native?.inspector === "glb")
434
+ validateGlbPortablePaths(outputDir, typegen, publicPath);
435
+ const snapshot = native?.bytes ?? embeddedGlb?.bytes;
436
+ const hash = snapshot ? createHash("sha256").update(snapshot).digest("hex") : hashFile(sourcePath);
437
+ if (native && options.sha256 !== undefined && !new RegExp(`^(?:sha256-)?${hash}$`, "i").test(options.sha256)) {
438
+ throw new Error("Aura3D assets add failed: supplied provenance SHA-256 does not match the inspected asset bytes.");
439
+ }
440
+ // Rights, render probes and quality from an earlier file do not attest to a
441
+ // different native asset merely because it is admitted under the same id.
442
+ const changedRoot = native && previous && (previous.hash ?? previous.sha256 ?? "").replace(/^sha256-/i, "").toLowerCase() !== hash;
443
+ const changedVolume = native?.inspector === "nrrd" && previous && !sameScalarVolumeDependencies(previous, format, native.dependencies);
444
+ const glbDependencies = native?.inspector === "glb" ? native.dependencies : embeddedGlb?.dependencies;
445
+ const previousGlbRootMatches = previous && (previous.hash ?? previous.sha256 ?? "").replace(/^sha256-/i, "").toLowerCase() === hash;
446
+ const changedGlb = glbDependencies && previous && (previous.type !== type || !previousGlbRootMatches ||
447
+ !sameGlbDependencies(previous, glbDependencies));
448
+ const existing = changedRoot || changedVolume || changedGlb ? undefined : previous;
449
+ const priorProvenance = (native || embeddedGlb) && existing?.provenance?.sha256 &&
450
+ !new RegExp(`^(?:sha256-)?${hash}$`, "i").test(existing.provenance.sha256)
451
+ ? { ...existing.provenance, sha256: undefined }
452
+ : existing?.provenance;
169
453
  if (type === "lut")
170
454
  admitCubeLut(sourcePath, format);
171
- const inspection = inspectAssetFile(sourcePath, format);
172
- const outputFileName = `${options.name}.${hash.slice(0, 8)}.${format}`;
455
+ const inspection = native?.inspection ?? (embeddedGlb ? inspectGlb(embeddedGlb.bytes) : inspectAssetFile(sourcePath, format));
456
+ // An unchanged root cannot supply fresh rights evidence for substituted
457
+ // sidecars (or an incomplete prior bundle). On later re-adds, use only the
458
+ // already identity-bound provenance or explicitly supplied new evidence;
459
+ // otherwise the same embedded text would silently resurrect stale rights.
460
+ const detectedProvenance = native?.inspector === "glb" && previousGlbRootMatches
461
+ ? undefined
462
+ : inspection.provenance;
463
+ // Keep a detached header and its unmodified relative child under an
464
+ // immutable namespace bound to both snapshots, not just the header hash.
465
+ const namespace = native?.inspector === "nrrd" ? scalarVolumeNamespace(format, hash, native.dependencies)
466
+ : native?.inspector === "glb" ? glbBundleNamespace(hash, native.dependencies) : undefined;
467
+ const outputFileName = namespace
468
+ ? `${namespace}/${native?.inspector === "glb" ? glbBundleDocumentName(native.dependencies) : `volume.${format}`}`
469
+ : `${options.name}.${hash.slice(0, 8)}.${format}`;
173
470
  const outputPath = join(outputDir, outputFileName);
174
- if (options.copy !== false) {
175
- mkdirSync(resolve(projectDir, outputDir), { recursive: true });
176
- copyFileSync(sourcePath, resolve(projectDir, outputPath));
177
- copyAssetDependencies(projectDir, sourcePath, outputDir, inspection.dependencies);
471
+ const stagedNative = native && namespace ? {
472
+ ...native,
473
+ dependencies: native.dependencies.map((item) => ({ ...item, path: `${namespace}/${item.path}` }))
474
+ } : native;
475
+ if (stagedNative?.inspector === "nrrd") {
476
+ assertScalarVolumeTargetLayout(projectDir, outputPath, outputDir, typegen, options.name, stagedNative.dependencies);
477
+ }
478
+ const glbTargets = native?.inspector === "glb" && stagedNative && namespace
479
+ ? assertGlbTargetLayout(projectDir, sourcePath, outputPath, outputDir, typegen, options.name, namespace, native.dependencies, stagedNative.dependencies, current, priorGlbManifest)
480
+ : undefined;
481
+ if (native && options.copy === false) {
482
+ const stagedPath = resolve(projectDir, outputPath);
483
+ assertNoNativeSymlinks(projectDir, stagedPath);
484
+ if (native.inspector === "glb")
485
+ assertStagedGlbFile(projectDir, stagedPath, native.bytes, "copy:false requires the matching hashed file at the output path");
486
+ else if (!existsSync(stagedPath) || !statSync(stagedPath).isFile() || statSync(stagedPath).size !== native.bytes.length || !readBoundedNativeFile(stagedPath).equals(native.bytes)) {
487
+ throw new Error("Aura3D assets add failed: copy:false requires the matching hashed file to already exist at the output path.");
488
+ }
489
+ for (const dependency of stagedNative.dependencies) {
490
+ const stagedDependency = resolve(projectDir, outputDir, dependency.path);
491
+ assertNoNativeSymlinks(projectDir, stagedDependency);
492
+ if (native.inspector === "glb")
493
+ assertStagedGlbFile(projectDir, stagedDependency, dependency.bytes, `copy:false requires the matching dependency ${dependency.uri} at the output path`);
494
+ else if (!existsSync(stagedDependency) || !statSync(stagedDependency).isFile() || statSync(stagedDependency).size !== dependency.bytes.length || !readBoundedNativeFile(stagedDependency).equals(dependency.bytes)) {
495
+ throw new Error(`Aura3D assets add failed: copy:false requires the matching dependency ${dependency.uri} at the output path.`);
496
+ }
497
+ }
178
498
  }
179
- const thumbnailUrl = writeThumbnail(projectDir, outputDir, publicPath, options.name, inspection.bounds);
499
+ const thumbnailUrl = `${publicPath}${options.name}.thumb.svg`;
180
500
  const entry = {
181
501
  id: options.name,
182
502
  type,
183
503
  format,
504
+ ...(native?.formatMetadata ? { formatMetadata: native.formatMetadata } : {}),
505
+ ...(type === "model" ? {
506
+ modelInspection: native && native.inspector !== "pvr" && native.inspector !== "nrrd" && native.inspector !== "glb"
507
+ ? { version: 1, status: "parser-inspected", inspector: (native.inspector === "native" ? format : native.inspector), digest: `sha256-${hash}` }
508
+ : { version: 1, status: (format === "glb" || format === "gltf" ? "metadata-only" : "legacy-unverified") }
509
+ } : {}),
510
+ ...(native ? { integrity: {
511
+ version: 1,
512
+ digest: `sha256-${hash}`,
513
+ ...(native.inspector === "obj" || native.inspector === "nrrd" || native.dependencies.length ? { dependencies: Object.fromEntries(native.dependencies.map((item) => [item.uri, `sha256-${createHash("sha256").update(item.bytes).digest("hex")}`])), requireDependencies: true } : {})
514
+ } } : {}),
184
515
  /*
185
516
  * Record a source path that still exists after the command finishes.
186
517
  *
@@ -198,7 +529,7 @@ export function addAsset(options) {
198
529
  outputPath: normalizeRelativePath(outputPath),
199
530
  url: `${publicPath}${outputFileName}`,
200
531
  hash: `sha256-${hash}`,
201
- sizeBytes: statSync(sourcePath).size,
532
+ sizeBytes: snapshot?.byteLength ?? statSync(sourcePath).size,
202
533
  bounds: inspection.bounds,
203
534
  boundsMetadata: inspection.boundsMetadata,
204
535
  materials: inspection.materials,
@@ -209,7 +540,7 @@ export function addAsset(options) {
209
540
  skeleton: inspection.skeleton,
210
541
  morphTargets: inspection.morphTargets,
211
542
  hierarchy: inspection.hierarchy,
212
- provenance: createAssetProvenance(projectDir, sourcePath, options, options.replaceProvenanceEvidence ? inspection.provenance : mergeDetectedProvenance(existing?.provenance, inspection.provenance), outputPath),
543
+ provenance: createAssetProvenance(projectDir, sourcePath, options, options.replaceProvenanceEvidence ? detectedProvenance : mergeDetectedProvenance(priorProvenance, detectedProvenance), outputPath),
213
544
  textures: inspection.textures,
214
545
  dependencies: inspection.dependencies,
215
546
  orientation: options.orientation ?? (existing?.orientation?.source === "manifest-override" && existing.orientation.assetHash === `sha256-${hash}`
@@ -245,7 +576,13 @@ export function addAsset(options) {
245
576
  ...(c17AnimationClips(inspection.animation) ?? existing?.animationClips
246
577
  ? { animationClips: c17AnimationClips(inspection.animation) ?? existing?.animationClips }
247
578
  : {}),
248
- warnings: createAssetWarnings(sourcePath, inspection)
579
+ warnings: [
580
+ ...createAssetWarnings(sourcePath, inspection, snapshot?.byteLength),
581
+ ...(inspection.nativeWarnings ?? []),
582
+ ...(type === "model" && !native && format !== "gltf" && format !== "glb"
583
+ ? [`Legacy model admission: .${format || "unknown"} bytes were not parser-inspected; this behavior is deprecated and does not establish adapter or format support.`]
584
+ : [])
585
+ ]
249
586
  };
250
587
  if (options.orientation) {
251
588
  if (options.orientation.source !== "manifest-override") {
@@ -268,8 +605,30 @@ export function addAsset(options) {
268
605
  entry
269
606
  ]
270
607
  });
271
- writeAssetManifest(projectDir, manifest);
272
- writeTypedAssets(projectDir, manifest);
608
+ if (native) {
609
+ const glbBeforeCommit = native.inspector === "glb" ? () => {
610
+ native.assertSourceUnchanged?.();
611
+ assertStagedGlbFile(projectDir, resolve(projectDir, outputPath), native.bytes, "staged GLB changed during admission");
612
+ for (const dependency of stagedNative.dependencies) {
613
+ assertStagedGlbFile(projectDir, resolve(projectDir, outputDir, dependency.path), dependency.bytes, `staged GLB dependency ${dependency.uri} changed during admission`);
614
+ }
615
+ } : undefined;
616
+ publishNativeAdmission(projectDir, manifest, outputPath, sourcePath, options.name, inspection.bounds, options.copy === false ? undefined : stagedNative, glbBeforeCommit, glbTargets);
617
+ }
618
+ else {
619
+ embeddedGlb?.assertUnchanged();
620
+ if (options.copy !== false) {
621
+ mkdirSync(resolve(projectDir, outputDir), { recursive: true });
622
+ if (embeddedGlb)
623
+ writeFileSync(resolve(projectDir, outputPath), embeddedGlb.bytes);
624
+ else
625
+ copyFileSync(sourcePath, resolve(projectDir, outputPath));
626
+ copyAssetDependencies(projectDir, sourcePath, outputDir, inspection.dependencies);
627
+ }
628
+ writeThumbnail(projectDir, outputDir, publicPath, options.name, inspection.bounds);
629
+ writeAssetManifest(projectDir, manifest);
630
+ writeTypedAssets(projectDir, manifest);
631
+ }
273
632
  return {
274
633
  ok: true,
275
634
  manifestPath,
@@ -277,7 +636,11 @@ export function addAsset(options) {
277
636
  messages: [
278
637
  `Added ${entry.id} -> ${entry.url}`,
279
638
  `Wrote ${DEFAULT_AURA_ASSET_MANIFEST}`,
280
- `Wrote ${manifest.typegen}`
639
+ `Wrote ${manifest.typegen}`,
640
+ ...(native && native.inspector !== "pvr" && native.inspector !== "nrrd" && native.inspector !== "glb" ? [`Register a renderer-capable ${format.toUpperCase()} adapter in the app's FormatRegistry before loading this asset.`] : []),
641
+ ...(native?.inspector === "pvr" ? ["PVR bytes were parser-inspected; texture upload and device support have not been established by admission."] : []),
642
+ ...(native?.inspector === "obj" ? ["OBJ inspection covers geometry, material references, and image signatures; image decoding and rendered fidelity remain unverified."] : []),
643
+ ...(type === "model" && !native && format !== "gltf" && format !== "glb" ? ["Legacy model admission is unverified and deprecated; use --model-inspection strict after a bounded parser is available."] : [])
281
644
  ]
282
645
  };
283
646
  }
@@ -756,9 +1119,64 @@ export function validateAssets(options = {}) {
756
1119
  failures.push(`Missing asset output for "${asset.id}": ${asset.outputPath}`);
757
1120
  continue;
758
1121
  }
759
- const actualHash = `sha256-${hashFile(outputPath)}`;
760
- if (actualHash !== asset.hash)
761
- failures.push(`Hash mismatch for "${asset.id}": expected ${asset.hash}, found ${actualHash}`);
1122
+ let scalarRoot;
1123
+ let glbJson;
1124
+ let actualHash;
1125
+ if (asset.format === "glb") {
1126
+ try {
1127
+ const checked = checkGlbDependencies(projectDir, outputPath, asset);
1128
+ actualHash = checked.hash;
1129
+ glbJson = checked.json;
1130
+ failures.push(...checked.failures);
1131
+ validateGlbBundleNamespace(projectDir, outputPath, asset, checked, failures);
1132
+ }
1133
+ catch (error) {
1134
+ failures.push(`${asset.id}: GLB dependency validation failed: ${error instanceof Error ? error.message : String(error)}`);
1135
+ continue;
1136
+ }
1137
+ }
1138
+ else if (asset.type === "scalar-volume" && (asset.format === "nrrd" || asset.format === "nhdr")) {
1139
+ try {
1140
+ assertNoNativeSymlinks(projectDir, outputPath);
1141
+ scalarRoot = readBoundedNativeFile(outputPath);
1142
+ actualHash = `sha256-${createHash("sha256").update(scalarRoot).digest("hex")}`;
1143
+ }
1144
+ catch (error) {
1145
+ failures.push(`${asset.id}: asset output could not be read safely: ${error instanceof Error ? error.message : String(error)}`);
1146
+ continue;
1147
+ }
1148
+ }
1149
+ else
1150
+ actualHash = `sha256-${hashFile(outputPath)}`;
1151
+ const expectedHash = asset.format === "glb" ? asset.integrity?.digest ?? asset.hash ?? asset.sha256 : asset.hash;
1152
+ if (actualHash !== expectedHash)
1153
+ failures.push(`Hash mismatch for "${asset.id}": expected ${expectedHash}, found ${actualHash}`);
1154
+ if (scalarRoot)
1155
+ failures.push(...validateScalarVolumeDependencies(projectDir, manifest.outputDir, asset, outputPath, scalarRoot));
1156
+ if (asset.format === "obj" && asset.modelInspection?.status === "parser-inspected") {
1157
+ if (!asset.integrity?.requireDependencies)
1158
+ failures.push(`${asset.id}: inspected OBJ must require dependency integrity.`);
1159
+ const digests = asset.integrity?.dependencies ?? {};
1160
+ for (const uri of asset.dependencies ?? []) {
1161
+ if (!Object.hasOwn(digests, uri))
1162
+ failures.push(`${asset.id}: missing dependency integrity for ${uri}.`);
1163
+ }
1164
+ for (const [uri, digest] of Object.entries(digests)) {
1165
+ try {
1166
+ const url = new URL(uri, "https://aura3d.invalid/project/model.obj");
1167
+ const { path } = nativeObjDependencyPath(url);
1168
+ const dependency = resolve(dirname(outputPath), path);
1169
+ assertNoNativeSymlinks(projectDir, dependency);
1170
+ if (!existsSync(dependency) || !statSync(dependency).isFile())
1171
+ failures.push(`${asset.id}: missing OBJ dependency ${uri}.`);
1172
+ else if (`sha256-${createHash("sha256").update(readBoundedNativeFile(dependency)).digest("hex")}` !== digest.toLowerCase())
1173
+ failures.push(`${asset.id}: OBJ dependency hash mismatch for ${uri}.`);
1174
+ }
1175
+ catch {
1176
+ failures.push(`${asset.id}: invalid or unsafe OBJ dependency ${uri}.`);
1177
+ }
1178
+ }
1179
+ }
762
1180
  const provenance = resolveAssetProvenance(asset, externalProvenance);
763
1181
  if (noPlaceholders && isPlaceholderAsset(asset, provenance)) {
764
1182
  failures.push(`Placeholder asset is not allowed in strict release validation: "${asset.id}". Replace it with a real typed asset and provenance.`);
@@ -793,7 +1211,7 @@ export function validateAssets(options = {}) {
793
1211
  skinAllow ??= readSkinAllowlist(projectDir);
794
1212
  failures.push(...skinReleaseFailures(projectDir, asset, skinAllow).failures);
795
1213
  }
796
- warnings.push(...createDerivedMetadataDriftWarnings(outputPath, asset));
1214
+ warnings.push(...createDerivedMetadataDriftWarnings(outputPath, asset, glbJson));
797
1215
  if (release)
798
1216
  warnings.push(...createReleaseStructuredQualityWarnings(projectDir, asset));
799
1217
  if (release)
@@ -846,6 +1264,80 @@ export function validateAssets(options = {}) {
846
1264
  messages: failures.length === 0 ? [release ? "Asset manifest is release-valid." : "Asset manifest is valid."] : failures
847
1265
  };
848
1266
  }
1267
+ /** A bundle path is an immutable identity of the actual document and sidecars. */
1268
+ function validateGlbBundleNamespace(projectDir, outputPath, asset, checked, failures) {
1269
+ const parts = relative(projectDir, outputPath).split(sep);
1270
+ const marker = parts.lastIndexOf("glb-bundles");
1271
+ if (marker < 0 || parts[0] === ".." || isAbsolute(relative(projectDir, outputPath)))
1272
+ return;
1273
+ // Only reserve the layout emitted by admission. An unrelated legacy asset
1274
+ // may happen to live in a directory named "glb-bundles".
1275
+ if (parts.length !== marker + 3 || !/^[a-f0-9]{64}$/.test(parts[marker + 1] ?? "") ||
1276
+ !/^document(?:-[1-9][0-9]*)?\.glb$/.test(parts[marker + 2] ?? ""))
1277
+ return;
1278
+ const recorded = asset.integrity?.dependencies ?? {};
1279
+ if (!checked.uris.length || asset.integrity?.requireDependencies !== true ||
1280
+ Object.keys(recorded).length !== checked.uris.length ||
1281
+ checked.uris.some((uri) => !Object.hasOwn(recorded, uri))) {
1282
+ failures.push(`${asset.id}: GLB bundle dependency integrity must exactly match the document references.`);
1283
+ }
1284
+ // Missing or unreadable children already have their own failures; never
1285
+ // invent bytes or calculate a namespace from an incomplete bundle.
1286
+ if (checked.references.length !== checked.uris.length)
1287
+ return;
1288
+ const outputDir = resolve(projectDir, ...parts.slice(0, marker));
1289
+ const expected = resolve(outputDir, glbBundleNamespaceFromHashes(checked.hash, checked.references), glbBundleDocumentName(checked.references));
1290
+ if (expected !== outputPath)
1291
+ failures.push(`${asset.id}: GLB bundle namespace does not match its root and dependency bytes.`);
1292
+ }
1293
+ /** Re-read the declared child reference from the same root bytes being hashed. */
1294
+ function validateScalarVolumeDependencies(projectDir, outputDir, asset, outputPath, root) {
1295
+ const failures = [];
1296
+ try {
1297
+ const reference = inspectNRRDDataReference(root, { maxBytes: NATIVE_IMPORT_MAX_BYTES });
1298
+ const integrity = asset.integrity;
1299
+ const digests = integrity?.dependencies ?? {};
1300
+ const keys = Object.keys(digests);
1301
+ const listed = asset.dependencies ?? [];
1302
+ if (integrity?.requireDependencies !== true)
1303
+ failures.push(`${asset.id}: scalar volume must require dependency integrity.`);
1304
+ if (reference === undefined) {
1305
+ if (keys.length || listed.length)
1306
+ failures.push(`${asset.id}: embedded NRRD has unexpected dependency metadata.`);
1307
+ validateScalarVolumeNamespace(projectDir, outputDir, asset, outputPath, root, [], failures);
1308
+ return failures;
1309
+ }
1310
+ const path = safeNRRDDependencyPath(reference, basename(outputPath));
1311
+ if (keys.length !== 1 || !Object.hasOwn(digests, reference) || listed.length !== 1 || listed[0] !== reference) {
1312
+ failures.push(`${asset.id}: NRRD dependency metadata does not match the declared reference.`);
1313
+ return failures;
1314
+ }
1315
+ const child = resolve(dirname(outputPath), path);
1316
+ assertNoNativeSymlinks(projectDir, child);
1317
+ const remaining = NATIVE_IMPORT_MAX_BYTES - root.byteLength;
1318
+ if (remaining <= 0)
1319
+ throw new Error("NRRD root and dependency exceed the 64 MiB input budget");
1320
+ const bytes = readBoundedNativeFile(child, remaining);
1321
+ const digest = `sha256-${createHash("sha256").update(bytes).digest("hex")}`;
1322
+ if (digest !== digests[reference].toLowerCase())
1323
+ failures.push(`${asset.id}: NRRD dependency hash mismatch for ${reference}.`);
1324
+ validateScalarVolumeNamespace(projectDir, outputDir, asset, outputPath, root, [{ uri: reference, path, bytes }], failures);
1325
+ }
1326
+ catch (error) {
1327
+ failures.push(`${asset.id}: NRRD dependency validation failed: ${error instanceof Error ? error.message : String(error)}`);
1328
+ }
1329
+ return failures;
1330
+ }
1331
+ function validateScalarVolumeNamespace(projectDir, outputDir, asset, outputPath, root, dependencies, failures) {
1332
+ const base = resolve(projectDir, outputDir, "scalar-volumes");
1333
+ const rel = relative(base, outputPath);
1334
+ if (rel.startsWith(`..${sep}`) || rel === ".." || isAbsolute(rel))
1335
+ return;
1336
+ const rootHash = createHash("sha256").update(root).digest("hex");
1337
+ const expected = resolve(projectDir, outputDir, scalarVolumeNamespace(asset.format, rootHash, dependencies), `volume.${asset.format}`);
1338
+ if (expected !== outputPath)
1339
+ failures.push(`${asset.id}: scalar-volume namespace does not match its root and dependency bytes.`);
1340
+ }
849
1341
  export function createAssetThumbnails(options = {}) {
850
1342
  const projectDir = resolve(options.projectDir ?? process.cwd());
851
1343
  const manifest = readAssetManifest(projectDir);
@@ -1798,17 +2290,23 @@ function pushUnique(target, values) {
1798
2290
  function createDuplicateHashWarnings(manifest, externalProvenance) {
1799
2291
  const byHash = new Map();
1800
2292
  for (const asset of manifest.assets) {
1801
- const existing = byHash.get(asset.hash) ?? [];
2293
+ // A document can be byte-identical while its referenced resources differ.
2294
+ // For admitted multi-file assets, the complete identity defines duplication.
2295
+ const multiFile = asset.type === "scalar-volume" || (asset.format === "glb" && asset.integrity?.requireDependencies === true);
2296
+ const key = multiFile
2297
+ ? JSON.stringify([asset.type, asset.format, asset.hash, Object.entries(asset.integrity?.dependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))])
2298
+ : JSON.stringify(["root", asset.hash]);
2299
+ const existing = byHash.get(key) ?? [];
1802
2300
  existing.push(asset);
1803
- byHash.set(asset.hash, existing);
2301
+ byHash.set(key, existing);
1804
2302
  }
1805
2303
  const warnings = [];
1806
- for (const [hash, assets] of byHash) {
2304
+ for (const assets of byHash.values()) {
1807
2305
  if (assets.length < 2)
1808
2306
  continue;
1809
2307
  const unexplained = assets.filter((asset) => !hasDuplicateHashAllowlist(resolveAssetProvenance(asset, externalProvenance)));
1810
2308
  if (unexplained.length > 0) {
1811
- warnings.push(`duplicate asset hash ${hash} used by ${assets.map((asset) => `"${asset.id}"`).join(", ")} without duplicate-ok provenance evidence.`);
2309
+ warnings.push(`duplicate asset hash ${assets[0].hash} used by ${assets.map((asset) => `"${asset.id}"`).join(", ")} without duplicate-ok provenance evidence.`);
1812
2310
  }
1813
2311
  }
1814
2312
  return warnings;
@@ -1873,6 +2371,495 @@ function defaultAttachPoint(slot) {
1873
2371
  return "spine";
1874
2372
  return "root";
1875
2373
  }
2374
+ const NATIVE_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
2375
+ const NATIVE_RENDER_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
2376
+ function readBoundedNativeFile(path, maxBytes = NATIVE_IMPORT_MAX_BYTES) {
2377
+ if (!existsSync(path))
2378
+ throw new Error(`Aura3D assets add failed: "${path}" does not exist.`);
2379
+ const descriptor = openSync(path, "r");
2380
+ try {
2381
+ const stat = fstatSync(descriptor);
2382
+ if (!stat.isFile() || stat.size > maxBytes) {
2383
+ throw new Error(`Aura3D assets add failed: native asset must be a regular file within the ${maxBytes}-byte remaining input budget (at most 64 MiB).`);
2384
+ }
2385
+ const bytes = Buffer.allocUnsafe(stat.size);
2386
+ let offset = 0;
2387
+ while (offset < bytes.length) {
2388
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset);
2389
+ if (!count)
2390
+ throw new Error("Aura3D assets add failed: native file changed while being read.");
2391
+ offset += count;
2392
+ }
2393
+ const extra = Buffer.alloc(1);
2394
+ if (readSync(descriptor, extra, 0, 1, offset) !== 0)
2395
+ throw new Error("Aura3D assets add failed: native file grew while being read.");
2396
+ return bytes;
2397
+ }
2398
+ finally {
2399
+ closeSync(descriptor);
2400
+ }
2401
+ }
2402
+ /** Only local, portable relative NRRD children can be staged unchanged. */
2403
+ function safeNRRDDependencyPath(reference, rootName) {
2404
+ if (!reference || reference.length > 2048 || reference.normalize("NFC") !== reference ||
2405
+ isAbsolute(reference) || reference.startsWith("/") || /[\0-\x1f\x7f\\?#%:]/.test(reference)) {
2406
+ throw new Error("Aura3D assets add failed: NRRD dependency must be a safe local relative path.");
2407
+ }
2408
+ const segments = reference.split("/");
2409
+ if (segments.some((part) => !part || part === "." || part === ".." || part.endsWith(".") || part.endsWith(" ") ||
2410
+ Buffer.byteLength(part, "utf8") > 255 || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
2411
+ throw new Error("Aura3D assets add failed: NRRD dependency path is not portable or contains traversal.");
2412
+ }
2413
+ if (segments.length === 1 && reference.toLowerCase() === rootName.toLowerCase()) {
2414
+ throw new Error("Aura3D assets add failed: NRRD dependency collides with its staged header.");
2415
+ }
2416
+ return reference;
2417
+ }
2418
+ function assertScalarVolumeTargetLayout(projectDir, outputPath, outputDir, typegen, name, dependencies) {
2419
+ const targets = [
2420
+ resolve(projectDir, outputPath),
2421
+ ...dependencies.map((item) => resolve(projectDir, outputDir, item.path)),
2422
+ resolve(projectDir, outputDir, `${name}.thumb.svg`),
2423
+ resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
2424
+ resolve(projectDir, typegen),
2425
+ resolve(projectDir, ".aura3d-scalar-admission.lock")
2426
+ ].map((path) => path.normalize("NFC").replaceAll("\\", "/").toLowerCase());
2427
+ for (let i = 0; i < targets.length; i++)
2428
+ for (let j = i + 1; j < targets.length; j++) {
2429
+ const a = targets[i], b = targets[j];
2430
+ if (a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)) {
2431
+ throw new Error("Aura3D assets add failed: scalar-volume output, dependency, thumbnail, manifest and typegen paths collide.");
2432
+ }
2433
+ }
2434
+ }
2435
+ function captureGlbPublicationTarget(projectDir, path) {
2436
+ assertNoNativeSymlinks(projectDir, path);
2437
+ try {
2438
+ const stat = lstatSync(path);
2439
+ if (!stat.isFile())
2440
+ throw new Error(`Aura3D assets add failed: generated output is not a regular file: ${path}`);
2441
+ return stat;
2442
+ }
2443
+ catch (error) {
2444
+ if (error.code === "ENOENT")
2445
+ return undefined;
2446
+ throw error;
2447
+ }
2448
+ }
2449
+ function validateGlbPortablePaths(outputDir, typegen, publicPath) {
2450
+ for (const [label, path] of [["output", outputDir], ["typegen", typegen]]) {
2451
+ if (path.split("/").some((part) => /[\u0000-\u001f\u007f\uD800-\uDFFF<>:"|*]/u.test(part) ||
2452
+ part.endsWith(".") || part.endsWith(" ") || Buffer.byteLength(part, "utf8") > 255 ||
2453
+ /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
2454
+ throw new Error(`Aura3D assets add failed: GLB ${label} path contains a nonportable name.`);
2455
+ }
2456
+ }
2457
+ if (/[\u0000-\u001f\u007f\uD800-\uDFFF]/u.test(publicPath)) {
2458
+ throw new Error("Aura3D assets add failed: GLB public path contains an unsafe character.");
2459
+ }
2460
+ }
2461
+ function sameGlbPublicationTarget(left, right) {
2462
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size &&
2463
+ left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
2464
+ }
2465
+ function assertGlbPublicationTarget(projectDir, path, expected) {
2466
+ const actual = captureGlbPublicationTarget(projectDir, path);
2467
+ if ((!expected && actual) || (expected && (!actual || !sameGlbPublicationTarget(expected, actual)))) {
2468
+ throw new Error(`Aura3D assets add failed: generated output changed or was created during GLB admission: ${path}`);
2469
+ }
2470
+ }
2471
+ function assertGlbTargetLayout(projectDir, sourcePath, outputPath, outputDir, typegen, name, namespace, dependencies, stagedDependencies, current, priorManifest) {
2472
+ const canonicalProject = realpathSync(projectDir);
2473
+ const projectPath = (path) => resolve(canonicalProject, relative(projectDir, path));
2474
+ const folded = (path) => path.normalize("NFC").replaceAll("\\", "/").toLowerCase();
2475
+ const overlaps = (left, right) => left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
2476
+ const assetPaths = [...new Set([
2477
+ resolve(projectDir, outputPath),
2478
+ ...stagedDependencies.map((item) => resolve(projectDir, outputDir, item.path))
2479
+ ])].map((path) => folded(projectPath(path)));
2480
+ const generatedFiles = [
2481
+ resolve(projectDir, outputDir, `${name}.thumb.svg`),
2482
+ resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
2483
+ resolve(projectDir, typegen),
2484
+ resolve(projectDir, ".aura3d-scalar-admission.lock")
2485
+ ];
2486
+ const generatedPaths = generatedFiles.map((path) => folded(projectPath(path)));
2487
+ const generatedSnapshots = new Map(generatedFiles.slice(0, 3).map((path) => [path, captureGlbPublicationTarget(projectDir, path)]));
2488
+ assertGlbPublicationTarget(projectDir, generatedFiles[1], priorManifest);
2489
+ const targets = [...assetPaths, ...generatedPaths];
2490
+ for (let left = 0; left < targets.length; left++)
2491
+ for (let right = left + 1; right < targets.length; right++) {
2492
+ if (overlaps(targets[left], targets[right])) {
2493
+ throw new Error("Aura3D assets add failed: GLB output, dependency, thumbnail, manifest, typegen or lock paths collide.");
2494
+ }
2495
+ }
2496
+ const bundleDir = folded(projectPath(resolve(projectDir, outputDir, namespace)));
2497
+ if (generatedPaths.some((path) => overlaps(path, bundleDir))) {
2498
+ throw new Error("Aura3D assets add failed: generated files cannot be placed inside a GLB bundle namespace.");
2499
+ }
2500
+ const sourcePaths = [sourcePath, ...dependencies.map((item) => resolve(dirname(sourcePath), item.path))]
2501
+ .map((path) => folded(realpathSync(path)));
2502
+ if (sourcePaths.some((source) => generatedPaths.some((target) => overlaps(source, target)))) {
2503
+ throw new Error("Aura3D assets add failed: generated files cannot overwrite a GLB source or dependency.");
2504
+ }
2505
+ const previous = current.assets.find((asset) => asset.id === name);
2506
+ const previousThumbnail = previous?.thumbnailUrl && resolvePublicArtifactPath(projectDir, current, previous.thumbnailUrl);
2507
+ if (generatedSnapshots.get(generatedFiles[0]) &&
2508
+ (!previousThumbnail || folded(projectPath(previousThumbnail)) !== generatedPaths[0])) {
2509
+ throw new Error("Aura3D assets add failed: cannot overwrite an existing unowned thumbnail.");
2510
+ }
2511
+ if (generatedSnapshots.get(generatedFiles[2]) &&
2512
+ (!priorManifest || folded(projectPath(resolve(projectDir, current.typegen))) !== generatedPaths[2])) {
2513
+ throw new Error("Aura3D assets add failed: cannot overwrite an existing unowned typed asset module.");
2514
+ }
2515
+ for (const asset of current.assets) {
2516
+ const priorOutput = resolve(projectDir, asset.outputPath);
2517
+ const priorPath = folded(projectPath(priorOutput));
2518
+ if (generatedPaths.some((target) => overlaps(target, priorPath))) {
2519
+ throw new Error("Aura3D assets add failed: generated files cannot overwrite an existing asset.");
2520
+ }
2521
+ const priorDir = dirname(priorOutput);
2522
+ if (["glb-bundles", "scalar-volumes"].includes(basename(dirname(priorDir))) &&
2523
+ /^[a-f0-9]{64}$/.test(basename(priorDir))) {
2524
+ const priorBundle = folded(projectPath(priorDir));
2525
+ if (generatedPaths.some((target) => overlaps(target, priorBundle))) {
2526
+ throw new Error("Aura3D assets add failed: generated files cannot overwrite an existing asset namespace.");
2527
+ }
2528
+ }
2529
+ else if (asset.format === "glb" &&
2530
+ generatedPaths.some((target) => overlaps(target, folded(projectPath(priorDir))))) {
2531
+ // A legacy GLB's recorded `dependencies` may be stale or absent. Read
2532
+ // its bounded JSON so generating a thumbnail or typed module cannot
2533
+ // overwrite (or fabricate) an actually referenced sidecar.
2534
+ let references;
2535
+ try {
2536
+ references = referencedGlbResourcePaths(projectDir, priorOutput);
2537
+ }
2538
+ catch (error) {
2539
+ throw new Error(`Aura3D assets add failed: cannot protect existing GLB dependencies for "${asset.id}".`, { cause: error });
2540
+ }
2541
+ if (references.some((path) => generatedPaths.some((target) => overlaps(target, folded(projectPath(path)))))) {
2542
+ throw new Error("Aura3D assets add failed: generated files cannot overwrite an existing GLB dependency.");
2543
+ }
2544
+ }
2545
+ if (asset.id !== name && asset.thumbnailUrl) {
2546
+ const priorThumbnail = resolvePublicArtifactPath(projectDir, current, asset.thumbnailUrl);
2547
+ if (priorThumbnail && folded(projectPath(priorThumbnail)) === generatedPaths[0]) {
2548
+ throw new Error("Aura3D assets add failed: thumbnail collides with another asset.");
2549
+ }
2550
+ }
2551
+ }
2552
+ return generatedSnapshots;
2553
+ }
2554
+ /** Serialize scalar and external-GLB admissions before reading their shared manifest. */
2555
+ function withScalarVolumeAdmissionLock(projectDir, operation) {
2556
+ const path = resolve(projectDir, ".aura3d-scalar-admission.lock");
2557
+ let descriptor;
2558
+ try {
2559
+ descriptor = openSync(path, "wx", 0o600);
2560
+ }
2561
+ catch (error) {
2562
+ if (error.code === "EEXIST") {
2563
+ throw new Error("Aura3D assets add failed: another scalar-volume admission or external GLB admission is in progress or its lock remains.");
2564
+ }
2565
+ throw error;
2566
+ }
2567
+ const identity = fstatSync(descriptor);
2568
+ try {
2569
+ return operation();
2570
+ }
2571
+ finally {
2572
+ closeSync(descriptor);
2573
+ // Do not remove a lock file replaced by another owner.
2574
+ try {
2575
+ const current = lstatSync(path);
2576
+ if (current.dev === identity.dev && current.ino === identity.ino)
2577
+ rmSync(path);
2578
+ }
2579
+ catch (error) {
2580
+ if (error.code !== "ENOENT")
2581
+ throw error;
2582
+ }
2583
+ }
2584
+ }
2585
+ /** The scalar loader maps the sample-center unit cube into node-local space. */
2586
+ function inspectScalarVolumeBounds(path, format, matrix) {
2587
+ const min = [Infinity, Infinity, Infinity];
2588
+ const max = [-Infinity, -Infinity, -Infinity];
2589
+ for (let corner = 0; corner < 8; corner++) {
2590
+ for (let axis = 0; axis < 3; axis++) {
2591
+ const value = matrix[12 + axis] + ((corner & 1) ? matrix[axis] : 0)
2592
+ + ((corner & 2) ? matrix[4 + axis] : 0) + ((corner & 4) ? matrix[8 + axis] : 0);
2593
+ min[axis] = Math.min(min[axis], value);
2594
+ max[axis] = Math.max(max[axis], value);
2595
+ }
2596
+ }
2597
+ const canonical = (value) => {
2598
+ const rounded = round(value);
2599
+ return rounded === 0 ? 0 : rounded;
2600
+ };
2601
+ const size = min.map((value, axis) => canonical(max[axis] - value));
2602
+ const boundsMetadata = {
2603
+ min: min.map(canonical),
2604
+ max: max.map(canonical),
2605
+ size,
2606
+ center: min.map((value, axis) => canonical((value + max[axis]) / 2)),
2607
+ maxDimension: Math.max(...size),
2608
+ grounded: Math.abs(canonical(min[1])) <= 0.08
2609
+ };
2610
+ return { ...inspectAssetFile(path, format), bounds: size, boundsMetadata };
2611
+ }
2612
+ function inspectNativeMeshFile(path, format) {
2613
+ const bytes = readBoundedNativeFile(path);
2614
+ const mesh = format === "ply" ? parsePLY(bytes) : parseSTL(bytes);
2615
+ if (format === "ply" && "elements" in mesh) {
2616
+ const properties = mesh.elements.vertex?.properties ?? {};
2617
+ const splatFields = ["scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3", "opacity", "f_dc_0", "f_dc_1", "f_dc_2"];
2618
+ if (splatFields.every((field) => Object.hasOwn(properties, field))) {
2619
+ throw new Error("Aura3D assets add failed: Gaussian splat PLY needs a splat pipeline; mesh admission would discard its splat attributes.");
2620
+ }
2621
+ }
2622
+ return prepareNativeMeshData(path, format, bytes, mesh);
2623
+ }
2624
+ function prepareNativeMeshData(path, format, bytes, mesh, warnings = []) {
2625
+ validateNativeMeshRenderBudget(mesh);
2626
+ // Bounds are serialized into JSON, where -0 is written as 0. Return the
2627
+ // same canonical value in memory so a fresh admission and a reloaded
2628
+ // manifest have identical public metadata.
2629
+ const roundBound = (value) => {
2630
+ const rounded = round(value);
2631
+ return rounded === 0 ? 0 : rounded;
2632
+ };
2633
+ const min = [Infinity, Infinity, Infinity];
2634
+ const max = [-Infinity, -Infinity, -Infinity];
2635
+ for (let offset = 0; offset < mesh.positions.length; offset += 3) {
2636
+ for (let axis = 0; axis < 3; axis++) {
2637
+ min[axis] = Math.min(min[axis], mesh.positions[offset + axis]);
2638
+ max[axis] = Math.max(max[axis], mesh.positions[offset + axis]);
2639
+ }
2640
+ }
2641
+ const size = min.map((value, axis) => roundBound(max[axis] - value));
2642
+ const boundsMetadata = {
2643
+ min: min.map(roundBound),
2644
+ max: max.map(roundBound),
2645
+ size,
2646
+ center: min.map((value, axis) => roundBound((value + max[axis]) / 2)),
2647
+ maxDimension: Math.max(...size),
2648
+ grounded: Math.abs(roundBound(min[1])) <= 0.08
2649
+ };
2650
+ return {
2651
+ bytes,
2652
+ inspector: format,
2653
+ dependencies: [],
2654
+ inspection: {
2655
+ ...inspectAssetFile(path, format),
2656
+ vertexCount: mesh.positions.length / 3,
2657
+ bounds: size,
2658
+ boundsMetadata,
2659
+ nativeWarnings: [...mesh.diagnostics, ...warnings]
2660
+ }
2661
+ };
2662
+ }
2663
+ /** Inspect OBJ and exactly the material resources consumed by its runtime loader. */
2664
+ function inspectNativeObjFile(path) {
2665
+ const bytes = readBoundedNativeFile(path);
2666
+ const decode = (value) => new TextDecoder("utf-8", { fatal: true }).decode(value);
2667
+ const loader = new OBJLoader();
2668
+ const parsed = loader.inspect(decode(bytes));
2669
+ if (parsed.materialLibraries.length > 256)
2670
+ throw new Error("OBJ_DEPENDENCY_LIMIT: more than 256 material references");
2671
+ const usedMaterials = new Set(parsed.materialNames);
2672
+ const rootUrl = "https://aura3d.invalid/project/";
2673
+ const documentUrl = `${rootUrl}model.obj`;
2674
+ const sourceDir = dirname(path);
2675
+ const dependencies = new Map();
2676
+ const paths = new Map();
2677
+ const materials = new Map();
2678
+ let totalBytes = 0;
2679
+ const readDependency = (url) => {
2680
+ const { uri, path: local } = nativeObjDependencyPath(url);
2681
+ const existing = dependencies.get(uri);
2682
+ if (existing)
2683
+ return existing;
2684
+ const folded = local.toLowerCase();
2685
+ if (paths.has(folded))
2686
+ throw new Error("OBJ_DEPENDENCY_PATH: distinct resource URLs collide on the local filesystem");
2687
+ if (dependencies.size >= 256)
2688
+ throw new Error("OBJ_DEPENDENCY_LIMIT: more than 256 dependencies");
2689
+ const file = resolve(sourceDir, local);
2690
+ assertNoNativeSymlinks(sourceDir, file);
2691
+ const remaining = NATIVE_IMPORT_MAX_BYTES - totalBytes;
2692
+ if (statSync(file).size > remaining)
2693
+ throw new Error("OBJ_DEPENDENCY_LIMIT: aggregate dependencies exceed 64 MiB");
2694
+ const input = readBoundedNativeFile(file, remaining);
2695
+ totalBytes += input.byteLength;
2696
+ if (totalBytes > NATIVE_IMPORT_MAX_BYTES)
2697
+ throw new Error("OBJ_DEPENDENCY_LIMIT: aggregate dependencies exceed 64 MiB");
2698
+ const result = { uri, path: local, bytes: input };
2699
+ dependencies.set(uri, result);
2700
+ paths.set(folded, uri);
2701
+ return result;
2702
+ };
2703
+ const dependencyUrl = (reference, base) => {
2704
+ if (!reference || /[\\\0]/.test(reference) || /%(?:2e|2f|5c)/i.test(reference))
2705
+ throw new Error("OBJ_DEPENDENCY_PATH: unsafe dependency reference");
2706
+ try {
2707
+ return new URL(reference, base);
2708
+ }
2709
+ catch {
2710
+ throw new Error("OBJ_DEPENDENCY_PATH: invalid dependency URL");
2711
+ }
2712
+ };
2713
+ const requireRelativeReference = (reference) => {
2714
+ if (reference.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(reference)) {
2715
+ throw new Error("OBJ_DEPENDENCY_PATH: resource references must be relative to their source file");
2716
+ }
2717
+ };
2718
+ for (const library of parsed.materialLibraries) {
2719
+ requireRelativeReference(library);
2720
+ const url = dependencyUrl(library, documentUrl);
2721
+ const mtl = readDependency(url);
2722
+ for (const material of loader.inspectMaterialLibrary(decode(mtl.bytes), url.href)) {
2723
+ materials.set(material.name, material);
2724
+ if (materials.size > 65_536)
2725
+ throw new Error("OBJ_DEPENDENCY_LIMIT: more than 65536 material definitions");
2726
+ }
2727
+ }
2728
+ let textureReferences = 0;
2729
+ for (const material of materials.values()) {
2730
+ if (!usedMaterials.has(material.name))
2731
+ continue;
2732
+ if (material.textures.length && !parsed.hasTexcoords)
2733
+ throw new Error(`OBJ material ${material.name} uses a texture but the mesh has no texture coordinates.`);
2734
+ for (let index = 0; index < material.textures.length; index++) {
2735
+ const texture = material.textures[index];
2736
+ requireRelativeReference(material.textureReferences[index]);
2737
+ if (++textureReferences > 4096)
2738
+ throw new Error("OBJ_DEPENDENCY_LIMIT: more than 4096 texture references");
2739
+ const item = readDependency(dependencyUrl(texture, rootUrl));
2740
+ const png = item.bytes.length >= 8 && [137, 80, 78, 71, 13, 10, 26, 10].every((value, index) => item.bytes[index] === value);
2741
+ const jpeg = item.bytes.length >= 3 && item.bytes[0] === 255 && item.bytes[1] === 216 && item.bytes[2] === 255;
2742
+ if (!png && !jpeg)
2743
+ throw new Error(`OBJ_TEXTURE_FORMAT: ${item.uri} is not a PNG or JPEG`);
2744
+ }
2745
+ }
2746
+ const min = parsed.min.map(round);
2747
+ const max = parsed.max.map(round);
2748
+ const size = parsed.min.map((value, axis) => round(parsed.max[axis] - value));
2749
+ const boundsMetadata = {
2750
+ min, max, size,
2751
+ center: parsed.min.map((value, axis) => round((value + parsed.max[axis]) / 2)),
2752
+ maxDimension: Math.max(...size),
2753
+ grounded: Math.abs(min[1]) <= 0.08
2754
+ };
2755
+ return {
2756
+ bytes,
2757
+ inspector: "obj",
2758
+ dependencies: [...dependencies.values()],
2759
+ inspection: {
2760
+ ...inspectAssetFile(path, "obj"),
2761
+ vertexCount: parsed.vertexCount,
2762
+ bounds: size,
2763
+ boundsMetadata,
2764
+ materials: parsed.materialNames,
2765
+ textures: [...materials.values()].filter((item) => usedMaterials.has(item.name)).flatMap((item) => item.textures.map((value) => new URL(value).pathname.slice("/project/".length))),
2766
+ dependencies: [...dependencies.values()].map((item) => item.uri),
2767
+ nativeWarnings: [...parsed.diagnostics, ...[...materials.values()].flatMap((item) => item.diagnostics)]
2768
+ }
2769
+ };
2770
+ }
2771
+ function nativeObjDependencyPath(url) {
2772
+ if (url.origin !== "https://aura3d.invalid" || !url.pathname.startsWith("/project/") || url.search || url.hash || url.username || url.password || /%(?:2e|2f|5c)/i.test(url.pathname)) {
2773
+ throw new Error("OBJ_DEPENDENCY_PATH: dependencies must be local and remain inside the source directory");
2774
+ }
2775
+ const uri = url.pathname.slice("/project/".length);
2776
+ let path;
2777
+ try {
2778
+ path = decodeURIComponent(uri);
2779
+ }
2780
+ catch {
2781
+ throw new Error("OBJ_DEPENDENCY_PATH: invalid URI encoding");
2782
+ }
2783
+ if (!path || /[\\\0]/.test(path) || path.split("/").some((part) => !part || part === "." || part === ".."))
2784
+ throw new Error("OBJ_DEPENDENCY_PATH: invalid local path");
2785
+ return { uri, path };
2786
+ }
2787
+ function checkNativeAdmissionDestination(options, native) {
2788
+ const projectDir = resolve(options.projectDir ?? process.cwd());
2789
+ const current = readAssetManifest(projectDir);
2790
+ const outputDir = normalizeRelativePath(options.outputDir ?? current.outputDir ?? DEFAULT_AURA_ASSET_OUTPUT_DIR);
2791
+ const typegen = normalizeRelativePath(options.typegen ?? current.typegen ?? DEFAULT_AURA_ASSET_TYPEGEN);
2792
+ const publicPath = normalizePublicPath(options.publicPath ?? current.assetBasePath ?? DEFAULT_AURA_ASSET_PUBLIC_PATH);
2793
+ validateNativeAdmissionPaths(projectDir, options.name, outputDir, typegen, publicPath);
2794
+ const hash = createHash("sha256").update(native.bytes).digest("hex");
2795
+ const output = resolve(projectDir, outputDir, `${options.name}.${hash.slice(0, 8)}.obj`);
2796
+ const generatedTargets = [
2797
+ output,
2798
+ ...native.dependencies.map((item) => resolve(projectDir, outputDir, item.path)),
2799
+ resolve(projectDir, outputDir, `${options.name}.thumb.svg`),
2800
+ resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
2801
+ resolve(projectDir, typegen)
2802
+ ];
2803
+ if (new Set(generatedTargets.map((target) => target.toLowerCase())).size !== generatedTargets.length) {
2804
+ throw new Error("Aura3D assets add failed: native output paths collide.");
2805
+ }
2806
+ const sourcePath = resolve(projectDir, options.file);
2807
+ if (generatedTargets.some((target) => target === sourcePath && target !== output))
2808
+ throw new Error("Aura3D assets add failed: generated files cannot overwrite the source asset.");
2809
+ for (const target of generatedTargets) {
2810
+ assertNoNativeSymlinks(projectDir, target);
2811
+ if (existsSync(target) && !statSync(target).isFile())
2812
+ throw new Error(`Aura3D assets add failed: output is not a regular file: ${target}`);
2813
+ let parent = dirname(target);
2814
+ while (!existsSync(parent))
2815
+ parent = dirname(parent);
2816
+ if (!statSync(parent).isDirectory())
2817
+ throw new Error(`Aura3D assets add failed: output parent is not a directory: ${parent}`);
2818
+ }
2819
+ if (options.copy === false) {
2820
+ const paths = [
2821
+ { path: resolve(projectDir, outputDir, `${options.name}.${hash.slice(0, 8)}.obj`), bytes: native.bytes },
2822
+ ...native.dependencies.map((item) => ({ path: resolve(projectDir, outputDir, item.path), bytes: item.bytes }))
2823
+ ];
2824
+ for (const item of paths) {
2825
+ assertNoNativeSymlinks(projectDir, item.path);
2826
+ if (!existsSync(item.path) || !statSync(item.path).isFile() || statSync(item.path).size !== item.bytes.length || !readBoundedNativeFile(item.path).equals(item.bytes)) {
2827
+ throw new Error("Aura3D assets add failed: copy:false requires identical pre-staged native files.");
2828
+ }
2829
+ }
2830
+ }
2831
+ }
2832
+ // The parser's allocation limits are wider than its render bridge's 32 MiB
2833
+ // buffer and 4096-group limits. Check the same bridge constraints at admission.
2834
+ function validateNativeMeshRenderBudget(mesh) {
2835
+ const vertexCount = mesh.positions.length / 3;
2836
+ if (!vertexCount || !mesh.indices.length)
2837
+ throw new Error("MESH_GEOMETRY: no renderable vertices or indices");
2838
+ const groups = (mesh.groups ?? [{ name: "mesh", start: 0, count: mesh.indices.length }]).filter((group) => group.count > 0);
2839
+ if (groups.length > 4096)
2840
+ throw new Error("MESH_ADAPTER_LIMIT: more than 4096 nonempty groups");
2841
+ const vertexStride = 4 * (3 + (mesh.normals ? 3 : 0) + (mesh.texcoords ? 2 : 0) + (mesh.colors ? 4 : 0));
2842
+ let bytes = 0;
2843
+ for (const group of groups) {
2844
+ // The bridge remaps multiple groups, duplicating only referenced vertices.
2845
+ if (bytes + group.count * 4 > NATIVE_RENDER_BUFFER_MAX_BYTES)
2846
+ throw new Error("MESH_ADAPTER_LIMIT: glTF buffer exceeds 32 MiB");
2847
+ let count = vertexCount;
2848
+ if (groups.length > 1) {
2849
+ const referenced = new Set();
2850
+ for (let index = group.start; index < group.start + group.count; index++) {
2851
+ referenced.add(mesh.indices[index]);
2852
+ if (bytes + group.count * 4 + referenced.size * vertexStride > NATIVE_RENDER_BUFFER_MAX_BYTES) {
2853
+ throw new Error("MESH_ADAPTER_LIMIT: glTF buffer exceeds 32 MiB");
2854
+ }
2855
+ }
2856
+ count = referenced.size;
2857
+ }
2858
+ bytes += count * vertexStride + group.count * 4;
2859
+ if (bytes > NATIVE_RENDER_BUFFER_MAX_BYTES)
2860
+ throw new Error("MESH_ADAPTER_LIMIT: glTF buffer exceeds 32 MiB");
2861
+ }
2862
+ }
1876
2863
  function inspectAssetFile(path, format) {
1877
2864
  if (format === "gltf")
1878
2865
  return inspectGltf(JSON.parse(readFileSync(path, "utf8")), dirname(path));
@@ -2562,9 +3549,9 @@ function createGlbComplexityWarnings(sizeBytes, vertexCount, format) {
2562
3549
  }
2563
3550
  return warnings;
2564
3551
  }
2565
- function createAssetWarnings(path, inspection) {
3552
+ function createAssetWarnings(path, inspection, inspectedSize) {
2566
3553
  const warnings = [];
2567
- const size = statSync(path).size;
3554
+ const size = inspectedSize ?? statSync(path).size;
2568
3555
  const format = extname(path).slice(1).toLowerCase();
2569
3556
  warnings.push(...createGlbComplexityWarnings(size, inspection.vertexCount, format));
2570
3557
  if (size > 25 * 1024 * 1024)
@@ -2821,7 +3808,7 @@ function createDurableReleaseProvenanceWarnings(asset, provenance) {
2821
3808
  * Reported as warnings rather than failures because release validation already promotes
2822
3809
  * warnings to blocking failures for release candidates, while non-release callers keep working.
2823
3810
  */
2824
- function createDerivedMetadataDriftWarnings(outputPath, asset) {
3811
+ function createDerivedMetadataDriftWarnings(outputPath, asset, glbJson) {
2825
3812
  if (asset.type !== "model")
2826
3813
  return [];
2827
3814
  if (asset.format !== "glb" && asset.format !== "gltf")
@@ -2830,7 +3817,9 @@ function createDerivedMetadataDriftWarnings(outputPath, asset) {
2830
3817
  return [];
2831
3818
  let inspection;
2832
3819
  try {
2833
- inspection = inspectAssetFile(outputPath, asset.format);
3820
+ // The GLB was already inspected and hashed from a bounded JSON read above;
3821
+ // do not read the whole binary container again just to compare metadata.
3822
+ inspection = glbJson ? inspectGltf(glbJson) : inspectAssetFile(outputPath, asset.format);
2834
3823
  }
2835
3824
  catch {
2836
3825
  // A file we cannot inspect is already reported by the hash and dependency checks.
@@ -3086,6 +4075,9 @@ function hasValidManifestOrientationOverride(asset) {
3086
4075
  }
3087
4076
  function releaseStoredAssetWarnings(projectDir, asset) {
3088
4077
  let warnings = asset.warnings ?? [];
4078
+ // Older native admissions stored this informational notice as a warning.
4079
+ // Registry selection is an app runtime concern, so it is not a release gate.
4080
+ warnings = warnings.filter((warning) => !/^(?:OBJ|PLY|STL|3MF) requires explicit registration of a renderer-capable adapter in the app's FormatRegistry\.$/.test(warning));
3089
4081
  if (hasValidManifestOrientationOverride(asset)) {
3090
4082
  warnings = warnings.filter((warning) => !/orientation metadata missing; facing direction cannot be validated/i.test(warning));
3091
4083
  }
@@ -3515,10 +4507,257 @@ function writeThumbnail(projectDir, outputDir, publicPath, id, bounds) {
3515
4507
  const fileName = `${id}.thumb.svg`;
3516
4508
  const outputPath = resolve(projectDir, outputDir, fileName);
3517
4509
  mkdirSync(dirname(outputPath), { recursive: true });
3518
- const label = `${id}${bounds ? ` ${bounds.join("x")}` : ""}`;
3519
- writeFileSync(outputPath, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">${escapeXml(label)}</text></svg>`);
4510
+ writeFileSync(outputPath, thumbnailContents(id, bounds));
3520
4511
  return `${publicPath}${fileName}`;
3521
4512
  }
4513
+ function thumbnailContents(id, bounds) {
4514
+ const label = `${id}${bounds ? ` ${bounds.join("x")}` : ""}`;
4515
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">${escapeXml(label)}</text></svg>`;
4516
+ }
4517
+ function validateNativeAdmissionPaths(projectDir, name, outputDir, typegen, publicPath) {
4518
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name) || name === "__proto__") {
4519
+ throw new Error("Aura3D assets add failed: native asset name must contain only letters, digits, underscores, or hyphens.");
4520
+ }
4521
+ for (const [label, path] of [["output", outputDir], ["typegen", typegen]]) {
4522
+ if (!path || isAbsolute(path) || /^[A-Za-z]:/.test(path) || path.split("/").some((part) => !part || part === "." || part === "..") || /[\0?#%]/.test(path)) {
4523
+ throw new Error(`Aura3D assets add failed: ${label} must be a safe project-relative path.`);
4524
+ }
4525
+ }
4526
+ if ([DEFAULT_AURA_ASSET_MANIFEST, "package.json"].includes(typegen))
4527
+ throw new Error("Aura3D assets add failed: typegen cannot overwrite the manifest or package.json.");
4528
+ let decoded;
4529
+ try {
4530
+ decoded = decodeURIComponent(publicPath);
4531
+ }
4532
+ catch {
4533
+ decoded = "";
4534
+ }
4535
+ if (!publicPath.startsWith("/") || publicPath.startsWith("//") || /[\\?#\0]/.test(publicPath) || decoded !== publicPath ||
4536
+ (publicPath !== "/" && publicPath.slice(1, -1).split("/").some((part) => !part || part === "." || part === ".."))) {
4537
+ throw new Error("Aura3D assets add failed: native asset public path must be a same-origin absolute path without traversal.");
4538
+ }
4539
+ assertNoNativeSymlinks(projectDir, resolve(projectDir, outputDir));
4540
+ assertNoNativeSymlinks(projectDir, resolve(projectDir, typegen));
4541
+ assertNoNativeSymlinks(projectDir, resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST));
4542
+ }
4543
+ function assertNoNativeSymlinks(projectDir, path) {
4544
+ const relativePath = relative(projectDir, path);
4545
+ if (!relativePath || relativePath.startsWith(`..${sep}`) || relativePath === ".." || isAbsolute(relativePath)) {
4546
+ throw new Error("Aura3D assets add failed: native output path must remain inside the project.");
4547
+ }
4548
+ let cursor = projectDir;
4549
+ for (const segment of relativePath.split(sep)) {
4550
+ cursor = join(cursor, segment);
4551
+ try {
4552
+ if (lstatSync(cursor).isSymbolicLink())
4553
+ throw new Error("Aura3D assets add failed: native output paths may not contain symbolic links.");
4554
+ }
4555
+ catch (error) {
4556
+ if (error.code !== "ENOENT")
4557
+ throw error;
4558
+ break;
4559
+ }
4560
+ }
4561
+ }
4562
+ /** Stage every generated file, then roll back already-published paths if a later write fails. */
4563
+ function publishNativeAdmission(projectDir, manifest, outputPath, sourcePath, name, bounds, native, beforeCommit, glbTargets) {
4564
+ const immutable = native?.inspector === "nrrd" || native?.inspector === "glb";
4565
+ const suppliedAssets = native ? [
4566
+ { target: resolve(projectDir, outputPath), bytes: native.bytes },
4567
+ ...native.dependencies.map((item) => ({ target: resolve(projectDir, manifest.outputDir, item.path), bytes: item.bytes }))
4568
+ ] : [];
4569
+ // Distinct URI spellings can resolve to the same local resource. Publish it
4570
+ // only once, while retaining every original URI in the integrity map.
4571
+ const assetsByTarget = new Map();
4572
+ for (const item of suppliedAssets) {
4573
+ const prior = assetsByTarget.get(item.target);
4574
+ if (prior && !prior.equals(item.bytes))
4575
+ throw new Error("Aura3D assets add failed: dependency paths resolve to different bytes.");
4576
+ assetsByTarget.set(item.target, item.bytes);
4577
+ }
4578
+ const assetFiles = [...assetsByTarget].map(([target, bytes]) => ({ target, bytes }));
4579
+ const assertImmutable = (target, bytes) => {
4580
+ if (native?.inspector === "glb")
4581
+ assertStagedGlbFile(projectDir, target, bytes, "existing GLB namespace contains different or unsafe bytes");
4582
+ else
4583
+ assertImmutableScalarFile(target, bytes);
4584
+ };
4585
+ const pendingAssets = assetFiles.filter((item) => {
4586
+ if (!immutable)
4587
+ return true;
4588
+ assertNoNativeSymlinks(projectDir, item.target);
4589
+ if (!existsSync(item.target))
4590
+ return true;
4591
+ assertImmutable(item.target, item.bytes);
4592
+ return false;
4593
+ });
4594
+ const targets = [
4595
+ ...pendingAssets.map((item) => item.target),
4596
+ resolve(projectDir, manifest.outputDir, `${name}.thumb.svg`),
4597
+ resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
4598
+ resolve(projectDir, manifest.typegen)
4599
+ ];
4600
+ if (new Set(targets).size !== targets.length)
4601
+ throw new Error("Aura3D assets add failed: output, thumbnail, manifest and typegen paths must be distinct.");
4602
+ if (targets.some((target) => target === sourcePath && target !== resolve(projectDir, outputPath))) {
4603
+ throw new Error("Aura3D assets add failed: generated files cannot overwrite the source asset.");
4604
+ }
4605
+ for (const target of targets) {
4606
+ assertNoNativeSymlinks(projectDir, target);
4607
+ if (existsSync(target) && !statSync(target).isFile())
4608
+ throw new Error(`Aura3D assets add failed: output is not a regular file: ${target}`);
4609
+ }
4610
+ const stage = mkdtempSync(join(projectDir, ".aura3d-admission-"));
4611
+ const createdDirs = [];
4612
+ const published = [];
4613
+ let retainStage = false;
4614
+ try {
4615
+ const packagePath = resolve(projectDir, "package.json");
4616
+ if (existsSync(packagePath))
4617
+ copyFileSync(packagePath, join(stage, "package.json"));
4618
+ writeAssetManifest(stage, manifest);
4619
+ const generated = writeTypedAssets(stage, { ...manifest, typegen: "native-generated.ts" });
4620
+ const thumb = join(stage, "thumbnail.svg");
4621
+ writeFileSync(thumb, thumbnailContents(name, bounds));
4622
+ const stagedFiles = [];
4623
+ for (let index = 0; index < pendingAssets.length; index++) {
4624
+ const file = join(stage, `asset-${index}`);
4625
+ writeFileSync(file, pendingAssets[index].bytes);
4626
+ stagedFiles.push(file);
4627
+ }
4628
+ stagedFiles.push(thumb, join(stage, DEFAULT_AURA_ASSET_MANIFEST), generated);
4629
+ for (const target of targets) {
4630
+ let parent = dirname(target);
4631
+ const missing = [];
4632
+ while (!existsSync(parent)) {
4633
+ missing.push(parent);
4634
+ parent = dirname(parent);
4635
+ }
4636
+ if (!statSync(parent).isDirectory())
4637
+ throw new Error(`Aura3D assets add failed: output parent is not a directory: ${parent}`);
4638
+ for (const dir of missing.reverse()) {
4639
+ if (glbTargets)
4640
+ assertNoNativeSymlinks(projectDir, dir);
4641
+ mkdirSync(dir);
4642
+ createdDirs.push(dir);
4643
+ }
4644
+ }
4645
+ for (let index = 0; index < targets.length; index++) {
4646
+ // All asset hard-links now exist, but no thumbnail, manifest, or typed
4647
+ // module has been changed. A failed recheck rolls those links back.
4648
+ if (index === pendingAssets.length)
4649
+ beforeCommit?.();
4650
+ const target = targets[index];
4651
+ assertNoNativeSymlinks(projectDir, target);
4652
+ if (immutable && index < pendingAssets.length) {
4653
+ try {
4654
+ // An exclusive hard link publishes complete bytes without replacing
4655
+ // another writer's existing content-addressed file.
4656
+ const identity = native?.inspector === "glb" ? lstatSync(stagedFiles[index]) : undefined;
4657
+ linkSync(stagedFiles[index], target);
4658
+ published.push({ target, ...(identity ? { identity: { dev: identity.dev, ino: identity.ino } } : {}) });
4659
+ }
4660
+ catch (error) {
4661
+ if (error.code !== "EEXIST")
4662
+ throw error;
4663
+ assertNoNativeSymlinks(projectDir, target);
4664
+ assertImmutable(target, pendingAssets[index].bytes);
4665
+ }
4666
+ continue;
4667
+ }
4668
+ if (glbTargets) {
4669
+ if (!glbTargets.has(target))
4670
+ throw new Error("Aura3D assets add failed: unplanned GLB publication target.");
4671
+ const expected = glbTargets.get(target);
4672
+ assertGlbPublicationTarget(projectDir, target, expected);
4673
+ const staged = lstatSync(stagedFiles[index]);
4674
+ const record = { target };
4675
+ if (expected) {
4676
+ record.backup = join(stage, `backup-${index}`);
4677
+ renameSync(target, record.backup);
4678
+ published.push(record);
4679
+ const saved = lstatSync(record.backup);
4680
+ // Renaming can update ctime; the inode, size and mtime must still be
4681
+ // the same file that was approved before the transaction began.
4682
+ if (saved.dev !== expected.dev || saved.ino !== expected.ino ||
4683
+ saved.size !== expected.size || saved.mtimeMs !== expected.mtimeMs) {
4684
+ throw new Error(`Aura3D assets add failed: generated output changed during GLB publication: ${target}`);
4685
+ }
4686
+ }
4687
+ // Never replace a file created after the snapshot, even for generated
4688
+ // outputs. A failed exclusive link leaves that other writer's file intact.
4689
+ linkSync(stagedFiles[index], target);
4690
+ record.identity = { dev: staged.dev, ino: staged.ino };
4691
+ if (!expected)
4692
+ published.push(record);
4693
+ continue;
4694
+ }
4695
+ const backup = existsSync(target) ? join(stage, `backup-${index}`) : undefined;
4696
+ if (backup)
4697
+ renameSync(target, backup);
4698
+ published.push({ target, backup });
4699
+ renameSync(stagedFiles[index], target);
4700
+ }
4701
+ }
4702
+ catch (error) {
4703
+ const rollbackFailures = [];
4704
+ for (const { target, backup, identity } of published.reverse()) {
4705
+ try {
4706
+ if (glbTargets) {
4707
+ if (identity) {
4708
+ let current;
4709
+ try {
4710
+ current = lstatSync(target);
4711
+ }
4712
+ catch (error) {
4713
+ if (error.code !== "ENOENT")
4714
+ throw error;
4715
+ }
4716
+ if (current) {
4717
+ if (current.dev !== identity.dev || current.ino !== identity.ino)
4718
+ throw new Error("published file was replaced");
4719
+ rmSync(target);
4720
+ }
4721
+ }
4722
+ // Linking the backup is exclusive: rollback never erases another
4723
+ // writer's replacement. Retain the stage if restoration is blocked.
4724
+ if (backup)
4725
+ linkSync(backup, target);
4726
+ }
4727
+ else {
4728
+ if (existsSync(target))
4729
+ rmSync(target);
4730
+ if (backup)
4731
+ renameSync(backup, target);
4732
+ }
4733
+ }
4734
+ catch {
4735
+ rollbackFailures.push(target);
4736
+ }
4737
+ }
4738
+ for (const dir of createdDirs.reverse()) {
4739
+ try {
4740
+ rmdirSync(dir);
4741
+ }
4742
+ catch { /* Leave unrelated concurrent content intact. */ }
4743
+ }
4744
+ if (rollbackFailures.length) {
4745
+ // A failed restore must never delete the original backups with the stage.
4746
+ retainStage = true;
4747
+ throw new Error(`Aura3D assets add rollback could not restore ${rollbackFailures.join(", ")}; original files remain in ${stage}.`, { cause: error });
4748
+ }
4749
+ throw error;
4750
+ }
4751
+ finally {
4752
+ if (!retainStage)
4753
+ rmSync(stage, { recursive: true, force: true });
4754
+ }
4755
+ }
4756
+ function assertImmutableScalarFile(path, bytes) {
4757
+ if (!statSync(path).isFile() || statSync(path).size !== bytes.length || !readBoundedNativeFile(path).equals(bytes)) {
4758
+ throw new Error("Aura3D assets add failed: existing scalar-volume namespace contains different bytes.");
4759
+ }
4760
+ }
3522
4761
  /** GU47 E-C: `--type lut` admits only a valid 3-D `.cube` (the S10b display-grade LUT). */
3523
4762
  function admitCubeLut(sourcePath, format) {
3524
4763
  if (format !== "cube") {
@@ -3532,19 +4771,21 @@ function admitCubeLut(sourcePath, format) {
3532
4771
  }
3533
4772
  }
3534
4773
  function inferAssetType(format) {
3535
- if (["glb", "gltf"].includes(format))
4774
+ if (["glb", "gltf", "obj", "ply", "stl", "3mf", "vox"].includes(format) || NATIVE_CATALOG_MODEL_FORMATS.has(format))
3536
4775
  return "model";
3537
- if (["png", "jpg", "jpeg", "webp", "ktx2"].includes(format))
4776
+ if (["png", "jpg", "jpeg", "webp", "ktx2", "pvr"].includes(format))
3538
4777
  return "texture";
3539
4778
  if (["hdr", "exr"].includes(format))
3540
4779
  return "environment";
3541
4780
  if (["mp3", "wav", "ogg"].includes(format))
3542
4781
  return "audio";
4782
+ if (format === "nrrd" || format === "nhdr")
4783
+ return "scalar-volume";
3543
4784
  if (format === "navmesh")
3544
4785
  return "navigation";
3545
4786
  if (format === "cube")
3546
4787
  return "lut";
3547
- throw new Error(`Unsupported Aura3D asset format: ${format || "unknown"}. Suggested fix: use glb, gltf, png, jpg, webp, ktx2, hdr, exr, mp3, wav, ogg, navmesh, or cube.`);
4788
+ throw new Error(`Unsupported Aura3D asset format: ${format || "unknown"}. Suggested fix: use glb, gltf, obj, ply, stl, 3mf, vox, png, jpg, webp, ktx2, pvr, hdr, exr, mp3, wav, ogg, nrrd, nhdr, navmesh, or cube.`);
3548
4789
  }
3549
4790
  function hashFile(path) {
3550
4791
  return createHash("sha256").update(readFileSync(path)).digest("hex");