@vizejs/vite-plugin-musea 0.81.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/gallery/assets/{MonacoEditor-BAL3w4pf.js → MonacoEditor-VccI_xm1.js} +2 -2
  2. package/dist/gallery/assets/{cssMode-DTjSpNAL.js → cssMode-DYl_gbi2.js} +1 -1
  3. package/dist/gallery/assets/{editor.api2-CIEJpqKT.js → editor.api2-C1iuJsz5.js} +1 -1
  4. package/dist/gallery/assets/{editor.main-DU-sPy-k.js → editor.main-CB_LUZEd.js} +2 -2
  5. package/dist/gallery/assets/{freemarker2-DZuwWwqM.js → freemarker2-CwuG5RrE.js} +1 -1
  6. package/dist/gallery/assets/{handlebars-C6CWcO31.js → handlebars-DQnqIqtZ.js} +1 -1
  7. package/dist/gallery/assets/{html-DTPfnMqY.js → html-Cj3GxbMF.js} +1 -1
  8. package/dist/gallery/assets/{htmlMode-DmLB9Bik.js → htmlMode-ckJrF6jc.js} +1 -1
  9. package/dist/gallery/assets/{index-DjSpyxD0.js → index-6QLn8Kxp.js} +22 -22
  10. package/dist/gallery/assets/{index-CwT3Ex21.css → index-P1L8IaBA.css} +1 -1
  11. package/dist/gallery/assets/{javascript-dFy7cqCx.js → javascript-DNOPgN7f.js} +1 -1
  12. package/dist/gallery/assets/{jsonMode-Bge74JBI.js → jsonMode-BMPHk4yv.js} +1 -1
  13. package/dist/gallery/assets/{liquid-H5lVD6p3.js → liquid-cUC3T7D4.js} +1 -1
  14. package/dist/gallery/assets/{lspLanguageFeatures-CkkzJ5B0.js → lspLanguageFeatures-DnNJLDpx.js} +1 -1
  15. package/dist/gallery/assets/{mdx-DuMAerqf.js → mdx-DDDAX75G.js} +1 -1
  16. package/dist/gallery/assets/{monaco.contribution-Cn9RKjKZ.js → monaco.contribution-BsZfIZIS.js} +2 -2
  17. package/dist/gallery/assets/{python-BqM-0Ttj.js → python-CkEL94qN.js} +1 -1
  18. package/dist/gallery/assets/{razor-BDNVe10U.js → razor-DpH3myLF.js} +1 -1
  19. package/dist/gallery/assets/{tsMode-uoOez2iL.js → tsMode-D4vJtygH.js} +1 -1
  20. package/dist/gallery/assets/{typescript-DI6pcvqw.js → typescript-f3jEZleA.js} +1 -1
  21. package/dist/gallery/assets/{workers-DS42og38.js → workers-7fp33vpu.js} +1 -1
  22. package/dist/gallery/assets/{xml-ChP0eqUe.js → xml-CrndFNV8.js} +1 -1
  23. package/dist/gallery/assets/{yaml-DCdtNHC4.js → yaml-BAl6eAY9.js} +1 -1
  24. package/dist/gallery/index.html +2 -2
  25. package/dist/index.d.mts.map +1 -1
  26. package/dist/index.mjs +450 -194
  27. package/dist/index.mjs.map +1 -1
  28. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -6,6 +6,7 @@ import { transformWithEsbuild } from "vite";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { vizeConfigStore } from "@vizejs/vite-plugin";
9
+ import { randomBytes, timingSafeEqual } from "node:crypto";
9
10
  import { fileURLToPath } from "node:url";
10
11
  //#region src/native-loader.ts
