@stage5/lumine 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/assets.js CHANGED
@@ -275,7 +275,7 @@ export async function assetsUpload(options) {
275
275
  const filePaths = options.positional.slice(1);
276
276
  if (!filePaths.length) {
277
277
  throw new Error(
278
- "Usage: lumine assets upload <file...> (images and audio only)",
278
+ "Usage: lumine assets upload <file...> (runtime media: images, audio, GLB/self-contained glTF, KTX2, HDR, EXR, BIN, DRC)",
279
279
  );
280
280
  }
281
281
  const buildId = await resolveSdkBuildId(options);
@@ -400,9 +400,10 @@ export async function assetsDelete(options) {
400
400
  // matching workspace. An asset must be unreferenced by ALL THREE to be a
401
401
  // candidate. If the server does not provide `referencedByProject` (older API
402
402
  // deployment), prune shows the plan but refuses to delete — the published-
403
- // snapshot check cannot be done client-side. Structural blind spot, always
403
+ // snapshot check cannot be done client-side. Structural blind spots, always
404
404
  // disclosed: references stored in app data (privateDb/sharedDb/viewer DB by
405
- // Twinkle.files-using apps) are invisible to every scan.
405
+ // Twinkle.files-using apps), and references stored only inside uploaded asset
406
+ // files such as .gltf manifests, are invisible to every scan.
406
407
  export async function assetsPrune(options) {
407
408
  const buildId = await resolveSdkBuildId(options);
408
409
  const auth = await ensureAuth(options);
@@ -470,7 +471,7 @@ export async function assetsPrune(options) {
470
471
  );
471
472
  }
472
473
  console.log(
473
- "Caution: asset URLs stored in app data (privateDb/sharedDb/user DBs by apps using Twinkle.files) cannot be detected. If this app stores uploaded-file URLs at runtime, do not prune its assets.",
474
+ "Caution: asset URLs stored in app data (privateDb/sharedDb/user DBs by apps using Twinkle.files), or referenced only inside uploaded files such as .gltf manifests, cannot be detected. Keep those URLs in project files too, or do not prune this build's assets.",
474
475
  );
475
476
  if (!serverVerified) {
476
477
  console.log(
@@ -558,7 +559,7 @@ export async function readAssetUploadCandidate(filePath) {
558
559
  const mimeType = ASSET_MIME_BY_EXTENSION[extension];
559
560
  if (!mimeType) {
560
561
  throw new Error(
561
- `Unsupported asset type for ${fileName}. Build assets support image and audio files (${Object.keys(ASSET_MIME_BY_EXTENSION).join(", ")}).`,
562
+ `Unsupported asset type for ${fileName}. Build assets support runtime media files (${Object.keys(ASSET_MIME_BY_EXTENSION).join(", ")}).`,
562
563
  );
563
564
  }
564
565
  let buffer;
@@ -573,9 +574,62 @@ export async function readAssetUploadCandidate(filePath) {
573
574
  if (!buffer.length) {
574
575
  throw new Error(`Asset file is empty: ${filePath}`);
575
576
  }
577
+ if (extension === ".gltf") {
578
+ assertGltfUploadCandidateHasNoRelativeUris({ fileName, buffer });
579
+ }
576
580
  return { absolutePath, fileName, mimeType, buffer };
577
581
  }
578
582
 
583
+ function isExternalOrEmbeddedGltfUri(uri) {
584
+ const normalizedUri = String(uri || "").trim();
585
+ if (!normalizedUri) return true;
586
+ if (normalizedUri.startsWith("/") || normalizedUri.startsWith("//")) {
587
+ return true;
588
+ }
589
+ const schemeMatch = normalizedUri.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
590
+ if (!schemeMatch) return false;
591
+ return ["http", "https", "data"].includes(schemeMatch[1].toLowerCase());
592
+ }
593
+
594
+ function collectRelativeGltfUris(parsedGltf) {
595
+ const relativeUris = [];
596
+ if (!parsedGltf || typeof parsedGltf !== "object") {
597
+ return relativeUris;
598
+ }
599
+ for (const entry of [
600
+ ...(Array.isArray(parsedGltf.buffers) ? parsedGltf.buffers : []),
601
+ ...(Array.isArray(parsedGltf.images) ? parsedGltf.images : []),
602
+ ]) {
603
+ const uri = typeof entry?.uri === "string" ? entry.uri.trim() : "";
604
+ if (uri && !isExternalOrEmbeddedGltfUri(uri)) {
605
+ relativeUris.push(uri);
606
+ }
607
+ }
608
+ return relativeUris;
609
+ }
610
+
611
+ function assertGltfUploadCandidateHasNoRelativeUris({ fileName, buffer }) {
612
+ let parsedGltf;
613
+ try {
614
+ parsedGltf = JSON.parse(buffer.toString("utf8"));
615
+ } catch {
616
+ throw new Error(`Invalid .gltf JSON in ${fileName}.`);
617
+ }
618
+ const relativeUris = collectRelativeGltfUris(parsedGltf);
619
+ if (relativeUris.length === 0) {
620
+ return;
621
+ }
622
+ const sampleUris = relativeUris
623
+ .slice(0, 3)
624
+ .map((uri) => `"${uri}"`)
625
+ .join(", ");
626
+ throw new Error(
627
+ `${fileName} contains relative glTF URI${
628
+ relativeUris.length === 1 ? "" : "s"
629
+ } (${sampleUris}${relativeUris.length > 3 ? ", ..." : ""}). Raw .gltf companion files are stored under separate asset URLs and will not resolve relative to the .gltf. Use .glb, embed data URIs, or rewrite .gltf URIs to absolute uploaded asset URLs before uploading.`,
630
+ );
631
+ }
632
+
579
633
  export async function uploadRuntimeAsset({
580
634
  options,
581
635
  auth,
package/lib/constants.js CHANGED
@@ -37,8 +37,9 @@ export const ASSETS_METADATA_FILE = "assets.json";
37
37
  // Must match the web workspace's 5MB part size: the server presigns one S3
38
38
  // part URL per 5MB of the declared fileSize.
39
39
  export const ASSET_UPLOAD_CHUNK_BYTES = 5 * 1024 * 1024;
40
- // Mirrors the web workspace's asset accept list (images + audio). The server
41
- // only rejects video, but the platform's asset policy is image/audio.
40
+ // Mirrors the web workspace's runtime asset accept list. The server rejects
41
+ // video; client allowlists keep project asset uploads focused on files loaders
42
+ // can consume from the sandbox.
42
43
  export const ASSET_MIME_BY_EXTENSION = {
43
44
  ".png": "image/png",
44
45
  ".jpg": "image/jpeg",
@@ -60,6 +61,13 @@ export const ASSET_MIME_BY_EXTENSION = {
60
61
  ".flac": "audio/flac",
61
62
  ".aif": "audio/aiff",
62
63
  ".aiff": "audio/aiff",
64
+ ".glb": "model/gltf-binary",
65
+ ".gltf": "model/gltf+json",
66
+ ".ktx2": "image/ktx2",
67
+ ".hdr": "image/vnd.radiance",
68
+ ".exr": "image/x-exr",
69
+ ".bin": "application/octet-stream",
70
+ ".drc": "application/octet-stream",
63
71
  };
64
72
  // Mirrors the platform's project-file limits. File count and effective-line
65
73
  // limits are hardcoded server constants; the total-byte limit is
@@ -155,22 +163,28 @@ lumine save --summary "Describe the change"
155
163
  need --allow-write and mutate real app data.
156
164
  - Owned canonical builds may be published only when the user explicitly asks.
157
165
 
158
- ## Assets (Images & Audio)
166
+ ## Assets (Runtime Media)
159
167
 
160
- - Binary files are NOT project files. Never place images or audio in this
168
+ - Binary files are NOT project files. Never place bundled media in this
161
169
  workspace — lumine save rejects binaries. Assets live in Twinkle's asset
162
170
  storage and are referenced from code by absolute URL.
163
171
  - .twinkle/${ASSETS_METADATA_FILE} lists this build's uploaded assets (the current
164
172
  CLI user's uploads) with their URLs. Reference an asset by its \`url\` value.
165
- - \`lumine assets upload <file...>\` uploads images/audio from disk and prints
166
- the URL to use in code. \`lumine assets list\` prints assets and refreshes
173
+ - \`lumine assets upload <file...>\` uploads supported runtime assets (images,
174
+ audio, GLB/self-contained glTF models, texture maps, HDR/EXR files, and glTF
175
+ companion files) from disk and prints the URL to use in code. Raw .gltf files
176
+ with relative companion URIs must be converted to .glb or rewritten to
177
+ absolute uploaded asset URLs first. Reference every uploaded asset that must
178
+ survive pruning from project files. \`lumine assets list\`
179
+ prints assets and refreshes
167
180
  .twinkle/${ASSETS_METADATA_FILE}; \`lumine assets delete <assetId>\` removes one;
168
181
  \`lumine assets prune\` deletes your uploads that nothing references (server
169
182
  checks draft files, published/version snapshots, and derived builds; local
170
183
  unsaved edits are scanned too). The server enforces storage quotas.
171
184
  - CAUTION: asset URLs an app stores in privateDb/sharedDb/user DBs at runtime
172
- (Twinkle.files uploads) are invisible to prune. Never run \`assets prune\` on
173
- a build whose app stores uploaded-file URLs in app data.
185
+ (Twinkle.files uploads), or URLs referenced only inside uploaded files such as
186
+ .gltf manifests, are invisible to prune. Keep those asset URLs in project
187
+ files too, or do not run \`assets prune\` for that build.
174
188
  - On team projects the manifest also lists \`projectAssets\` (the project
175
189
  owner's uploads) and \`unmatchedReferences\` (asset URLs in code matching no
176
190
  known asset). Reuse existing asset URLs — do NOT re-upload media the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {