@vizejs/vite-plugin-musea 0.81.0 → 0.82.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 (27) hide show
  1. package/dist/gallery/assets/{MonacoEditor-BAL3w4pf.js → MonacoEditor-Dmje1nZi.js} +2 -2
  2. package/dist/gallery/assets/{cssMode-DTjSpNAL.js → cssMode-CHkzGWlG.js} +1 -1
  3. package/dist/gallery/assets/{editor.api2-CIEJpqKT.js → editor.api2-DSQ9Vpiw.js} +1 -1
  4. package/dist/gallery/assets/{editor.main-DU-sPy-k.js → editor.main-Dr80cIvC.js} +2 -2
  5. package/dist/gallery/assets/{freemarker2-DZuwWwqM.js → freemarker2-C590qJFy.js} +1 -1
  6. package/dist/gallery/assets/{handlebars-C6CWcO31.js → handlebars-DMRDuFi1.js} +1 -1
  7. package/dist/gallery/assets/{html-DTPfnMqY.js → html-TtN3jv37.js} +1 -1
  8. package/dist/gallery/assets/{htmlMode-DmLB9Bik.js → htmlMode-By_JGvvp.js} +1 -1
  9. package/dist/gallery/assets/{index-DjSpyxD0.js → index-jXDqIaD8.js} +22 -22
  10. package/dist/gallery/assets/{javascript-dFy7cqCx.js → javascript-y17h-ata.js} +1 -1
  11. package/dist/gallery/assets/{jsonMode-Bge74JBI.js → jsonMode-n1uMwhuY.js} +1 -1
  12. package/dist/gallery/assets/{liquid-H5lVD6p3.js → liquid-D-a8XQan.js} +1 -1
  13. package/dist/gallery/assets/{lspLanguageFeatures-CkkzJ5B0.js → lspLanguageFeatures-6bRTxJu4.js} +1 -1
  14. package/dist/gallery/assets/{mdx-DuMAerqf.js → mdx-CC7DfHFT.js} +1 -1
  15. package/dist/gallery/assets/{monaco.contribution-Cn9RKjKZ.js → monaco.contribution-CfGRLrR9.js} +2 -2
  16. package/dist/gallery/assets/{python-BqM-0Ttj.js → python-BUAcshPr.js} +1 -1
  17. package/dist/gallery/assets/{razor-BDNVe10U.js → razor-C5P2I6v1.js} +1 -1
  18. package/dist/gallery/assets/{tsMode-uoOez2iL.js → tsMode-B927vf_Z.js} +1 -1
  19. package/dist/gallery/assets/{typescript-DI6pcvqw.js → typescript-DTfzyqGo.js} +1 -1
  20. package/dist/gallery/assets/{workers-DS42og38.js → workers-DiXcHCU4.js} +1 -1
  21. package/dist/gallery/assets/{xml-ChP0eqUe.js → xml-CnjQeZMF.js} +1 -1
  22. package/dist/gallery/assets/{yaml-DCdtNHC4.js → yaml-KgJNvybZ.js} +1 -1
  23. package/dist/gallery/index.html +1 -1
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +344 -141
  26. package/dist/index.mjs.map +1 -1
  27. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -7,6 +7,7 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { vizeConfigStore } from "@vizejs/vite-plugin";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { randomBytes, timingSafeEqual } from "node:crypto";
10
11
  //#region src/native-loader.ts