11
12
  /**
@@ -90,6 +91,156 @@ function analyzeSfcFallback(source, _options) {
90
91
  }
91
92
  }
92
93
  //#endregion
94
+ //#region src/security.ts
95
+ const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;
96
+ var HttpError = class extends Error {
97
+ status;
98
+ constructor(message, status) {
99
+ super(message);
100
+ this.name = "HttpError";
101
+ this.status = status;
102
+ }
103
+ };
104
+ function createDevSessionToken() {
105
+ return randomBytes(32).toString("base64url");
106
+ }
107
+ function realpathNearest(targetPath) {
108
+ let current = path.resolve(targetPath);
109
+ const missingParts = [];
110
+ while (true) try {
111
+ const real = fs.realpathSync.native(current);
112
+ return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;
113
+ } catch {
114
+ const parent = path.dirname(current);
115
+ if (parent === current) return path.resolve(targetPath);
116
+ missingParts.push(path.basename(current));
117
+ current = parent;
118
+ }
119
+ }
120
+ function isResolvedPathInside(parentDir, candidatePath) {
121
+ const parent = path.resolve(parentDir);
122
+ const candidate = path.resolve(candidatePath);
123
+ const relative = path.relative(parent, candidate);
124
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
125
+ }
126
+ function isPathInsideAny(parentDirs, candidatePath) {
127
+ const candidate = realpathNearest(candidatePath);
128
+ return parentDirs.some((parentDir) => isResolvedPathInside(realpathNearest(parentDir), candidate));
129
+ }
130
+ function resolveInside(parentDir, candidatePath, label = "path") {
131
+ return resolveInsideAny([parentDir], candidatePath, label);
132
+ }
133
+ function resolveInsideAny(parentDirs, candidatePath, label = "path") {
134
+ if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
135
+ if (parentDirs.length === 0) throw new HttpError(`No allowed directories configured for ${label}`, 500);
136
+ const parent = path.resolve(parentDirs[0] ?? ".");
137
+ const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(parent, candidatePath);
138
+ if (!isPathInsideAny(parentDirs, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
139
+ return resolved;
140
+ }
141
+ function resolveUrlPathInside(parentDir, requestUrl, label = "path") {
142
+ const rawPath = requestUrl.split(/[?#]/, 1)[0] || "/";
143
+ let pathname;
144
+ try {
145
+ pathname = decodeURIComponent(rawPath);
146
+ } catch {
147
+ throw new HttpError(`${label} is not valid URL encoding`, 400);
148
+ }
149
+ pathname = pathname.replaceAll("\\", "/");
150
+ if (pathname.split("/").includes("..")) throw new HttpError(`${label} must not contain parent directory segments`, 400);
151
+ return resolveInside(parentDir, `.${pathname}`, label);
152
+ }
153
+ function collectRequestBody(req, limit = DEFAULT_API_BODY_LIMIT_BYTES) {
154
+ return new Promise((resolve, reject) => {
155
+ let body = "";
156
+ let size = 0;
157
+ let completed = false;
158
+ req.on("data", (chunk) => {
159
+ if (completed) return;
160
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
161
+ size += buffer.byteLength;
162
+ if (size > limit) {
163
+ completed = true;
164
+ reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));
165
+ return;
166
+ }
167
+ body += buffer.toString("utf-8");
168
+ });
169
+ req.on("end", () => {
170
+ if (!completed) {
171
+ completed = true;
172
+ resolve(body);
173
+ }
174
+ });
175
+ req.on("error", (error) => {
176
+ if (!completed) {
177
+ completed = true;
178
+ reject(error);
179
+ }
180
+ });
181
+ });
182
+ }
183
+ function validateDevApiRequest(req, sessionToken) {
184
+ const originError = validateOrigin(req);
185
+ if (originError) return originError;
186
+ if (!isUnsafeMethod(req.method)) return null;
187
+ if (!hasValidSessionToken(req, sessionToken)) return new HttpError("Invalid Musea dev session token", 403);
188
+ if (!isJsonRequest(req)) return new HttpError("Content-Type must be application/json", 415);
189
+ return null;
190
+ }
191
+ function serializeScriptValue(value) {
192
+ return (JSON.stringify(value) ?? "undefined").replace(/[<>&\u2028\u2029]/g, (char) => {
193
+ switch (char) {
194
+ case "<": return "\\u003C";
195
+ case ">": return "\\u003E";
196
+ case "&": return "\\u0026";
197
+ case "\u2028": return "\\u2028";
198
+ case "\u2029": return "\\u2029";
199
+ default: return char;
200
+ }
201
+ });
202
+ }
203
+ function isUnsafeMethod(method) {
204
+ return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
205
+ }
206
+ function isJsonRequest(req) {
207
+ return getHeader(req, "content-type")?.split(";")[0]?.trim().toLowerCase() === "application/json";
208
+ }
209
+ function validateOrigin(req) {
210
+ if (getHeader(req, "sec-fetch-site") === "cross-site") return new HttpError("Cross-origin Musea API requests are not allowed", 403);
211
+ const origin = getHeader(req, "origin");
212
+ if (!origin) return null;
213
+ const host = getHeader(req, "host");
214
+ if (!host) return new HttpError("Missing Host header", 400);
215
+ try {
216
+ if (new URL(origin).host !== host) return new HttpError("Cross-origin Musea API requests are not allowed", 403);
217
+ } catch {
218
+ return new HttpError("Invalid Origin header", 400);
219
+ }
220
+ return null;
221
+ }
222
+ function hasValidSessionToken(req, expectedToken) {
223
+ const actualToken = getHeader(req, "x-musea-session");
224
+ if (!actualToken) return false;
225
+ const actual = Buffer.from(actualToken);
226
+ const expected = Buffer.from(expectedToken);
227
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
228
+ }
229
+ function getHeader(req, name) {
230
+ const value = req.headers[name];
231
+ if (Array.isArray(value)) return value[0];
232
+ return value;
233
+ }
234
+ //#endregion
235
+ //#region src/component-source.ts
236
+ function allowedSourceRoots(root, scanRoots = []) {
237
+ return [...new Set([root, ...scanRoots].map((sourceRoot) => path.resolve(sourceRoot)))];
238
+ }
239
+ function resolveComponentSourcePath(art, artPath, sourceRoots) {
240
+ const componentPath = art.isInline && art.componentPath ? art.componentPath : art.metadata.component ? path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(artPath), art.metadata.component) : null;
241
+ return componentPath ? resolveInsideAny(sourceRoots, componentPath, "component path") : null;
242
+ }
243
+ //#endregion
93
244
  //#region src/utils.ts
94
245
  /**
95
246
  * Shared utility functions for the Musea Vite plugin.
@@ -182,9 +333,6 @@ function toPascalCase(str) {
182
333
  if (!pascal) return "Variant";
183
334
  return /^[\p{L}_$]/u.test(pascal) ? pascal : `Variant${pascal}`;
184
335
  }
185
- function escapeTemplate(str) {
186
- return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
187
- }
188
336
  function escapeHtml(str) {
189
337
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
190
338
  }
@@ -226,6 +374,9 @@ function resolveRelativeSpecifier(specifier, artDir) {
226
374
  function rewriteRelativeImportStatement(statement, artDir) {
227
375
  return statement.replace(/\bfrom\s+(['"])([^'"]+)\1/g, (_match, quote, specifier) => `from ${quote}${resolveRelativeSpecifier(specifier, artDir)}${quote}`).replace(/^(\s*import\s+)(['"])([^'"]+)\2(\s*;?\s*)$/s, (_match, prefix, quote, specifier, suffix) => `${prefix}${quote}${resolveRelativeSpecifier(specifier, artDir)}${quote}${suffix}`);
228
376
  }
377
+ function escapeTemplateLiteral(str) {
378
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
379
+ }
229
380
  function countCharBalance(source, openChar, closeChar) {
230
381
  let balance = 0;
231
382
  for (const char of source) if (char === openChar) balance++;
@@ -394,16 +545,16 @@ function parseScriptSetupForArt(content) {
394
545
  returnNames: [...returnNames]
395
546
  };
396
547
  }
397
- function generateArtModule(art, filePath) {
548
+ function generateArtModule(art, filePath, options = {}) {
398
549
  let componentImportPath;
399
- let componentName;
550
+ let componentTagName;
551
+ const componentBindingName = "__MuseaComponent";
400
552
  if (art.isInline && art.componentPath) {
401
- componentImportPath = art.componentPath;
402
- componentName = path.basename(art.componentPath, ".vue");
553
+ componentImportPath = options.root ? resolveComponentSourcePath(art, filePath, allowedSourceRoots(options.root, options.scanRoots ?? [])) ?? void 0 : art.componentPath;
554
+ componentTagName = "MuseaComponent";
403
555
  } else if (art.metadata.component) {
404
- const comp = art.metadata.component;
405
- componentImportPath = path.isAbsolute(comp) ? comp : path.resolve(path.dirname(filePath), comp);
406
- componentName = path.basename(comp, ".vue");
556
+ componentImportPath = options.root ? resolveComponentSourcePath(art, filePath, allowedSourceRoots(options.root, options.scanRoots ?? [])) ?? void 0 : path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(filePath), art.metadata.component);
557
+ componentTagName = "MuseaComponent";
407
558
  }
408
559
  const scriptSetup = art.scriptSetupContent ? parseScriptSetupForArt(art.scriptSetupContent) : null;
409
560
  let code = `
@@ -417,12 +568,9 @@ import { defineComponent, h } from 'vue';
417
568
  code += `${resolved}\n`;
418
569
  }
419
570
  }
420
- if (componentImportPath && componentName) {
421
- if (!scriptSetup?.imports.some((imp) => {
422
- if (imp.includes(`from '${componentImportPath}'`) || imp.includes(`from "${componentImportPath}"`)) return true;
423
- return new RegExp(`^import\\s+${componentName}[\\s,]`).test(imp.trim());
424
- })) code += `import ${componentName} from '${componentImportPath}';\n`;
425
- code += `export const __component__ = ${componentName};\n`;
571
+ if (componentImportPath && componentTagName) {
572
+ if (!scriptSetup?.imports.some((imp) => new RegExp(`^import\\s+${componentBindingName}[\\s,]`).test(imp.trim()))) code += `import ${componentBindingName} from ${JSON.stringify(componentImportPath)};\n`;
573
+ code += `export const __component__ = ${componentBindingName};\n`;
426
574
  }
427
575
  code += `
428
576
  export const metadata = ${JSON.stringify(art.metadata)};
@@ -432,15 +580,15 @@ export const __styles__ = ${JSON.stringify(art.styleBlocks ?? [])};
432
580
  for (const variant of art.variants) {
433
581
  const variantComponentName = toPascalCase(variant.name);
434
582
  let template = variant.template;
435
- if (componentName) template = template.replace(/<Self/g, `<${componentName}`).replace(/<\/Self>/g, `</${componentName}>`);
436
- const escapedTemplate = template.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
437
- const fullTemplate = `<div data-variant="${variant.name}">${escapedTemplate}</div>`;
438
- const componentNames = /* @__PURE__ */ new Set();
439
- if (componentName) componentNames.add(componentName);
583
+ if (componentTagName) template = template.replace(/<Self/g, `<${componentTagName}`).replace(/<\/Self>/g, `</${componentTagName}>`);
584
+ const escapedTemplate = escapeTemplateLiteral(template);
585
+ const fullTemplate = `<div data-variant="${escapeTemplateLiteral(escapeHtml(variant.name))}">${escapedTemplate}</div>`;
586
+ const componentNames = /* @__PURE__ */ new Map();
587
+ if (componentTagName) componentNames.set(componentTagName, componentBindingName);
440
588
  if (scriptSetup) {
441
- for (const name of scriptSetup.returnNames) if (/^[A-Z]/.test(name)) componentNames.add(name);
589
+ for (const name of scriptSetup.returnNames) if (/^[A-Z]/.test(name)) componentNames.set(name, name);
442
590
  }
443
- const components = componentNames.size > 0 ? ` components: { ${[...componentNames].join(", ")} },\n` : "";
591
+ const components = componentNames.size > 0 ? ` components: { ${[...componentNames].map(([name, value]) => `${JSON.stringify(name)}: ${value}`).join(", ")} },\n` : "";
444
592
  const hasSetupBody = scriptSetup?.setupBody.some((line) => line.trim().length > 0) ?? false;
445
593
  if (scriptSetup && (hasSetupBody || scriptSetup.returnNames.length > 0)) code += `
446
594
  export const ${variantComponentName} = defineComponent({
@@ -452,7 +600,7 @@ ${scriptSetup.setupBody.map((l) => ` ${l}`).join("\n")}
452
600
  template: \`${fullTemplate}\`,
453
601
  });
454
602
  `;
455
- else if (componentName) code += `
603
+ else if (componentTagName) code += `
456
604
  export const ${variantComponentName} = {
457
605
  name: '${variantComponentName}',
458
606
  ${components} template: \`${fullTemplate}\`,
@@ -564,7 +712,7 @@ function generateGalleryBody(basePath) {
564
712
  */
565
713
  function generateGalleryScript(basePath) {
566
714
  return `
567
- const basePath = '${basePath}';
715
+ const basePath = ${serializeScriptValue(basePath)};
568
716
  let arts = [];
569
717
  let selectedArt = null;
570
718
  let searchQuery = '';
@@ -601,17 +749,18 @@ function generateGalleryScript(basePath) {
601
749
 
602
750
  let html = '';
603
751
  for (const [category, items] of Object.entries(categories)) {
752
+ const escapedCategory = escapeHtml(category);
604
753
  html += '<div class="sidebar-section">';
605
- html += '<div class="category-header" data-category="' + category + '">';
754
+ html += '<div class="category-header" data-category="' + escapedCategory + '">';
606
755
  html += '<svg class="category-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>';
607
- html += '<span>' + category + '</span>';
756
+ html += '<span>' + escapedCategory + '</span>';
608
757
  html += '<span class="category-count">' + items.length + '</span>';
609
758
  html += '</div>';
610
- html += '<ul class="art-list" data-category="' + category + '">';
759
+ html += '<ul class="art-list" data-category="' + escapedCategory + '">';
611
760
  for (const art of items) {
612
761
  const active = selectedArt?.path === art.path ? 'active' : '';
613
762
  const variantCount = art.variants?.length || 0;
614
- html += '<li class="art-item ' + active + '" data-path="' + art.path + '">';
763
+ html += '<li class="art-item ' + active + '" data-path="' + escapeHtml(art.path) + '">';
615
764
  html += '<span>' + escapeHtml(art.metadata.title) + '</span>';
616
765
  html += '<span class="art-variant-count">' + variantCount + ' variant' + (variantCount !== 1 ? 's' : '') + '</span>';
617
766
  html += '</li>';
@@ -634,7 +783,7 @@ function generateGalleryScript(basePath) {
634
783
  sidebar.querySelectorAll('.category-header').forEach(header => {
635
784
  header.addEventListener('click', () => {
636
785
  header.classList.toggle('collapsed');
637
- const list = sidebar.querySelector('.art-list[data-category="' + header.dataset.category + '"]');
786
+ const list = header.parentElement?.querySelector('.art-list');
638
787
  if (list) list.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
639
788
  });
640
789
  });
@@ -737,14 +886,15 @@ function generateGalleryScript(basePath) {
737
886
  /**
738
887
  * Generate the inline gallery HTML page.
739
888
  */
740
- function generateGalleryHtml(basePath, themeConfig) {
889
+ function generateGalleryHtml(basePath, devSessionToken, themeConfig) {
890
+ const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(themeConfig)};` : "";
741
891
  return `<!DOCTYPE html>
742
892
  <html lang="en">
743
893
  <head>
744
894
  <meta charset="UTF-8">
745
895
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
746
896
  <title>Musea - Component Gallery</title>
747
- <script>window.__MUSEA_BASE_PATH__='${basePath}';${themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : ""}<\/script>
897
+ <script>window.__MUSEA_BASE_PATH__=${serializeScriptValue(basePath)};window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(devSessionToken)};${themeScript}<\/script>
748
898
  <style>${generateGalleryStyles()}
749
899
  </style>
750
900
  </head>
@@ -760,7 +910,7 @@ function generateGalleryHtml(basePath, themeConfig) {
760
910
  */
761
911
  function generateGalleryModule(basePath) {
762
912
  return `
763
- export const basePath = '${basePath}';
913
+ export const basePath = ${serializeScriptValue(basePath)};
764
914
  export async function loadArts() {
765
915
  const res = await fetch(basePath + '/api/arts');
766
916
  return res.json();
@@ -1189,16 +1339,19 @@ function generatePreviewHtml(art, variant, _basePath, viteBase) {
1189
1339
  //#region src/preview/index.ts
1190
1340
  function generatePreviewModule(art, variantComponentName, variantName, cssImports = [], previewSetup = null) {
1191
1341
  const artModuleId = `virtual:musea-art:${art.path}`;
1192
- const escapedVariantName = escapeTemplate(variantName);
1193
- const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
1194
- const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
1342
+ const artModuleIdLiteral = JSON.stringify(artModuleId);
1343
+ const variantNameLiteral = JSON.stringify(variantName);
1344
+ const variantComponentNameLiteral = JSON.stringify(variantComponentName);
1345
+ const cssImportStatements = cssImports.map((cssPath) => `import ${JSON.stringify(cssPath)};`).join("\n");
1346
+ const setupImport = previewSetup ? `import __museaPreviewSetup from ${JSON.stringify(previewSetup)};` : "";
1195
1347
  const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
1196
1348
  const actionEvents = JSON.stringify(art.metadata.actionEvents ?? []);
1349
+ const artStyleId = `musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`;
1197
1350
  return `
1198
1351
  ${cssImportStatements}
1199
1352
  ${setupImport}
1200
1353
  import { createApp, reactive, h } from 'vue';
1201
- import * as artModule from '${artModuleId}';
1354
+ import * as artModule from ${artModuleIdLiteral};
1202
1355
 
1203
1356
  const container = document.getElementById('app');
1204
1357
 
@@ -1209,7 +1362,7 @@ const propsOverride = reactive({});
1209
1362
  const slotsOverride = reactive({ default: '' });
1210
1363
 
1211
1364
  function ensureArtStyles(styles) {
1212
- const styleId = '${`musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`}';
1365
+ const styleId = ${JSON.stringify(artStyleId)};
1213
1366
  const existing = document.getElementById(styleId);
1214
1367
 
1215
1368
  if (!Array.isArray(styles) || styles.length === 0) {
@@ -1226,6 +1379,30 @@ function ensureArtStyles(styles) {
1226
1379
  }
1227
1380
  }
1228
1381
 
1382
+ function renderError(title, error) {
1383
+ container.textContent = '';
1384
+ const root = document.createElement('div');
1385
+ root.className = 'musea-error';
1386
+
1387
+ const titleEl = document.createElement('div');
1388
+ titleEl.className = 'musea-error-title';
1389
+ titleEl.textContent = title;
1390
+ root.appendChild(titleEl);
1391
+
1392
+ const messageEl = document.createElement('div');
1393
+ messageEl.textContent = error instanceof Error ? error.message : String(error);
1394
+ root.appendChild(messageEl);
1395
+
1396
+ const stack = error instanceof Error ? error.stack : '';
1397
+ if (stack) {
1398
+ const stackEl = document.createElement('pre');
1399
+ stackEl.textContent = stack;
1400
+ root.appendChild(stackEl);
1401
+ }
1402
+
1403
+ container.appendChild(root);
1404
+ }
1405
+
1229
1406
  window.__museaSetProps = (props) => {
1230
1407
  // Clear old keys
1231
1408
  for (const key of Object.keys(propsOverride)) {
@@ -1244,11 +1421,11 @@ window.__museaSetSlots = (slots) => {
1244
1421
  async function mount() {
1245
1422
  try {
1246
1423
  // Get the specific variant component
1247
- const VariantComponent = artModule['${variantComponentName}'];
1424
+ const VariantComponent = artModule[${variantComponentNameLiteral}];
1248
1425
  const RawComponent = artModule.__component__;
1249
1426
 
1250
1427
  if (!VariantComponent) {
1251
- throw new Error('Variant component "${variantComponentName}" not found in art module');
1428
+ throw new Error('Variant component ' + ${variantComponentNameLiteral} + ' not found in art module');
1252
1429
  }
1253
1430
 
1254
1431
  // Create and mount the app
@@ -1260,8 +1437,8 @@ async function mount() {
1260
1437
  app.mount(container);
1261
1438
  currentApp = app;
1262
1439
 
1263
- console.log('[musea-preview] Mounted variant: ${escapedVariantName}');
1264
- __museaInitAddons(container, '${escapedVariantName}', ${actionEvents});
1440
+ console.log('[musea-preview] Mounted variant:', ${variantNameLiteral});
1441
+ __museaInitAddons(container, ${variantNameLiteral}, ${actionEvents});
1265
1442
 
1266
1443
  // Override set-props to remount with raw component + props
1267
1444
  const TargetComponent = RawComponent || VariantComponent;
@@ -1281,13 +1458,7 @@ async function mount() {
1281
1458
  };
1282
1459
  } catch (error) {
1283
1460
  console.error('[musea-preview] Failed to mount:', error);
1284
- container.innerHTML = \`
1285
- <div class="musea-error">
1286
- <div class="musea-error-title">Failed to render component</div>
1287
- <div>\${error.message}</div>
1288
- <pre>\${error.stack || ''}</pre>
1289
- </div>
1290
- \`;
1461
+ renderError('Failed to render component', error);
1291
1462
  }
1292
1463
  }
1293
1464
 
@@ -1300,7 +1471,7 @@ async function remountWithProps(Component) {
1300
1471
  return () => {
1301
1472
  const slotFns = {};
1302
1473
  for (const [name, content] of Object.entries(slotsOverride)) {
1303
- if (content) slotFns[name] = () => h('span', { innerHTML: content });
1474
+ if (content) slotFns[name] = () => h('span', String(content));
1304
1475
  }
1305
1476
  return h(Component, { ...propsOverride }, slotFns);
1306
1477
  };
@@ -1318,17 +1489,20 @@ mount();
1318
1489
  }
1319
1490
  function generatePreviewModuleWithProps(art, variantComponentName, variantName, propsOverride, cssImports = [], previewSetup = null) {
1320
1491
  const artModuleId = `virtual:musea-art:${art.path}`;
1321
- const escapedVariantName = escapeTemplate(variantName);
1492
+ const artModuleIdLiteral = JSON.stringify(artModuleId);
1493
+ const variantNameLiteral = JSON.stringify(variantName);
1494
+ const variantComponentNameLiteral = JSON.stringify(variantComponentName);
1322
1495
  const propsJson = JSON.stringify(propsOverride);
1323
- const cssImportStatements = cssImports.map((cssPath) => `import '${cssPath}';`).join("\n");
1324
- const setupImport = previewSetup ? `import __museaPreviewSetup from '${previewSetup}';` : "";
1496
+ const cssImportStatements = cssImports.map((cssPath) => `import ${JSON.stringify(cssPath)};`).join("\n");
1497
+ const setupImport = previewSetup ? `import __museaPreviewSetup from ${JSON.stringify(previewSetup)};` : "";
1325
1498
  const setupCall = previewSetup ? "await __museaPreviewSetup(app);" : "";
1326
1499
  const actionEvents = JSON.stringify(art.metadata.actionEvents ?? []);
1500
+ const artStyleId = `musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`;
1327
1501
  return `
1328
1502
  ${cssImportStatements}
1329
1503
  ${setupImport}
1330
1504
  import { createApp, h } from 'vue';
1331
- import * as artModule from '${artModuleId}';
1505
+ import * as artModule from ${artModuleIdLiteral};
1332
1506
 
1333
1507
  const container = document.getElementById('app');
1334
1508
  const propsOverride = ${propsJson};
@@ -1336,7 +1510,7 @@ const propsOverride = ${propsJson};
1336
1510
  ${MUSEA_ADDONS_INIT_CODE}
1337
1511
 
1338
1512
  function ensureArtStyles(styles) {
1339
- const styleId = '${`musea-art-styles-${art.path.replace(/[^\w-]+/g, "_")}`}';
1513
+ const styleId = ${JSON.stringify(artStyleId)};
1340
1514
  const existing = document.getElementById(styleId);
1341
1515
 
1342
1516
  if (!Array.isArray(styles) || styles.length === 0) {
@@ -1353,11 +1527,28 @@ function ensureArtStyles(styles) {
1353
1527
  }
1354
1528
  }
1355
1529
 
1530
+ function renderError(title, error) {
1531
+ container.textContent = '';
1532
+ const root = document.createElement('div');
1533
+ root.className = 'musea-error';
1534
+
1535
+ const titleEl = document.createElement('div');
1536
+ titleEl.className = 'musea-error-title';
1537
+ titleEl.textContent = title;
1538
+ root.appendChild(titleEl);
1539
+
1540
+ const messageEl = document.createElement('div');
1541
+ messageEl.textContent = error instanceof Error ? error.message : String(error);
1542
+ root.appendChild(messageEl);
1543
+
1544
+ container.appendChild(root);
1545
+ }
1546
+
1356
1547
  async function mount() {
1357
1548
  try {
1358
- const VariantComponent = artModule['${variantComponentName}'];
1549
+ const VariantComponent = artModule[${variantComponentNameLiteral}];
1359
1550
  if (!VariantComponent) {
1360
- throw new Error('Variant component "${variantComponentName}" not found');
1551
+ throw new Error('Variant component ' + ${variantComponentNameLiteral} + ' not found');
1361
1552
  }
1362
1553
 
1363
1554
  const WrappedComponent = {
@@ -1372,11 +1563,11 @@ async function mount() {
1372
1563
  container.innerHTML = '';
1373
1564
  container.className = 'musea-variant';
1374
1565
  app.mount(container);
1375
- console.log('[musea-preview] Mounted variant: ${escapedVariantName} with props override');
1376
- __museaInitAddons(container, '${escapedVariantName}', ${actionEvents});
1566
+ console.log('[musea-preview] Mounted variant with props override:', ${variantNameLiteral});
1567
+ __museaInitAddons(container, ${variantNameLiteral}, ${actionEvents});
1377
1568
  } catch (error) {
1378
1569
  console.error('[musea-preview] Failed to mount:', error);
1379
- container.innerHTML = '<div class="musea-error"><div class="musea-error-title">Failed to render</div><div>' + error.message + '</div></div>';
1570
+ renderError('Failed to render', error);
1380
1571
  }
1381
1572
  }
1382
1573
 
@@ -1395,7 +1586,11 @@ function resolveGallerySourceDir() {
1395
1586
  function toViteFsPath(filePath) {
1396
1587
  return encodeURI(`/@fs${filePath.split(path.sep).join("/")}`);
1397
1588
  }
1398
- async function tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig) {
1589
+ function generateDevGlobalsScript(basePath, devSessionToken, themeConfig) {
1590
+ const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(themeConfig)};` : "";
1591
+ return `window.__MUSEA_BASE_PATH__=${serializeScriptValue(basePath)};window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(devSessionToken)};${themeScript}`;
1592
+ }
1593
+ async function tryLoadSourceGalleryHtml(devServer, url, basePath, devSessionToken, themeConfig) {
1399
1594
  const gallerySourceDir = resolveGallerySourceDir();
1400
1595
  const indexHtmlPath = path.join(gallerySourceDir, "index.html");
1401
1596
  try {
@@ -1404,10 +1599,9 @@ async function tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig) {
1404
1599
  return null;
1405
1600
  }
1406
1601
  const sourceEntryPath = toViteFsPath(path.join(gallerySourceDir, "main.ts"));
1407
- const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
1408
1602
  let html = await fs.promises.readFile(indexHtmlPath, "utf-8");
1409
1603
  html = html.replace("src=\"./main.ts\"", `src="${sourceEntryPath}"`);
1410
- html = html.replace("</head>", `<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}<\/script></head>`);
1604
+ html = html.replace("</head>", `<script>${generateDevGlobalsScript(basePath, devSessionToken, themeConfig)}<\/script></head>`);
1411
1605
  return devServer.transformIndexHtml(url, html);
1412
1606
  }
1413
1607
  /**
@@ -1422,7 +1616,7 @@ async function tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig) {
1422
1616
  * - Art module route
1423
1617
  */
1424
1618
  function registerMiddleware(devServer, ctx) {
1425
- const { basePath, themeConfig, artFiles } = ctx;
1619
+ const { basePath, devSessionToken, themeConfig, artFiles } = ctx;
1426
1620
  devServer.middlewares.use(basePath, async (req, res, next) => {
1427
1621
  const url = req.url || "/";
1428
1622
  if (url === "/" || url === "/index.html" || url.startsWith("/tokens") || url.startsWith("/component/") || url.startsWith("/tests")) {
@@ -1431,19 +1625,18 @@ function registerMiddleware(devServer, ctx) {
1431
1625
  try {
1432
1626
  await fs.promises.access(indexHtmlPath);
1433
1627
  let html = await fs.promises.readFile(indexHtmlPath, "utf-8");
1434
- const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
1435
- html = html.replace("</head>", `<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}<\/script></head>`);
1628
+ html = html.replace("</head>", `<script>${generateDevGlobalsScript(basePath, devSessionToken, themeConfig)}<\/script></head>`);
1436
1629
  res.setHeader("Content-Type", "text/html");
1437
1630
  res.end(html);
1438
1631
  return;
1439
1632
  } catch {
1440
- const sourceHtml = await tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig);
1633
+ const sourceHtml = await tryLoadSourceGalleryHtml(devServer, url, basePath, devSessionToken, themeConfig);
1441
1634
  if (sourceHtml) {
1442
1635
  res.setHeader("Content-Type", "text/html");
1443
1636
  res.end(sourceHtml);
1444
1637
  return;
1445
1638
  }
1446
- const html = generateGalleryHtml(basePath, themeConfig);
1639
+ const html = generateGalleryHtml(basePath, devSessionToken, themeConfig);
1447
1640
  res.setHeader("Content-Type", "text/html");
1448
1641
  res.end(html);
1449
1642
  return;
@@ -1451,8 +1644,8 @@ function registerMiddleware(devServer, ctx) {
1451
1644
  }
1452
1645
  if (url.startsWith("/assets/")) {
1453
1646
  const galleryDistDir = resolveGalleryDistDir();
1454
- const filePath = path.join(galleryDistDir, url);
1455
1647
  try {
1648
+ const filePath = resolveUrlPathInside(galleryDistDir, url, "asset path");
1456
1649
  if ((await fs.promises.stat(filePath)).isFile()) {
1457
1650
  const content = await fs.promises.readFile(filePath);
1458
1651
  const ext = path.extname(filePath);
@@ -1469,7 +1662,13 @@ function registerMiddleware(devServer, ctx) {
1469
1662
  res.end(content);
1470
1663
  return;
1471
1664
  }
1472
- } catch {}
1665
+ } catch (error) {
1666
+ if (error instanceof HttpError) {
1667
+ res.statusCode = error.status;
1668
+ res.end(error.message);
1669
+ return;
1670
+ }
1671
+ }
1473
1672
  }
1474
1673
  next();
1475
1674
  });
@@ -1567,13 +1766,19 @@ function registerMiddleware(devServer, ctx) {
1567
1766
  res.setHeader("Cache-Control", "no-cache");
1568
1767
  res.end(result.code);
1569
1768
  } else {
1570
- const moduleCode = generateArtModule(art, artPath);
1769
+ const moduleCode = generateArtModule(art, artPath, {
1770
+ root: devServer.config.root,
1771
+ scanRoots: ctx.scanRoots
1772
+ });
1571
1773
  res.setHeader("Content-Type", "application/javascript");
1572
1774
  res.end(moduleCode);
1573
1775
  }
1574
1776
  } catch (err) {
1575
1777
  console.error("[musea] Failed to transform art module:", err);
1576
- const moduleCode = generateArtModule(art, artPath);
1778
+ const moduleCode = generateArtModule(art, artPath, {
1779
+ root: devServer.config.root,
1780
+ scanRoots: ctx.scanRoots
1781
+ });
1577
1782
  res.setHeader("Content-Type", "application/javascript");
1578
1783
  res.end(moduleCode);
1579
1784
  }
@@ -1792,6 +1997,18 @@ function scanTokenUsage(artFiles, tokenMap) {
1792
1997
  */
1793
1998
  const REFERENCE_PATTERN = /^\{(.+)\}$/;
1794
1999
  const MAX_RESOLVE_DEPTH = 10;
2000
+ const UNSAFE_TOKEN_PATH_SEGMENTS = new Set([
2001
+ "__proto__",
2002
+ "prototype",
2003
+ "constructor"
2004
+ ]);
2005
+ function parseTokenPath(dotPath) {
2006
+ const parts = dotPath.split(".");
2007
+ if (parts.length === 0 || parts.some((part) => part.trim() === "")) throw new Error(`Invalid token path "${dotPath}"`);
2008
+ const unsafeSegment = parts.find((part) => UNSAFE_TOKEN_PATH_SEGMENTS.has(part));
2009
+ if (unsafeSegment) throw new Error(`Token path segment "${unsafeSegment}" is not allowed`);
2010
+ return parts;
2011
+ }
1795
2012
  /**
1796
2013
  * Flatten nested categories into a flat map keyed by dot-path.
1797
2014
  */
@@ -1862,7 +2079,7 @@ async function writeRawTokenFile(tokensPath, data) {
1862
2079
  * Set a token at a dot-separated path in the raw JSON structure.
1863
2080
  */
1864
2081
  function setTokenAtPath(data, dotPath, token) {
1865
- const parts = dotPath.split(".");
2082
+ const parts = parseTokenPath(dotPath);
1866
2083
  let current = data;
1867
2084
  for (let i = 0; i < parts.length - 1; i++) {
1868
2085
  const key = parts[i];
@@ -1882,7 +2099,7 @@ function setTokenAtPath(data, dotPath, token) {
1882
2099
  * Delete a token at a dot-separated path, cleaning empty parents.
1883
2100
  */
1884
2101
  function deleteTokenAtPath(data, dotPath) {
1885
- const parts = dotPath.split(".");
2102
+ const parts = parseTokenPath(dotPath);
1886
2103
  const parents = [];
1887
2104
  let current = data;
1888
2105
  for (let i = 0; i < parts.length - 1; i++) {
@@ -1960,27 +2177,34 @@ function findDependentTokens(tokenMap, targetPath) {
1960
2177
  * Generates HTML, Markdown, and JSON documentation from parsed token categories,
1961
2178
  * and provides the main processStyleDictionary orchestrator function.
1962
2179
  */
2180
+ const SAFE_CSS_COLOR_PATTERN = /^(?:#[0-9a-fA-F]{3,8}|(?:rgb|hsl)a?\(\s*[-+.\d,%\s]+\))$/;
2181
+ function safeCssColor(value, type) {
2182
+ if (typeof value !== "string") return null;
2183
+ const trimmed = value.trim();
2184
+ return (type === "color" || trimmed.startsWith("#") || trimmed.startsWith("rgb") || trimmed.startsWith("hsl")) && SAFE_CSS_COLOR_PATTERN.test(trimmed) ? trimmed : null;
2185
+ }
1963
2186
  /**
1964
2187
  * Generate HTML documentation for tokens.
1965
2188
  */
1966
2189
  function generateTokensHtml(categories) {
1967
2190
  const renderToken = (name, token) => {
2191
+ const color = safeCssColor(token.value, token.type);
1968
2192
  return `
1969
2193
  <div class="token">
1970
2194
  <div class="token-preview">
1971
- ${typeof token.value === "string" && (token.value.startsWith("#") || token.value.startsWith("rgb") || token.value.startsWith("hsl") || token.type === "color") ? `<div class="color-swatch" style="background: ${token.value}"></div>` : ""}
2195
+ ${color ? `<div class="color-swatch" style="background: ${color}"></div>` : ""}
1972
2196
  </div>
1973
2197
  <div class="token-info">
1974
- <div class="token-name">${name}</div>
1975
- <div class="token-value">${token.value}</div>
1976
- ${token.description ? `<div class="token-description">${token.description}</div>` : ""}
2198
+ <div class="token-name">${escapeHtml(name)}</div>
2199
+ <div class="token-value">${escapeHtml(String(token.value))}</div>
2200
+ ${token.description ? `<div class="token-description">${escapeHtml(token.description)}</div>` : ""}
1977
2201
  </div>
1978
2202
  </div>
1979
2203
  `;
1980
2204
  };
1981
2205
  const renderCategory = (category, level = 2) => {
1982
2206
  const heading = `h${Math.min(level, 6)}`;
1983
- let html = `<${heading}>${category.name}</${heading}>`;
2207
+ let html = `<${heading}>${escapeHtml(category.name)}</${heading}>`;
1984
2208
  html += "<div class=\"tokens-grid\">";
1985
2209
  for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
1986
2210
  html += "</div>";
@@ -2130,16 +2354,20 @@ async function processStyleDictionary(config) {
2130
2354
  }
2131
2355
  //#endregion
2132
2356
  //#region src/api-tokens.ts
2133
- /**
2134
- * Musea gallery API token route handlers.
2135
- *
2136
- * Handles GET/POST/PUT/DELETE for /api/tokens endpoints:
2137
- * - GET /tokens -- list all resolved design tokens
2138
- * - GET /tokens/usage -- token usage across art files
2139
- * - POST /tokens -- create a new token
2140
- * - PUT /tokens -- update an existing token
2141
- * - DELETE /tokens -- delete a token
2142
- */
2357
+ function resolveTokensPath(ctx) {
2358
+ return resolveInside(ctx.config.root, ctx.tokensPath, "tokensPath");
2359
+ }
2360
+ function sendTokenMutationError(e, sendError) {
2361
+ if (e instanceof HttpError) {
2362
+ sendError(e.message, e.status);
2363
+ return;
2364
+ }
2365
+ if (e instanceof Error && /token path/i.test(e.message)) {
2366
+ sendError(e.message, 400);
2367
+ return;
2368
+ }
2369
+ sendError(e instanceof Error ? e.message : String(e));
2370
+ }
2143
2371
  /** GET /api/tokens/usage */
2144
2372
  async function handleTokensUsage(ctx, sendJson) {
2145
2373
  if (!ctx.tokensPath) {
@@ -2147,7 +2375,7 @@ async function handleTokensUsage(ctx, sendJson) {
2147
2375
  return;
2148
2376
  }
2149
2377
  try {
2150
- const categories = await parseTokens(path.resolve(ctx.config.root, ctx.tokensPath));
2378
+ const categories = await parseTokens(resolveTokensPath(ctx));
2151
2379
  resolveReferences(categories, buildTokenMap(categories));
2152
2380
  const resolvedTokenMap = buildTokenMap(categories);
2153
2381
  sendJson(scanTokenUsage(ctx.artFiles, resolvedTokenMap));
@@ -2172,7 +2400,7 @@ async function handleTokensGet(ctx, sendJson) {
2172
2400
  return;
2173
2401
  }
2174
2402
  try {
2175
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2403
+ const absoluteTokensPath = resolveTokensPath(ctx);
2176
2404
  const categories = await parseTokens(absoluteTokensPath);
2177
2405
  resolveReferences(categories, buildTokenMap(categories));
2178
2406
  const resolvedTokenMap = buildTokenMap(categories);
@@ -2212,7 +2440,7 @@ async function handleTokensCreate(ctx, readBody, sendJson, sendError) {
2212
2440
  sendError("Missing required fields: path, token.value", 400);
2213
2441
  return;
2214
2442
  }
2215
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2443
+ const absoluteTokensPath = resolveTokensPath(ctx);
2216
2444
  const rawData = await readRawTokenFile(absoluteTokensPath);
2217
2445
  const currentMap = buildTokenMap(await parseTokens(absoluteTokensPath));
2218
2446
  if (currentMap[dotPath]) {
@@ -2237,7 +2465,7 @@ async function handleTokensCreate(ctx, readBody, sendJson, sendError) {
2237
2465
  tokenMap: buildTokenMap(categories)
2238
2466
  }, 201);
2239
2467
  } catch (e) {
2240
- sendError(e instanceof Error ? e.message : String(e));
2468
+ sendTokenMutationError(e, sendError);
2241
2469
  }
2242
2470
  }
2243
2471
  /** PUT /api/tokens (update) */
@@ -2253,7 +2481,7 @@ async function handleTokensUpdate(ctx, readBody, sendJson, sendError) {
2253
2481
  sendError("Missing required fields: path, token.value", 400);
2254
2482
  return;
2255
2483
  }
2256
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2484
+ const absoluteTokensPath = resolveTokensPath(ctx);
2257
2485
  if (token.$reference) {
2258
2486
  const validation = validateSemanticReference(buildTokenMap(await parseTokens(absoluteTokensPath)), token.$reference, dotPath);
2259
2487
  if (!validation.valid) {
@@ -2273,7 +2501,7 @@ async function handleTokensUpdate(ctx, readBody, sendJson, sendError) {
2273
2501
  tokenMap: buildTokenMap(categories)
2274
2502
  });
2275
2503
  } catch (e) {
2276
- sendError(e instanceof Error ? e.message : String(e));
2504
+ sendTokenMutationError(e, sendError);
2277
2505
  }
2278
2506
  }
2279
2507
  /** DELETE /api/tokens */
@@ -2289,7 +2517,7 @@ async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
2289
2517
  sendError("Missing required field: path", 400);
2290
2518
  return;
2291
2519
  }
2292
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2520
+ const absoluteTokensPath = resolveTokensPath(ctx);
2293
2521
  const dependents = findDependentTokens(buildTokenMap(await parseTokens(absoluteTokensPath)), dotPath);
2294
2522
  const rawData = await readRawTokenFile(absoluteTokensPath);
2295
2523
  if (!deleteTokenAtPath(rawData, dotPath)) {
@@ -2305,7 +2533,7 @@ async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
2305
2533
  dependentsWarning: dependents.length > 0 ? dependents : void 0
2306
2534
  });
2307
2535
  } catch (e) {
2308
- sendError(e instanceof Error ? e.message : String(e));
2536
+ sendTokenMutationError(e, sendError);
2309
2537
  }
2310
2538
  }
2311
2539
  //#endregion
@@ -2336,7 +2564,11 @@ async function handleArtPalette(ctx, match, sendJson, sendError) {
2336
2564
  typescript: ""
2337
2565
  };
2338
2566
  if (palette.controls.length === 0 && art.metadata.component) {
2339
- const resolvedComponentPath = path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(artPath), art.metadata.component);
2567
+ const resolvedComponentPath = resolveComponentSourcePath(art, artPath, allowedSourceRoots(ctx.config.root, ctx.scanRoots));
2568
+ if (!resolvedComponentPath) {
2569
+ sendJson(palette);
2570
+ return;
2571
+ }
2340
2572
  try {
2341
2573
  const componentSource = await fs.promises.readFile(resolvedComponentPath, "utf-8");
2342
2574
  const analysis = binding.analyzeSfc ? binding.analyzeSfc(componentSource, { filename: resolvedComponentPath }) : analyzeSfcFallback(componentSource, { filename: resolvedComponentPath });
@@ -2414,7 +2646,7 @@ async function handleArtAnalysis(ctx, match, sendJson, sendError) {
2414
2646
  return;
2415
2647
  }
2416
2648
  try {
2417
- const resolvedComponentPath = art.isInline && art.componentPath ? art.componentPath : art.metadata.component ? path.isAbsolute(art.metadata.component) ? art.metadata.component : path.resolve(path.dirname(artPath), art.metadata.component) : null;
2649
+ const resolvedComponentPath = resolveComponentSourcePath(art, artPath, allowedSourceRoots(ctx.config.root, ctx.scanRoots));
2418
2650
  if (resolvedComponentPath) {
2419
2651
  const source = await fs.promises.readFile(resolvedComponentPath, "utf-8");
2420
2652
  const binding = loadNative();
@@ -2512,15 +2744,23 @@ function handlePreviewWithProps(ctx, body, res, sendJson, sendError) {
2512
2744
  res.setHeader("Content-Type", "application/javascript");
2513
2745
  res.end(moduleCode);
2514
2746
  } catch (e) {
2747
+ if (e instanceof HttpError) {
2748
+ sendError(e.message, e.status);
2749
+ return;
2750
+ }
2515
2751
  sendError(e instanceof Error ? e.message : String(e));
2516
2752
  }
2517
2753
  }
2518
2754
  /** POST /api/generate */
2519
- async function handleGenerate(body, sendJson, sendError) {
2755
+ async function handleGenerate(ctx, body, sendJson, sendError) {
2520
2756
  try {
2521
2757
  const { componentPath: reqComponentPath, options: autogenOptions } = JSON.parse(body);
2758
+ if (typeof reqComponentPath !== "string") {
2759
+ sendError("Missing required field: componentPath", 400);
2760
+ return;
2761
+ }
2522
2762
  const { generateArtFile: genArt } = await import("./autogen/index.mjs");
2523
- const result = await genArt(reqComponentPath, autogenOptions);
2763
+ const result = await genArt(resolveInside(ctx.config.root, reqComponentPath, "componentPath"), autogenOptions);
2524
2764
  sendJson({
2525
2765
  generated: true,
2526
2766
  componentName: result.componentName,
@@ -2528,6 +2768,10 @@ async function handleGenerate(body, sendJson, sendError) {
2528
2768
  artFileContent: result.artFileContent
2529
2769
  });
2530
2770
  } catch (e) {
2771
+ if (e instanceof HttpError) {
2772
+ sendError(e.message, e.status);
2773
+ return;
2774
+ }
2531
2775
  sendError(e instanceof Error ? e.message : String(e));
2532
2776
  }
2533
2777
  }
@@ -2590,16 +2834,6 @@ async function handleRunVrt(ctx, body, sendJson, sendError) {
2590
2834
  }
2591
2835
  //#endregion
2592
2836
  //#region src/api-routes/index.ts
2593
- /** Helper to read the full request body as a string. */
2594
- function collectBody(req) {
2595
- return new Promise((resolve) => {
2596
- let body = "";
2597
- req.on("data", (chunk) => {
2598
- body += chunk;
2599
- });
2600
- req.on("end", () => resolve(body));
2601
- });
2602
- }
2603
2837
  /**
2604
2838
  * Create the API middleware handler for the Musea gallery.
2605
2839
  *
@@ -2616,107 +2850,117 @@ function createApiMiddleware(ctx) {
2616
2850
  const sendError = (message, status = 500) => {
2617
2851
  sendJson({ error: message }, status);
2618
2852
  };
2619
- const readBody = () => collectBody(req);
2853
+ const readBody = () => collectRequestBody(req, ctx.apiBodyLimit ?? 1048576);
2620
2854
  const url = req.url || "/";
2621
- if (url === "/arts" && req.method === "GET") {
2622
- sendJson(Array.from(ctx.artFiles.values()));
2623
- return;
2624
- }
2625
- if (url === "/tokens/usage" && req.method === "GET") {
2626
- await handleTokensUsage(ctx, sendJson);
2627
- return;
2628
- }
2629
- if (url === "/tokens" && req.method === "GET") {
2630
- await handleTokensGet(ctx, sendJson);
2631
- return;
2632
- }
2633
- if (url === "/tokens" && req.method === "POST") {
2634
- await handleTokensCreate(ctx, readBody, sendJson, sendError);
2635
- return;
2636
- }
2637
- if (url === "/tokens" && req.method === "PUT") {
2638
- await handleTokensUpdate(ctx, readBody, sendJson, sendError);
2639
- return;
2640
- }
2641
- if (url === "/tokens" && req.method === "DELETE") {
2642
- await handleTokensDelete(ctx, readBody, sendJson, sendError);
2643
- return;
2644
- }
2645
- if (url?.startsWith("/arts/") && req.method === "PUT") {
2646
- const sourceMatch = url.slice(6).match(/^(.+)\/source$/);
2647
- if (sourceMatch) {
2648
- const artPath = decodeURIComponent(sourceMatch[1]);
2649
- if (!ctx.artFiles.get(artPath)) {
2650
- sendError("Art not found", 404);
2651
- return;
2652
- }
2653
- const body = await collectBody(req);
2654
- try {
2655
- const { source } = JSON.parse(body);
2656
- if (typeof source !== "string") {
2657
- sendError("Missing required field: source", 400);
2658
- return;
2659
- }
2660
- await fs.promises.writeFile(artPath, source, "utf-8");
2661
- await ctx.processArtFile(artPath);
2662
- sendJson({ success: true });
2663
- } catch (e) {
2664
- sendError(e instanceof Error ? e.message : String(e));
2665
- }
2855
+ try {
2856
+ const requestError = validateDevApiRequest(req, ctx.devSessionToken);
2857
+ if (requestError) {
2858
+ sendError(requestError.message, requestError.status);
2666
2859
  return;
2667
2860
  }
2668
- next();
2669
- return;
2670
- }
2671
- if (url?.startsWith("/arts/") && req.method === "GET") {
2672
- const rest = url.slice(6);
2673
- const sourceMatch = rest.match(/^(.+)\/source$/);
2674
- const paletteMatch = rest.match(/^(.+)\/palette$/);
2675
- const analysisMatch = rest.match(/^(.+)\/analysis$/);
2676
- const docsMatch = rest.match(/^(.+)\/docs$/);
2677
- const a11yMatch = rest.match(/^(.+)\/variants\/([^/]+)\/a11y$/);
2678
- if (sourceMatch) {
2679
- await handleArtSource(ctx, sourceMatch, sendJson, sendError);
2861
+ if (url === "/arts" && req.method === "GET") {
2862
+ sendJson(Array.from(ctx.artFiles.values()));
2680
2863
  return;
2681
2864
  }
2682
- if (paletteMatch) {
2683
- await handleArtPalette(ctx, paletteMatch, sendJson, sendError);
2865
+ if (url === "/tokens/usage" && req.method === "GET") {
2866
+ await handleTokensUsage(ctx, sendJson);
2684
2867
  return;
2685
2868
  }
2686
- if (analysisMatch) {
2687
- await handleArtAnalysis(ctx, analysisMatch, sendJson, sendError);
2869
+ if (url === "/tokens" && req.method === "GET") {
2870
+ await handleTokensGet(ctx, sendJson);
2688
2871
  return;
2689
2872
  }
2690
- if (docsMatch) {
2691
- await handleArtDocs(ctx, docsMatch, sendJson, sendError);
2873
+ if (url === "/tokens" && req.method === "POST") {
2874
+ await handleTokensCreate(ctx, readBody, sendJson, sendError);
2692
2875
  return;
2693
2876
  }
2694
- if (a11yMatch) {
2695
- handleArtA11y(ctx, a11yMatch, sendJson, sendError);
2877
+ if (url === "/tokens" && req.method === "PUT") {
2878
+ await handleTokensUpdate(ctx, readBody, sendJson, sendError);
2696
2879
  return;
2697
2880
  }
2698
- const artPath = decodeURIComponent(rest);
2699
- const art = ctx.artFiles.get(artPath);
2700
- if (art) sendJson(art);
2701
- else sendError("Art not found", 404);
2702
- return;
2703
- }
2704
- if (req.method === "POST") {
2705
- const body = await collectBody(req);
2706
- if (url === "/preview-with-props") {
2707
- handlePreviewWithProps(ctx, body, res, sendJson, sendError);
2881
+ if (url === "/tokens" && req.method === "DELETE") {
2882
+ await handleTokensDelete(ctx, readBody, sendJson, sendError);
2708
2883
  return;
2709
2884
  }
2710
- if (url === "/generate") {
2711
- await handleGenerate(body, sendJson, sendError);
2885
+ if (url?.startsWith("/arts/") && req.method === "PUT") {
2886
+ const sourceMatch = url.slice(6).match(/^(.+)\/source$/);
2887
+ if (sourceMatch) {
2888
+ const artPath = decodeURIComponent(sourceMatch[1]);
2889
+ if (!ctx.artFiles.get(artPath)) {
2890
+ sendError("Art not found", 404);
2891
+ return;
2892
+ }
2893
+ const safeArtPath = resolveInside(ctx.config.root, artPath, "art path");
2894
+ const body = await readBody();
2895
+ const { source } = JSON.parse(body);
2896
+ if (typeof source !== "string") {
2897
+ sendError("Missing required field: source", 400);
2898
+ return;
2899
+ }
2900
+ await fs.promises.writeFile(safeArtPath, source, "utf-8");
2901
+ await ctx.processArtFile(safeArtPath);
2902
+ sendJson({ success: true });
2903
+ return;
2904
+ }
2905
+ next();
2712
2906
  return;
2713
2907
  }
2714
- if (url === "/run-vrt") {
2715
- await handleRunVrt(ctx, body, sendJson, sendError);
2908
+ if (url?.startsWith("/arts/") && req.method === "GET") {
2909
+ const rest = url.slice(6);
2910
+ const sourceMatch = rest.match(/^(.+)\/source$/);
2911
+ const paletteMatch = rest.match(/^(.+)\/palette$/);
2912
+ const analysisMatch = rest.match(/^(.+)\/analysis$/);
2913
+ const docsMatch = rest.match(/^(.+)\/docs$/);
2914
+ const a11yMatch = rest.match(/^(.+)\/variants\/([^/]+)\/a11y$/);
2915
+ if (sourceMatch) {
2916
+ await handleArtSource(ctx, sourceMatch, sendJson, sendError);
2917
+ return;
2918
+ }
2919
+ if (paletteMatch) {
2920
+ await handleArtPalette(ctx, paletteMatch, sendJson, sendError);
2921
+ return;
2922
+ }
2923
+ if (analysisMatch) {
2924
+ await handleArtAnalysis(ctx, analysisMatch, sendJson, sendError);
2925
+ return;
2926
+ }
2927
+ if (docsMatch) {
2928
+ await handleArtDocs(ctx, docsMatch, sendJson, sendError);
2929
+ return;
2930
+ }
2931
+ if (a11yMatch) {
2932
+ handleArtA11y(ctx, a11yMatch, sendJson, sendError);
2933
+ return;
2934
+ }
2935
+ const artPath = decodeURIComponent(rest);
2936
+ const art = ctx.artFiles.get(artPath);
2937
+ if (art) sendJson(art);
2938
+ else sendError("Art not found", 404);
2939
+ return;
2940
+ }
2941
+ if (req.method === "POST") {
2942
+ const body = await readBody();
2943
+ if (url === "/preview-with-props") {
2944
+ handlePreviewWithProps(ctx, body, res, sendJson, sendError);
2945
+ return;
2946
+ }
2947
+ if (url === "/generate") {
2948
+ await handleGenerate(ctx, body, sendJson, sendError);
2949
+ return;
2950
+ }
2951
+ if (url === "/run-vrt") {
2952
+ await handleRunVrt(ctx, body, sendJson, sendError);
2953
+ return;
2954
+ }
2955
+ }
2956
+ next();
2957
+ } catch (e) {
2958
+ if (e instanceof HttpError) {
2959
+ sendError(e.message, e.status);
2716
2960
  return;
2717
2961
  }
2962
+ sendError(e instanceof Error ? e.message : String(e));
2718
2963
  }
2719
- next();
2720
2964
  };
2721
2965
  }
2722
2966
  //#endregion
@@ -2777,12 +3021,18 @@ function createLoad(state) {
2777
3021
  if (id.startsWith("\0musea-art:")) {
2778
3022
  const artPath = id.slice(11).replace(/\?musea-virtual$/, "");
2779
3023
  const art = state.artFiles.get(artPath);
2780
- if (art) return generateArtModule(art, artPath);
3024
+ if (art) return generateArtModule(art, artPath, {
3025
+ root: state.getConfigRoot(),
3026
+ scanRoots: state.getScanRoots()
3027
+ });
2781
3028
  }
2782
3029
  if (id.startsWith("\0musea:")) {
2783
3030
  const realPath = id.slice(7).replace(/\?musea-virtual$/, "");
2784
3031
  const art = state.artFiles.get(realPath);
2785
- if (art) return generateArtModule(art, realPath);
3032
+ if (art) return generateArtModule(art, realPath, {
3033
+ root: state.getConfigRoot(),
3034
+ scanRoots: state.getScanRoots()
3035
+ });
2786
3036
  }
2787
3037
  return null;
2788
3038
  };
@@ -2855,6 +3105,7 @@ function musea(options = {}) {
2855
3105
  const themeConfig = buildThemeConfig(options.theme);
2856
3106
  const previewCss = options.previewCss ?? [];
2857
3107
  const previewSetup = options.previewSetup;
3108
+ const devSessionToken = createDevSessionToken();
2858
3109
  let config;
2859
3110
  let server = null;
2860
3111
  const artFiles = /* @__PURE__ */ new Map();
@@ -2870,6 +3121,7 @@ function musea(options = {}) {
2870
3121
  resolvedPreviewCss,
2871
3122
  resolvedPreviewSetup,
2872
3123
  getConfigRoot: () => config.root,
3124
+ getScanRoots: () => scanRoots,
2873
3125
  getServer: () => server,
2874
3126
  processArtFile
2875
3127
  };
@@ -2902,18 +3154,22 @@ function musea(options = {}) {
2902
3154
  devServer.watcher.add(scanRoots);
2903
3155
  registerMiddleware(devServer, {
2904
3156
  basePath,
3157
+ devSessionToken,
2905
3158
  themeConfig,
2906
3159
  artFiles,
3160
+ scanRoots,
2907
3161
  resolvedPreviewCss,
2908
3162
  resolvedPreviewSetup
2909
3163
  });
2910
3164
  devServer.middlewares.use(`${basePath}/api`, createApiMiddleware({
2911
3165
  config,
2912
3166
  artFiles,
3167
+ scanRoots,
2913
3168
  tokensPath,
2914
3169
  basePath,
2915
3170
  resolvedPreviewCss,
2916
3171
  resolvedPreviewSetup,
3172
+ devSessionToken,
2917
3173
  processArtFile,
2918
3174
  getDevServerPort: () => devServer.config.server.port || 5173
2919
3175
  }));