11
12
  /**
12
13
  * Native binding loader for @vizejs/native.
@@ -495,6 +496,126 @@ function generateGalleryStyles() {
495
496
  return `${styles_base_default}\n${styles_layout_default}\n${styles_components_default}`;
496
497
  }
497
498
  //#endregion
499
+ //#region src/security.ts
500
+ const DEFAULT_API_BODY_LIMIT_BYTES = 1024 * 1024;
501
+ var HttpError = class extends Error {
502
+ status;
503
+ constructor(message, status) {
504
+ super(message);
505
+ this.name = "HttpError";
506
+ this.status = status;
507
+ }
508
+ };
509
+ function createDevSessionToken() {
510
+ return randomBytes(32).toString("base64url");
511
+ }
512
+ function isPathInside(parentDir, candidatePath) {
513
+ const parent = path.resolve(parentDir);
514
+ const candidate = path.resolve(candidatePath);
515
+ const relative = path.relative(parent, candidate);
516
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
517
+ }
518
+ function resolveInside(parentDir, candidatePath, label = "path") {
519
+ if (candidatePath.includes("\0")) throw new HttpError(`${label} contains an invalid character`, 400);
520
+ const parent = path.resolve(parentDir);
521
+ const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(parent, candidatePath);
522
+ if (!isPathInside(parent, resolved)) throw new HttpError(`${label} escapes the allowed directory`, 400);
523
+ return resolved;
524
+ }
525
+ function resolveUrlPathInside(parentDir, requestUrl, label = "path") {
526
+ const rawPath = requestUrl.split(/[?#]/, 1)[0] || "/";
527
+ let pathname;
528
+ try {
529
+ pathname = decodeURIComponent(rawPath);
530
+ } catch {
531
+ throw new HttpError(`${label} is not valid URL encoding`, 400);
532
+ }
533
+ pathname = pathname.replaceAll("\\", "/");
534
+ if (pathname.split("/").includes("..")) throw new HttpError(`${label} must not contain parent directory segments`, 400);
535
+ return resolveInside(parentDir, `.${pathname}`, label);
536
+ }
537
+ function collectRequestBody(req, limit = DEFAULT_API_BODY_LIMIT_BYTES) {
538
+ return new Promise((resolve, reject) => {
539
+ let body = "";
540
+ let size = 0;
541
+ let completed = false;
542
+ req.on("data", (chunk) => {
543
+ if (completed) return;
544
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
545
+ size += buffer.byteLength;
546
+ if (size > limit) {
547
+ completed = true;
548
+ reject(new HttpError(`Request body exceeds ${limit} bytes`, 413));
549
+ return;
550
+ }
551
+ body += buffer.toString("utf-8");
552
+ });
553
+ req.on("end", () => {
554
+ if (!completed) {
555
+ completed = true;
556
+ resolve(body);
557
+ }
558
+ });
559
+ req.on("error", (error) => {
560
+ if (!completed) {
561
+ completed = true;
562
+ reject(error);
563
+ }
564
+ });
565
+ });
566
+ }
567
+ function validateDevApiRequest(req, sessionToken) {
568
+ const originError = validateOrigin(req);
569
+ if (originError) return originError;
570
+ if (!isUnsafeMethod(req.method)) return null;
571
+ if (!hasValidSessionToken(req, sessionToken)) return new HttpError("Invalid Musea dev session token", 403);
572
+ if (!isJsonRequest(req)) return new HttpError("Content-Type must be application/json", 415);
573
+ return null;
574
+ }
575
+ function serializeScriptValue(value) {
576
+ return (JSON.stringify(value) ?? "undefined").replace(/[<>&\u2028\u2029]/g, (char) => {
577
+ switch (char) {
578
+ case "<": return "\\u003C";
579
+ case ">": return "\\u003E";
580
+ case "&": return "\\u0026";
581
+ case "\u2028": return "\\u2028";
582
+ case "\u2029": return "\\u2029";
583
+ default: return char;
584
+ }
585
+ });
586
+ }
587
+ function isUnsafeMethod(method) {
588
+ return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
589
+ }
590
+ function isJsonRequest(req) {
591
+ return getHeader(req, "content-type")?.split(";")[0]?.trim().toLowerCase() === "application/json";
592
+ }
593
+ function validateOrigin(req) {
594
+ if (getHeader(req, "sec-fetch-site") === "cross-site") return new HttpError("Cross-origin Musea API requests are not allowed", 403);
595
+ const origin = getHeader(req, "origin");
596
+ if (!origin) return null;
597
+ const host = getHeader(req, "host");
598
+ if (!host) return new HttpError("Missing Host header", 400);
599
+ try {
600
+ if (new URL(origin).host !== host) return new HttpError("Cross-origin Musea API requests are not allowed", 403);
601
+ } catch {
602
+ return new HttpError("Invalid Origin header", 400);
603
+ }
604
+ return null;
605
+ }
606
+ function hasValidSessionToken(req, expectedToken) {
607
+ const actualToken = getHeader(req, "x-musea-session");
608
+ if (!actualToken) return false;
609
+ const actual = Buffer.from(actualToken);
610
+ const expected = Buffer.from(expectedToken);
611
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
612
+ }
613
+ function getHeader(req, name) {
614
+ const value = req.headers[name];
615
+ if (Array.isArray(value)) return value[0];
616
+ return value;
617
+ }
618
+ //#endregion
498
619
  //#region src/gallery/template.ts
499
620
  /**
500
621
  * HTML structure and inline JS generation for the Musea gallery.
@@ -564,7 +685,7 @@ function generateGalleryBody(basePath) {
564
685
  */
565
686
  function generateGalleryScript(basePath) {
566
687
  return `
567
- const basePath = '${basePath}';
688
+ const basePath = ${serializeScriptValue(basePath)};
568
689
  let arts = [];
569
690
  let selectedArt = null;
570
691
  let searchQuery = '';
@@ -737,14 +858,15 @@ function generateGalleryScript(basePath) {
737
858
  /**
738
859
  * Generate the inline gallery HTML page.
739
860
  */
740
- function generateGalleryHtml(basePath, themeConfig) {
861
+ function generateGalleryHtml(basePath, devSessionToken, themeConfig) {
862
+ const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(themeConfig)};` : "";
741
863
  return `<!DOCTYPE html>
742
864
  <html lang="en">
743
865
  <head>
744
866
  <meta charset="UTF-8">
745
867
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
746
868
  <title>Musea - Component Gallery</title>
747
- <script>window.__MUSEA_BASE_PATH__='${basePath}';${themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : ""}<\/script>
869
+ <script>window.__MUSEA_BASE_PATH__=${serializeScriptValue(basePath)};window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(devSessionToken)};${themeScript}<\/script>
748
870
  <style>${generateGalleryStyles()}
749
871
  </style>
750
872
  </head>
@@ -760,7 +882,7 @@ function generateGalleryHtml(basePath, themeConfig) {
760
882
  */
761
883
  function generateGalleryModule(basePath) {
762
884
  return `
763
- export const basePath = '${basePath}';
885
+ export const basePath = ${serializeScriptValue(basePath)};
764
886
  export async function loadArts() {
765
887
  const res = await fetch(basePath + '/api/arts');
766
888
  return res.json();
@@ -1226,6 +1348,30 @@ function ensureArtStyles(styles) {
1226
1348
  }
1227
1349
  }
1228
1350
 
1351
+ function renderError(title, error) {
1352
+ container.textContent = '';
1353
+ const root = document.createElement('div');
1354
+ root.className = 'musea-error';
1355
+
1356
+ const titleEl = document.createElement('div');
1357
+ titleEl.className = 'musea-error-title';
1358
+ titleEl.textContent = title;
1359
+ root.appendChild(titleEl);
1360
+
1361
+ const messageEl = document.createElement('div');
1362
+ messageEl.textContent = error instanceof Error ? error.message : String(error);
1363
+ root.appendChild(messageEl);
1364
+
1365
+ const stack = error instanceof Error ? error.stack : '';
1366
+ if (stack) {
1367
+ const stackEl = document.createElement('pre');
1368
+ stackEl.textContent = stack;
1369
+ root.appendChild(stackEl);
1370
+ }
1371
+
1372
+ container.appendChild(root);
1373
+ }
1374
+
1229
1375
  window.__museaSetProps = (props) => {
1230
1376
  // Clear old keys
1231
1377
  for (const key of Object.keys(propsOverride)) {
@@ -1281,13 +1427,7 @@ async function mount() {
1281
1427
  };
1282
1428
  } catch (error) {
1283
1429
  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
- \`;
1430
+ renderError('Failed to render component', error);
1291
1431
  }
1292
1432
  }
1293
1433
 
@@ -1300,7 +1440,7 @@ async function remountWithProps(Component) {
1300
1440
  return () => {
1301
1441
  const slotFns = {};
1302
1442
  for (const [name, content] of Object.entries(slotsOverride)) {
1303
- if (content) slotFns[name] = () => h('span', { innerHTML: content });
1443
+ if (content) slotFns[name] = () => h('span', String(content));
1304
1444
  }
1305
1445
  return h(Component, { ...propsOverride }, slotFns);
1306
1446
  };
@@ -1353,6 +1493,23 @@ function ensureArtStyles(styles) {
1353
1493
  }
1354
1494
  }
1355
1495
 
1496
+ function renderError(title, error) {
1497
+ container.textContent = '';
1498
+ const root = document.createElement('div');
1499
+ root.className = 'musea-error';
1500
+
1501
+ const titleEl = document.createElement('div');
1502
+ titleEl.className = 'musea-error-title';
1503
+ titleEl.textContent = title;
1504
+ root.appendChild(titleEl);
1505
+
1506
+ const messageEl = document.createElement('div');
1507
+ messageEl.textContent = error instanceof Error ? error.message : String(error);
1508
+ root.appendChild(messageEl);
1509
+
1510
+ container.appendChild(root);
1511
+ }
1512
+
1356
1513
  async function mount() {
1357
1514
  try {
1358
1515
  const VariantComponent = artModule['${variantComponentName}'];
@@ -1376,7 +1533,7 @@ async function mount() {
1376
1533
  __museaInitAddons(container, '${escapedVariantName}', ${actionEvents});
1377
1534
  } catch (error) {
1378
1535
  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>';
1536
+ renderError('Failed to render', error);
1380
1537
  }
1381
1538
  }
1382
1539
 
@@ -1395,7 +1552,11 @@ function resolveGallerySourceDir() {
1395
1552
  function toViteFsPath(filePath) {
1396
1553
  return encodeURI(`/@fs${filePath.split(path.sep).join("/")}`);
1397
1554
  }
1398
- async function tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig) {
1555
+ function generateDevGlobalsScript(basePath, devSessionToken, themeConfig) {
1556
+ const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${serializeScriptValue(themeConfig)};` : "";
1557
+ return `window.__MUSEA_BASE_PATH__=${serializeScriptValue(basePath)};window.__MUSEA_SESSION_TOKEN__=${serializeScriptValue(devSessionToken)};${themeScript}`;
1558
+ }
1559
+ async function tryLoadSourceGalleryHtml(devServer, url, basePath, devSessionToken, themeConfig) {
1399
1560
  const gallerySourceDir = resolveGallerySourceDir();
1400
1561
  const indexHtmlPath = path.join(gallerySourceDir, "index.html");
1401
1562
  try {
@@ -1404,10 +1565,9 @@ async function tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig) {
1404
1565
  return null;
1405
1566
  }
1406
1567
  const sourceEntryPath = toViteFsPath(path.join(gallerySourceDir, "main.ts"));
1407
- const themeScript = themeConfig ? `window.__MUSEA_THEME_CONFIG__=${JSON.stringify(themeConfig)};` : "";
1408
1568
  let html = await fs.promises.readFile(indexHtmlPath, "utf-8");
1409
1569
  html = html.replace("src=\"./main.ts\"", `src="${sourceEntryPath}"`);
1410
- html = html.replace("</head>", `<script>window.__MUSEA_BASE_PATH__='${basePath}';${themeScript}<\/script></head>`);
1570
+ html = html.replace("</head>", `<script>${generateDevGlobalsScript(basePath, devSessionToken, themeConfig)}<\/script></head>`);
1411
1571
  return devServer.transformIndexHtml(url, html);
1412
1572
  }
1413
1573
  /**
@@ -1422,7 +1582,7 @@ async function tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig) {
1422
1582
  * - Art module route
1423
1583
  */
1424
1584
  function registerMiddleware(devServer, ctx) {
1425
- const { basePath, themeConfig, artFiles } = ctx;
1585
+ const { basePath, devSessionToken, themeConfig, artFiles } = ctx;
1426
1586
  devServer.middlewares.use(basePath, async (req, res, next) => {
1427
1587
  const url = req.url || "/";
1428
1588
  if (url === "/" || url === "/index.html" || url.startsWith("/tokens") || url.startsWith("/component/") || url.startsWith("/tests")) {
@@ -1431,19 +1591,18 @@ function registerMiddleware(devServer, ctx) {
1431
1591
  try {
1432
1592
  await fs.promises.access(indexHtmlPath);
1433
1593
  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>`);
1594
+ html = html.replace("</head>", `<script>${generateDevGlobalsScript(basePath, devSessionToken, themeConfig)}<\/script></head>`);
1436
1595
  res.setHeader("Content-Type", "text/html");
1437
1596
  res.end(html);
1438
1597
  return;
1439
1598
  } catch {
1440
- const sourceHtml = await tryLoadSourceGalleryHtml(devServer, url, basePath, themeConfig);
1599
+ const sourceHtml = await tryLoadSourceGalleryHtml(devServer, url, basePath, devSessionToken, themeConfig);
1441
1600
  if (sourceHtml) {
1442
1601
  res.setHeader("Content-Type", "text/html");
1443
1602
  res.end(sourceHtml);
1444
1603
  return;
1445
1604
  }
1446
- const html = generateGalleryHtml(basePath, themeConfig);
1605
+ const html = generateGalleryHtml(basePath, devSessionToken, themeConfig);
1447
1606
  res.setHeader("Content-Type", "text/html");
1448
1607
  res.end(html);
1449
1608
  return;
@@ -1451,8 +1610,8 @@ function registerMiddleware(devServer, ctx) {
1451
1610
  }
1452
1611
  if (url.startsWith("/assets/")) {
1453
1612
  const galleryDistDir = resolveGalleryDistDir();
1454
- const filePath = path.join(galleryDistDir, url);
1455
1613
  try {
1614
+ const filePath = resolveUrlPathInside(galleryDistDir, url, "asset path");
1456
1615
  if ((await fs.promises.stat(filePath)).isFile()) {
1457
1616
  const content = await fs.promises.readFile(filePath);
1458
1617
  const ext = path.extname(filePath);
@@ -1469,7 +1628,13 @@ function registerMiddleware(devServer, ctx) {
1469
1628
  res.end(content);
1470
1629
  return;
1471
1630
  }
1472
- } catch {}
1631
+ } catch (error) {
1632
+ if (error instanceof HttpError) {
1633
+ res.statusCode = error.status;
1634
+ res.end(error.message);
1635
+ return;
1636
+ }
1637
+ }
1473
1638
  }
1474
1639
  next();
1475
1640
  });
@@ -1792,6 +1957,18 @@ function scanTokenUsage(artFiles, tokenMap) {
1792
1957
  */
1793
1958
  const REFERENCE_PATTERN = /^\{(.+)\}$/;
1794
1959
  const MAX_RESOLVE_DEPTH = 10;
1960
+ const UNSAFE_TOKEN_PATH_SEGMENTS = new Set([
1961
+ "__proto__",
1962
+ "prototype",
1963
+ "constructor"
1964
+ ]);
1965
+ function parseTokenPath(dotPath) {
1966
+ const parts = dotPath.split(".");
1967
+ if (parts.length === 0 || parts.some((part) => part.trim() === "")) throw new Error(`Invalid token path "${dotPath}"`);
1968
+ const unsafeSegment = parts.find((part) => UNSAFE_TOKEN_PATH_SEGMENTS.has(part));
1969
+ if (unsafeSegment) throw new Error(`Token path segment "${unsafeSegment}" is not allowed`);
1970
+ return parts;
1971
+ }
1795
1972
  /**
1796
1973
  * Flatten nested categories into a flat map keyed by dot-path.
1797
1974
  */
@@ -1862,7 +2039,7 @@ async function writeRawTokenFile(tokensPath, data) {
1862
2039
  * Set a token at a dot-separated path in the raw JSON structure.
1863
2040
  */
1864
2041
  function setTokenAtPath(data, dotPath, token) {
1865
- const parts = dotPath.split(".");
2042
+ const parts = parseTokenPath(dotPath);
1866
2043
  let current = data;
1867
2044
  for (let i = 0; i < parts.length - 1; i++) {
1868
2045
  const key = parts[i];
@@ -1882,7 +2059,7 @@ function setTokenAtPath(data, dotPath, token) {
1882
2059
  * Delete a token at a dot-separated path, cleaning empty parents.
1883
2060
  */
1884
2061
  function deleteTokenAtPath(data, dotPath) {
1885
- const parts = dotPath.split(".");
2062
+ const parts = parseTokenPath(dotPath);
1886
2063
  const parents = [];
1887
2064
  let current = data;
1888
2065
  for (let i = 0; i < parts.length - 1; i++) {
@@ -1960,27 +2137,34 @@ function findDependentTokens(tokenMap, targetPath) {
1960
2137
  * Generates HTML, Markdown, and JSON documentation from parsed token categories,
1961
2138
  * and provides the main processStyleDictionary orchestrator function.
1962
2139
  */
2140
+ const SAFE_CSS_COLOR_PATTERN = /^(?:#[0-9a-fA-F]{3,8}|(?:rgb|hsl)a?\(\s*[-+.\d,%\s]+\))$/;
2141
+ function safeCssColor(value, type) {
2142
+ if (typeof value !== "string") return null;
2143
+ const trimmed = value.trim();
2144
+ return (type === "color" || trimmed.startsWith("#") || trimmed.startsWith("rgb") || trimmed.startsWith("hsl")) && SAFE_CSS_COLOR_PATTERN.test(trimmed) ? trimmed : null;
2145
+ }
1963
2146
  /**
1964
2147
  * Generate HTML documentation for tokens.
1965
2148
  */
1966
2149
  function generateTokensHtml(categories) {
1967
2150
  const renderToken = (name, token) => {
2151
+ const color = safeCssColor(token.value, token.type);
1968
2152
  return `
1969
2153
  <div class="token">
1970
2154
  <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>` : ""}
2155
+ ${color ? `<div class="color-swatch" style="background: ${color}"></div>` : ""}
1972
2156
  </div>
1973
2157
  <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>` : ""}
2158
+ <div class="token-name">${escapeHtml(name)}</div>
2159
+ <div class="token-value">${escapeHtml(String(token.value))}</div>
2160
+ ${token.description ? `<div class="token-description">${escapeHtml(token.description)}</div>` : ""}
1977
2161
  </div>
1978
2162
  </div>
1979
2163
  `;
1980
2164
  };
1981
2165
  const renderCategory = (category, level = 2) => {
1982
2166
  const heading = `h${Math.min(level, 6)}`;
1983
- let html = `<${heading}>${category.name}</${heading}>`;
2167
+ let html = `<${heading}>${escapeHtml(category.name)}</${heading}>`;
1984
2168
  html += "<div class=\"tokens-grid\">";
1985
2169
  for (const [name, token] of Object.entries(category.tokens)) html += renderToken(name, token);
1986
2170
  html += "</div>";
@@ -2130,16 +2314,20 @@ async function processStyleDictionary(config) {
2130
2314
  }
2131
2315
  //#endregion
2132
2316
  //#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
- */
2317
+ function resolveTokensPath(ctx) {
2318
+ return resolveInside(ctx.config.root, ctx.tokensPath, "tokensPath");
2319
+ }
2320
+ function sendTokenMutationError(e, sendError) {
2321
+ if (e instanceof HttpError) {
2322
+ sendError(e.message, e.status);
2323
+ return;
2324
+ }
2325
+ if (e instanceof Error && /token path/i.test(e.message)) {
2326
+ sendError(e.message, 400);
2327
+ return;
2328
+ }
2329
+ sendError(e instanceof Error ? e.message : String(e));
2330
+ }
2143
2331
  /** GET /api/tokens/usage */
2144
2332
  async function handleTokensUsage(ctx, sendJson) {
2145
2333
  if (!ctx.tokensPath) {
@@ -2147,7 +2335,7 @@ async function handleTokensUsage(ctx, sendJson) {
2147
2335
  return;
2148
2336
  }
2149
2337
  try {
2150
- const categories = await parseTokens(path.resolve(ctx.config.root, ctx.tokensPath));
2338
+ const categories = await parseTokens(resolveTokensPath(ctx));
2151
2339
  resolveReferences(categories, buildTokenMap(categories));
2152
2340
  const resolvedTokenMap = buildTokenMap(categories);
2153
2341
  sendJson(scanTokenUsage(ctx.artFiles, resolvedTokenMap));
@@ -2172,7 +2360,7 @@ async function handleTokensGet(ctx, sendJson) {
2172
2360
  return;
2173
2361
  }
2174
2362
  try {
2175
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2363
+ const absoluteTokensPath = resolveTokensPath(ctx);
2176
2364
  const categories = await parseTokens(absoluteTokensPath);
2177
2365
  resolveReferences(categories, buildTokenMap(categories));
2178
2366
  const resolvedTokenMap = buildTokenMap(categories);
@@ -2212,7 +2400,7 @@ async function handleTokensCreate(ctx, readBody, sendJson, sendError) {
2212
2400
  sendError("Missing required fields: path, token.value", 400);
2213
2401
  return;
2214
2402
  }
2215
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2403
+ const absoluteTokensPath = resolveTokensPath(ctx);
2216
2404
  const rawData = await readRawTokenFile(absoluteTokensPath);
2217
2405
  const currentMap = buildTokenMap(await parseTokens(absoluteTokensPath));
2218
2406
  if (currentMap[dotPath]) {
@@ -2237,7 +2425,7 @@ async function handleTokensCreate(ctx, readBody, sendJson, sendError) {
2237
2425
  tokenMap: buildTokenMap(categories)
2238
2426
  }, 201);
2239
2427
  } catch (e) {
2240
- sendError(e instanceof Error ? e.message : String(e));
2428
+ sendTokenMutationError(e, sendError);
2241
2429
  }
2242
2430
  }
2243
2431
  /** PUT /api/tokens (update) */
@@ -2253,7 +2441,7 @@ async function handleTokensUpdate(ctx, readBody, sendJson, sendError) {
2253
2441
  sendError("Missing required fields: path, token.value", 400);
2254
2442
  return;
2255
2443
  }
2256
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2444
+ const absoluteTokensPath = resolveTokensPath(ctx);
2257
2445
  if (token.$reference) {
2258
2446
  const validation = validateSemanticReference(buildTokenMap(await parseTokens(absoluteTokensPath)), token.$reference, dotPath);
2259
2447
  if (!validation.valid) {
@@ -2273,7 +2461,7 @@ async function handleTokensUpdate(ctx, readBody, sendJson, sendError) {
2273
2461
  tokenMap: buildTokenMap(categories)
2274
2462
  });
2275
2463
  } catch (e) {
2276
- sendError(e instanceof Error ? e.message : String(e));
2464
+ sendTokenMutationError(e, sendError);
2277
2465
  }
2278
2466
  }
2279
2467
  /** DELETE /api/tokens */
@@ -2289,7 +2477,7 @@ async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
2289
2477
  sendError("Missing required field: path", 400);
2290
2478
  return;
2291
2479
  }
2292
- const absoluteTokensPath = path.resolve(ctx.config.root, ctx.tokensPath);
2480
+ const absoluteTokensPath = resolveTokensPath(ctx);
2293
2481
  const dependents = findDependentTokens(buildTokenMap(await parseTokens(absoluteTokensPath)), dotPath);
2294
2482
  const rawData = await readRawTokenFile(absoluteTokensPath);
2295
2483
  if (!deleteTokenAtPath(rawData, dotPath)) {
@@ -2305,7 +2493,7 @@ async function handleTokensDelete(ctx, readBody, sendJson, sendError) {
2305
2493
  dependentsWarning: dependents.length > 0 ? dependents : void 0
2306
2494
  });
2307
2495
  } catch (e) {
2308
- sendError(e instanceof Error ? e.message : String(e));
2496
+ sendTokenMutationError(e, sendError);
2309
2497
  }
2310
2498
  }
2311
2499
  //#endregion
@@ -2512,15 +2700,23 @@ function handlePreviewWithProps(ctx, body, res, sendJson, sendError) {
2512
2700
  res.setHeader("Content-Type", "application/javascript");
2513
2701
  res.end(moduleCode);
2514
2702
  } catch (e) {
2703
+ if (e instanceof HttpError) {
2704
+ sendError(e.message, e.status);
2705
+ return;
2706
+ }
2515
2707
  sendError(e instanceof Error ? e.message : String(e));
2516
2708
  }
2517
2709
  }
2518
2710
  /** POST /api/generate */
2519
- async function handleGenerate(body, sendJson, sendError) {
2711
+ async function handleGenerate(ctx, body, sendJson, sendError) {
2520
2712
  try {
2521
2713
  const { componentPath: reqComponentPath, options: autogenOptions } = JSON.parse(body);
2714
+ if (typeof reqComponentPath !== "string") {
2715
+ sendError("Missing required field: componentPath", 400);
2716
+ return;
2717
+ }
2522
2718
  const { generateArtFile: genArt } = await import("./autogen/index.mjs");
2523
- const result = await genArt(reqComponentPath, autogenOptions);
2719
+ const result = await genArt(resolveInside(ctx.config.root, reqComponentPath, "componentPath"), autogenOptions);
2524
2720
  sendJson({
2525
2721
  generated: true,
2526
2722
  componentName: result.componentName,
@@ -2528,6 +2724,10 @@ async function handleGenerate(body, sendJson, sendError) {
2528
2724
  artFileContent: result.artFileContent
2529
2725
  });
2530
2726
  } catch (e) {
2727
+ if (e instanceof HttpError) {
2728
+ sendError(e.message, e.status);
2729
+ return;
2730
+ }
2531
2731
  sendError(e instanceof Error ? e.message : String(e));
2532
2732
  }
2533
2733
  }
@@ -2590,16 +2790,6 @@ async function handleRunVrt(ctx, body, sendJson, sendError) {
2590
2790
  }
2591
2791
  //#endregion
2592
2792
  //#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
2793
  /**
2604
2794
  * Create the API middleware handler for the Musea gallery.
2605
2795
  *
@@ -2616,107 +2806,117 @@ function createApiMiddleware(ctx) {
2616
2806
  const sendError = (message, status = 500) => {
2617
2807
  sendJson({ error: message }, status);
2618
2808
  };
2619
- const readBody = () => collectBody(req);
2809
+ const readBody = () => collectRequestBody(req, ctx.apiBodyLimit ?? 1048576);
2620
2810
  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
- }
2811
+ try {
2812
+ const requestError = validateDevApiRequest(req, ctx.devSessionToken);
2813
+ if (requestError) {
2814
+ sendError(requestError.message, requestError.status);
2666
2815
  return;
2667
2816
  }
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);
2817
+ if (url === "/arts" && req.method === "GET") {
2818
+ sendJson(Array.from(ctx.artFiles.values()));
2680
2819
  return;
2681
2820
  }
2682
- if (paletteMatch) {
2683
- await handleArtPalette(ctx, paletteMatch, sendJson, sendError);
2821
+ if (url === "/tokens/usage" && req.method === "GET") {
2822
+ await handleTokensUsage(ctx, sendJson);
2684
2823
  return;
2685
2824
  }
2686
- if (analysisMatch) {
2687
- await handleArtAnalysis(ctx, analysisMatch, sendJson, sendError);
2825
+ if (url === "/tokens" && req.method === "GET") {
2826
+ await handleTokensGet(ctx, sendJson);
2688
2827
  return;
2689
2828
  }
2690
- if (docsMatch) {
2691
- await handleArtDocs(ctx, docsMatch, sendJson, sendError);
2829
+ if (url === "/tokens" && req.method === "POST") {
2830
+ await handleTokensCreate(ctx, readBody, sendJson, sendError);
2692
2831
  return;
2693
2832
  }
2694
- if (a11yMatch) {
2695
- handleArtA11y(ctx, a11yMatch, sendJson, sendError);
2833
+ if (url === "/tokens" && req.method === "PUT") {
2834
+ await handleTokensUpdate(ctx, readBody, sendJson, sendError);
2696
2835
  return;
2697
2836
  }
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);
2837
+ if (url === "/tokens" && req.method === "DELETE") {
2838
+ await handleTokensDelete(ctx, readBody, sendJson, sendError);
2708
2839
  return;
2709
2840
  }
2710
- if (url === "/generate") {
2711
- await handleGenerate(body, sendJson, sendError);
2841
+ if (url?.startsWith("/arts/") && req.method === "PUT") {
2842
+ const sourceMatch = url.slice(6).match(/^(.+)\/source$/);
2843
+ if (sourceMatch) {
2844
+ const artPath = decodeURIComponent(sourceMatch[1]);
2845
+ if (!ctx.artFiles.get(artPath)) {
2846
+ sendError("Art not found", 404);
2847
+ return;
2848
+ }
2849
+ const safeArtPath = resolveInside(ctx.config.root, artPath, "art path");
2850
+ const body = await readBody();
2851
+ const { source } = JSON.parse(body);
2852
+ if (typeof source !== "string") {
2853
+ sendError("Missing required field: source", 400);
2854
+ return;
2855
+ }
2856
+ await fs.promises.writeFile(safeArtPath, source, "utf-8");
2857
+ await ctx.processArtFile(safeArtPath);
2858
+ sendJson({ success: true });
2859
+ return;
2860
+ }
2861
+ next();
2712
2862
  return;
2713
2863
  }
2714
- if (url === "/run-vrt") {
2715
- await handleRunVrt(ctx, body, sendJson, sendError);
2864
+ if (url?.startsWith("/arts/") && req.method === "GET") {
2865
+ const rest = url.slice(6);
2866
+ const sourceMatch = rest.match(/^(.+)\/source$/);
2867
+ const paletteMatch = rest.match(/^(.+)\/palette$/);
2868
+ const analysisMatch = rest.match(/^(.+)\/analysis$/);
2869
+ const docsMatch = rest.match(/^(.+)\/docs$/);
2870
+ const a11yMatch = rest.match(/^(.+)\/variants\/([^/]+)\/a11y$/);
2871
+ if (sourceMatch) {
2872
+ await handleArtSource(ctx, sourceMatch, sendJson, sendError);
2873
+ return;
2874
+ }
2875
+ if (paletteMatch) {
2876
+ await handleArtPalette(ctx, paletteMatch, sendJson, sendError);
2877
+ return;
2878
+ }
2879
+ if (analysisMatch) {
2880
+ await handleArtAnalysis(ctx, analysisMatch, sendJson, sendError);
2881
+ return;
2882
+ }
2883
+ if (docsMatch) {
2884
+ await handleArtDocs(ctx, docsMatch, sendJson, sendError);
2885
+ return;
2886
+ }
2887
+ if (a11yMatch) {
2888
+ handleArtA11y(ctx, a11yMatch, sendJson, sendError);
2889
+ return;
2890
+ }
2891
+ const artPath = decodeURIComponent(rest);
2892
+ const art = ctx.artFiles.get(artPath);
2893
+ if (art) sendJson(art);
2894
+ else sendError("Art not found", 404);
2895
+ return;
2896
+ }
2897
+ if (req.method === "POST") {
2898
+ const body = await readBody();
2899
+ if (url === "/preview-with-props") {
2900
+ handlePreviewWithProps(ctx, body, res, sendJson, sendError);
2901
+ return;
2902
+ }
2903
+ if (url === "/generate") {
2904
+ await handleGenerate(ctx, body, sendJson, sendError);
2905
+ return;
2906
+ }
2907
+ if (url === "/run-vrt") {
2908
+ await handleRunVrt(ctx, body, sendJson, sendError);
2909
+ return;
2910
+ }
2911
+ }
2912
+ next();
2913
+ } catch (e) {
2914
+ if (e instanceof HttpError) {
2915
+ sendError(e.message, e.status);
2716
2916
  return;
2717
2917
  }
2918
+ sendError(e instanceof Error ? e.message : String(e));
2718
2919
  }
2719
- next();
2720
2920
  };
2721
2921
  }
2722
2922
  //#endregion
@@ -2855,6 +3055,7 @@ function musea(options = {}) {
2855
3055
  const themeConfig = buildThemeConfig(options.theme);
2856
3056
  const previewCss = options.previewCss ?? [];
2857
3057
  const previewSetup = options.previewSetup;
3058
+ const devSessionToken = createDevSessionToken();
2858
3059
  let config;
2859
3060
  let server = null;
2860
3061
  const artFiles = /* @__PURE__ */ new Map();
@@ -2902,6 +3103,7 @@ function musea(options = {}) {
2902
3103
  devServer.watcher.add(scanRoots);
2903
3104
  registerMiddleware(devServer, {
2904
3105
  basePath,
3106
+ devSessionToken,
2905
3107
  themeConfig,
2906
3108
  artFiles,
2907
3109
  resolvedPreviewCss,
@@ -2914,6 +3116,7 @@ function musea(options = {}) {
2914
3116
  basePath,
2915
3117
  resolvedPreviewCss,
2916
3118
  resolvedPreviewSetup,
3119
+ devSessionToken,
2917
3120
  processArtFile,
2918
3121
  getDevServerPort: () => devServer.config.server.port || 5173
2919
3122
  }));