@json-to-office/jto 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/cli.js +283 -96
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/assets/{HomePage-Dm2fRH6p.js → HomePage-g8gOpx6G.js} +3 -3
  4. package/dist/client/assets/HomePage-g8gOpx6G.js.map +1 -0
  5. package/dist/client/assets/{JsonEditorPage-CeOvPs5I.js → JsonEditorPage-CzMTUrNe.js} +3 -3
  6. package/dist/client/assets/{JsonEditorPage-CeOvPs5I.js.map → JsonEditorPage-CzMTUrNe.js.map} +1 -1
  7. package/dist/client/assets/{MonacoPluginProvider-BDwUsxu4.js → MonacoPluginProvider-CmMAmp3u.js} +3 -3
  8. package/dist/client/assets/{MonacoPluginProvider-BDwUsxu4.js.map → MonacoPluginProvider-CmMAmp3u.js.map} +1 -1
  9. package/dist/client/assets/{button-DpH0SUBp.js → button-BEg2__r6.js} +2 -2
  10. package/dist/client/assets/{button-DpH0SUBp.js.map → button-BEg2__r6.js.map} +1 -1
  11. package/dist/client/assets/{editor-e3ONSdDx.js → editor-DPs_adn-.js} +2 -2
  12. package/dist/client/assets/{editor-e3ONSdDx.js.map → editor-DPs_adn-.js.map} +1 -1
  13. package/dist/client/assets/{editor-monaco-json-CFSP94ky.js → editor-monaco-json-xnoKHnLp.js} +2 -2
  14. package/dist/client/assets/{editor-monaco-json-CFSP94ky.js.map → editor-monaco-json-xnoKHnLp.js.map} +1 -1
  15. package/dist/client/assets/{index-D2uJMa4G.js → index-DgKS-gEZ.js} +3 -3
  16. package/dist/client/assets/{index-D2uJMa4G.js.map → index-DgKS-gEZ.js.map} +1 -1
  17. package/dist/client/assets/preview-RxIHvUjz.js +3 -0
  18. package/dist/client/assets/preview-RxIHvUjz.js.map +1 -0
  19. package/dist/client/index.html +1 -1
  20. package/dist/client/templates/management-plan.pptx.json +18 -18
  21. package/dist/client/templates/minimalist-pitch-deck.pptx.json +19 -19
  22. package/dist/client/templates/modern-annual-report-2.docx.json +12 -12
  23. package/dist/client/templates/modern-annual-report-3.docx.json +14 -14
  24. package/dist/client/templates/standard-annual-report.docx.json +2 -2
  25. package/dist/render-server.js +62 -48
  26. package/dist/render-server.js.map +1 -1
  27. package/package.json +7 -8
  28. package/dist/client/assets/HomePage-Dm2fRH6p.js.map +0 -1
  29. package/dist/client/assets/preview-_aGms4Sr.js +0 -3
  30. package/dist/client/assets/preview-_aGms4Sr.js.map +0 -1
package/dist/cli.js CHANGED
@@ -234,10 +234,10 @@ function collectReferencedNames(config2, customThemes, adapterName) {
234
234
  }
235
235
  function collectReferencedWeights(config2, customThemes) {
236
236
  const weights = /* @__PURE__ */ new Set();
237
- const visit2 = (node) => {
237
+ const visit3 = (node) => {
238
238
  if (node == null) return;
239
239
  if (Array.isArray(node)) {
240
- for (const item of node) visit2(item);
240
+ for (const item of node) visit3(item);
241
241
  return;
242
242
  }
243
243
  if (typeof node === "object") {
@@ -245,21 +245,21 @@ function collectReferencedWeights(config2, customThemes) {
245
245
  if (k === "fontWeight" && typeof v === "number" && v >= 100 && v <= 900) {
246
246
  weights.add(v);
247
247
  } else {
248
- visit2(v);
248
+ visit3(v);
249
249
  }
250
250
  }
251
251
  }
252
252
  };
253
- visit2(config2);
254
- for (const theme of Object.values(customThemes ?? {})) visit2(theme);
253
+ visit3(config2);
254
+ for (const theme of Object.values(customThemes ?? {})) visit3(theme);
255
255
  return weights;
256
256
  }
257
257
  function collectReferencedItalic(config2, customThemes) {
258
258
  let found = false;
259
- const visit2 = (node) => {
259
+ const visit3 = (node) => {
260
260
  if (found || node == null) return;
261
261
  if (Array.isArray(node)) {
262
- for (const item of node) visit2(item);
262
+ for (const item of node) visit3(item);
263
263
  return;
264
264
  }
265
265
  if (typeof node === "object") {
@@ -268,14 +268,14 @@ function collectReferencedItalic(config2, customThemes) {
268
268
  found = true;
269
269
  return;
270
270
  }
271
- visit2(v);
271
+ visit3(v);
272
272
  }
273
273
  }
274
274
  };
275
- visit2(config2);
275
+ visit3(config2);
276
276
  if (!found) {
277
277
  for (const theme of Object.values(customThemes ?? {})) {
278
- visit2(theme);
278
+ visit3(theme);
279
279
  if (found) break;
280
280
  }
281
281
  }
@@ -422,12 +422,15 @@ var init_generator = __esm({
422
422
  }
423
423
  }
424
424
  const bypassCache = options?.bypassCache === true || extraEntries.length > 0;
425
+ const baseDir = typeof options?.baseDir === "string" ? options.baseDir : void 0;
425
426
  const cacheKeyData = {
426
427
  config: config2,
427
428
  customThemes: customThemes && Object.keys(customThemes).length > 0 ? customThemes : null,
428
429
  fontMode: fontMode ?? null,
429
430
  fontSubstitution: fontSubstitution ?? null,
430
- fontStrict: callerStrict ?? null
431
+ fontStrict: callerStrict ?? null,
432
+ // Same definition, different source directory → different local assets.
433
+ baseDir: baseDir ?? null
431
434
  };
432
435
  const cacheKey = this.cacheService.generateCacheKey(cacheKeyData);
433
436
  const hasDynamicContent = this.cacheService.hasDynamicContent(config2);
@@ -464,13 +467,15 @@ var init_generator = __esm({
464
467
  const plugins = registry.getPlugins();
465
468
  const generator = await this.adapter.createGenerator(plugins, {
466
469
  customThemes,
467
- fonts: fontOpts
470
+ fonts: fontOpts,
471
+ baseDir
468
472
  });
469
473
  buffer = await generator.generateBuffer(config2);
470
474
  } else {
471
475
  buffer = await this.adapter.generateBuffer(config2, {
472
476
  customThemes,
473
- fonts: fontOpts
477
+ fonts: fontOpts,
478
+ baseDir
474
479
  });
475
480
  }
476
481
  this.cacheService.set(cacheKey, buffer, config2, {
@@ -1413,7 +1418,12 @@ var init_loose = __esm({
1413
1418
  {
1414
1419
  bypassCache: Type.Optional(Type.Boolean()),
1415
1420
  returnUrl: Type.Optional(Type.Boolean()),
1416
- fonts: Type.Optional(FontOptionsSchema)
1421
+ fonts: Type.Optional(FontOptionsSchema),
1422
+ // Name of the discovered document this definition came from; the
1423
+ // server maps it to that document's directory so relative asset
1424
+ // paths resolve against it (#142). Names, never paths — a client
1425
+ // cannot point the server at an arbitrary directory.
1426
+ sourceName: Type.Optional(Type.String())
1417
1427
  },
1418
1428
  { additionalProperties: true }
1419
1429
  )
@@ -1630,36 +1640,36 @@ function isPrivateHost(host) {
1630
1640
  const firstIpv6Group = Number.parseInt(normalized.split(":")[0] || "0", 16);
1631
1641
  return firstIpv6Group >= 64512 && firstIpv6Group <= 65023 || firstIpv6Group >= 65152 && firstIpv6Group <= 65215;
1632
1642
  }
1633
- function assertAllowedUrl(source, path6, allowedHosts) {
1643
+ function assertAllowedUrl(source, path8, allowedHosts) {
1634
1644
  if (source.startsWith("data:")) return;
1635
1645
  let url;
1636
1646
  try {
1637
1647
  url = new URL(source);
1638
1648
  } catch {
1639
1649
  throw new UnsafeOutboundSourceError(
1640
- path6,
1650
+ path8,
1641
1651
  "local and relative file paths are disabled for HTTP requests"
1642
1652
  );
1643
1653
  }
1644
1654
  if (url.protocol !== "https:") {
1645
- throw new UnsafeOutboundSourceError(path6, "only HTTPS URLs are allowed");
1655
+ throw new UnsafeOutboundSourceError(path8, "only HTTPS URLs are allowed");
1646
1656
  }
1647
1657
  if (url.username || url.password) {
1648
1658
  throw new UnsafeOutboundSourceError(
1649
- path6,
1659
+ path8,
1650
1660
  "URLs containing credentials are not allowed"
1651
1661
  );
1652
1662
  }
1653
1663
  const host = url.hostname.toLowerCase().replace(/\.$/, "");
1654
1664
  if (isPrivateHost(host)) {
1655
1665
  throw new UnsafeOutboundSourceError(
1656
- path6,
1666
+ path8,
1657
1667
  `private or local host "${host}" is not allowed`
1658
1668
  );
1659
1669
  }
1660
1670
  if (!allowedHosts.some((pattern) => hostMatches(host, pattern))) {
1661
1671
  throw new UnsafeOutboundSourceError(
1662
- path6,
1672
+ path8,
1663
1673
  `host "${host}" is not in OUTBOUND_HOST_ALLOWLIST`
1664
1674
  );
1665
1675
  }
@@ -1677,13 +1687,13 @@ function extractCssReferences(input) {
1677
1687
  function extractAbsoluteUrls(input) {
1678
1688
  return input.match(/(?:https?|file):\/\/[^\s'"<>)}\]]+/gi) ?? [];
1679
1689
  }
1680
- function assertSafeSvg(svg, path6, allowedHosts) {
1690
+ function assertSafeSvg(svg, path8, allowedHosts) {
1681
1691
  if (/<\s*script\b/i.test(svg) || /<\s*(?:foreignObject|iframe|object|embed)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg)) {
1682
- throw new UnsafeOutboundSourceError(path6, "active SVG content is disabled");
1692
+ throw new UnsafeOutboundSourceError(path8, "active SVG content is disabled");
1683
1693
  }
1684
1694
  if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svg)) {
1685
1695
  throw new UnsafeOutboundSourceError(
1686
- path6,
1696
+ path8,
1687
1697
  "SVG document types and entities are disabled"
1688
1698
  );
1689
1699
  }
@@ -1692,20 +1702,20 @@ function assertSafeSvg(svg, path6, allowedHosts) {
1692
1702
  while (match = refPattern.exec(svg)) {
1693
1703
  const ref = match[2].trim();
1694
1704
  if (!ref || ref.startsWith("#") || ref.startsWith("data:")) continue;
1695
- assertAllowedUrl(ref, path6, allowedHosts);
1705
+ assertAllowedUrl(ref, path8, allowedHosts);
1696
1706
  }
1697
1707
  for (const ref of extractCssReferences(svg)) {
1698
1708
  if (ref.startsWith("#") || ref.startsWith("data:")) continue;
1699
- assertAllowedUrl(ref, path6, allowedHosts);
1709
+ assertAllowedUrl(ref, path8, allowedHosts);
1700
1710
  }
1701
1711
  }
1702
- function assertSafeEncodedImage(source, path6, allowedHosts) {
1712
+ function assertSafeEncodedImage(source, path8, allowedHosts) {
1703
1713
  let candidate = source.trim();
1704
1714
  let declaredSvg = false;
1705
1715
  if (candidate.startsWith("data:")) {
1706
1716
  const comma = candidate.indexOf(",");
1707
1717
  if (comma < 0) {
1708
- throw new UnsafeOutboundSourceError(path6, "malformed image data URL");
1718
+ throw new UnsafeOutboundSourceError(path8, "malformed image data URL");
1709
1719
  }
1710
1720
  const metadata = candidate.slice(5, comma);
1711
1721
  declaredSvg = metadata.split(";")[0].toLowerCase() === "image/svg+xml";
@@ -1714,7 +1724,7 @@ function assertSafeEncodedImage(source, path6, allowedHosts) {
1714
1724
  try {
1715
1725
  candidate = /(?:^|;)base64(?:;|$)/i.test(metadata) ? Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
1716
1726
  } catch {
1717
- throw new UnsafeOutboundSourceError(path6, "malformed SVG data URL");
1727
+ throw new UnsafeOutboundSourceError(path8, "malformed SVG data URL");
1718
1728
  }
1719
1729
  } else if (!/<\s*svg\b/i.test(candidate)) {
1720
1730
  candidate = Buffer.from(candidate, "base64").toString("utf8");
@@ -1722,37 +1732,37 @@ function assertSafeEncodedImage(source, path6, allowedHosts) {
1722
1732
  const svgStart = candidate.search(/<\s*svg\b/i);
1723
1733
  if (svgStart < 0) {
1724
1734
  if (declaredSvg) {
1725
- throw new UnsafeOutboundSourceError(path6, "malformed SVG image data");
1735
+ throw new UnsafeOutboundSourceError(path8, "malformed SVG image data");
1726
1736
  }
1727
1737
  return;
1728
1738
  }
1729
- assertSafeSvg(candidate, path6, allowedHosts);
1739
+ assertSafeSvg(candidate, path8, allowedHosts);
1730
1740
  }
1731
- function assertSafeResources(resources, path6, allowedHosts) {
1741
+ function assertSafeResources(resources, path8, allowedHosts) {
1732
1742
  if (typeof resources.js === "string" && resources.js.trim()) {
1733
1743
  throw new UnsafeOutboundSourceError(
1734
- `${path6}.js`,
1744
+ `${path8}.js`,
1735
1745
  "remote renderer JavaScript resources are disabled"
1736
1746
  );
1737
1747
  }
1738
1748
  if (typeof resources.css === "string") {
1739
1749
  for (const ref of extractCssReferences(resources.css)) {
1740
1750
  if (ref.startsWith("data:")) continue;
1741
- assertAllowedUrl(ref, `${path6}.css`, allowedHosts);
1751
+ assertAllowedUrl(ref, `${path8}.css`, allowedHosts);
1742
1752
  }
1743
1753
  }
1744
1754
  if (Array.isArray(resources.files) && resources.files.length > 0) {
1745
1755
  throw new UnsafeOutboundSourceError(
1746
- `${path6}.files`,
1756
+ `${path8}.files`,
1747
1757
  "remote renderer JavaScript resources are disabled"
1748
1758
  );
1749
1759
  }
1750
1760
  }
1751
- function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1761
+ function assertNoRemoteReferences(value, path8, allowedHosts, seen) {
1752
1762
  if (typeof value === "string") {
1753
1763
  if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource)\s*\(/i.test(value) || /\b(?:javascript|file):/i.test(value)) {
1754
1764
  throw new UnsafeOutboundSourceError(
1755
- path6,
1765
+ path8,
1756
1766
  "network-capable JavaScript and file URLs are disabled"
1757
1767
  );
1758
1768
  }
@@ -1761,11 +1771,11 @@ function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1761
1771
  ...extractCssReferences(value)
1762
1772
  ]) {
1763
1773
  if (url.startsWith("data:")) continue;
1764
- assertAllowedUrl(url, path6, allowedHosts);
1774
+ assertAllowedUrl(url, path8, allowedHosts);
1765
1775
  }
1766
1776
  if (/^\/\//.test(value.trim())) {
1767
1777
  throw new UnsafeOutboundSourceError(
1768
- path6,
1778
+ path8,
1769
1779
  "protocol-relative URLs are disabled"
1770
1780
  );
1771
1781
  }
@@ -1775,20 +1785,20 @@ function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1775
1785
  seen.add(value);
1776
1786
  if (Array.isArray(value)) {
1777
1787
  value.forEach(
1778
- (entry, index) => assertNoRemoteReferences(entry, `${path6}[${index}]`, allowedHosts, seen)
1788
+ (entry, index) => assertNoRemoteReferences(entry, `${path8}[${index}]`, allowedHosts, seen)
1779
1789
  );
1780
1790
  return;
1781
1791
  }
1782
1792
  for (const [key, child] of Object.entries(value)) {
1783
- assertNoRemoteReferences(child, `${path6}.${key}`, allowedHosts, seen);
1793
+ assertNoRemoteReferences(child, `${path8}.${key}`, allowedHosts, seen);
1784
1794
  }
1785
1795
  }
1786
- function visit(value, path6, containerKey, allowedHosts, seen) {
1796
+ function visit(value, path8, containerKey, allowedHosts, seen) {
1787
1797
  if (!value || typeof value !== "object" || seen.has(value)) return;
1788
1798
  seen.add(value);
1789
1799
  if (Array.isArray(value)) {
1790
1800
  value.forEach(
1791
- (entry, index) => visit(entry, `${path6}[${index}]`, containerKey, allowedHosts, seen)
1801
+ (entry, index) => visit(entry, `${path8}[${index}]`, containerKey, allowedHosts, seen)
1792
1802
  );
1793
1803
  return;
1794
1804
  }
@@ -1796,7 +1806,7 @@ function visit(value, path6, containerKey, allowedHosts, seen) {
1796
1806
  for (const key of Object.keys(record)) {
1797
1807
  if (DANGEROUS_KEYS.has(key)) {
1798
1808
  throw new UnsafeOutboundSourceError(
1799
- `${path6}.${key}`,
1809
+ `${path8}.${key}`,
1800
1810
  "prototype mutation keys are disabled"
1801
1811
  );
1802
1812
  }
@@ -1804,31 +1814,31 @@ function visit(value, path6, containerKey, allowedHosts, seen) {
1804
1814
  const kind = typeof record.kind === "string" ? record.kind : void 0;
1805
1815
  if (kind === "file" && typeof record.path === "string") {
1806
1816
  throw new UnsafeOutboundSourceError(
1807
- `${path6}.path`,
1817
+ `${path8}.path`,
1808
1818
  "local file sources are disabled for HTTP requests"
1809
1819
  );
1810
1820
  }
1811
1821
  if ((kind === "url" || kind === "variable") && typeof record.url === "string") {
1812
- assertAllowedUrl(record.url, `${path6}.url`, allowedHosts);
1822
+ assertAllowedUrl(record.url, `${path8}.url`, allowedHosts);
1813
1823
  }
1814
1824
  const componentName = typeof record.name === "string" ? record.name.toLowerCase() : void 0;
1815
1825
  const props = isRecord(record.props) ? record.props : void 0;
1816
1826
  if (componentName && props) {
1817
1827
  if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.path === "string") {
1818
- assertAllowedUrl(props.path, `${path6}.props.path`, allowedHosts);
1819
- assertSafeEncodedImage(props.path, `${path6}.props.path`, allowedHosts);
1828
+ assertAllowedUrl(props.path, `${path8}.props.path`, allowedHosts);
1829
+ assertSafeEncodedImage(props.path, `${path8}.props.path`, allowedHosts);
1820
1830
  }
1821
1831
  if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.base64 === "string") {
1822
1832
  assertSafeEncodedImage(
1823
1833
  props.base64,
1824
- `${path6}.props.base64`,
1834
+ `${path8}.props.base64`,
1825
1835
  allowedHosts
1826
1836
  );
1827
1837
  }
1828
1838
  if ((componentName === "highcharts" || componentName === "visual") && typeof props.serverUrl === "string") {
1829
1839
  assertAllowedUrl(
1830
1840
  props.serverUrl,
1831
- `${path6}.props.serverUrl`,
1841
+ `${path8}.props.serverUrl`,
1832
1842
  allowedHosts
1833
1843
  );
1834
1844
  }
@@ -1836,30 +1846,30 @@ function visit(value, path6, containerKey, allowedHosts, seen) {
1836
1846
  if (isRecord(props.resources)) {
1837
1847
  assertSafeResources(
1838
1848
  props.resources,
1839
- `${path6}.props.resources`,
1849
+ `${path8}.props.resources`,
1840
1850
  allowedHosts
1841
1851
  );
1842
1852
  }
1843
1853
  assertNoRemoteReferences(
1844
1854
  props.options,
1845
- `${path6}.props.options`,
1855
+ `${path8}.props.options`,
1846
1856
  allowedHosts,
1847
1857
  /* @__PURE__ */ new WeakSet()
1848
1858
  );
1849
1859
  }
1850
1860
  }
1851
1861
  if (containerKey === "image" && typeof record.path === "string") {
1852
- assertAllowedUrl(record.path, `${path6}.path`, allowedHosts);
1853
- assertSafeEncodedImage(record.path, `${path6}.path`, allowedHosts);
1862
+ assertAllowedUrl(record.path, `${path8}.path`, allowedHosts);
1863
+ assertSafeEncodedImage(record.path, `${path8}.path`, allowedHosts);
1854
1864
  }
1855
1865
  if (typeof record.svg === "string") {
1856
- assertSafeSvg(record.svg, `${path6}.svg`, allowedHosts);
1866
+ assertSafeSvg(record.svg, `${path8}.svg`, allowedHosts);
1857
1867
  }
1858
1868
  if (containerKey === "resources") {
1859
- assertSafeResources(record, path6, allowedHosts);
1869
+ assertSafeResources(record, path8, allowedHosts);
1860
1870
  }
1861
1871
  for (const [key, child] of Object.entries(record)) {
1862
- visit(child, `${path6}.${key}`, key, allowedHosts, seen);
1872
+ visit(child, `${path8}.${key}`, key, allowedHosts, seen);
1863
1873
  }
1864
1874
  }
1865
1875
  function assertSafeOutboundSources(value, policy, rootPath = "request") {
@@ -1880,9 +1890,9 @@ var init_outbound_source_policy = __esm({
1880
1890
  "use strict";
1881
1891
  init_esm_shims();
1882
1892
  UnsafeOutboundSourceError = class extends Error {
1883
- constructor(path6, reason) {
1884
- super(`Unsafe outbound source at ${path6}: ${reason}`);
1885
- this.path = path6;
1893
+ constructor(path8, reason) {
1894
+ super(`Unsafe outbound source at ${path8}: ${reason}`);
1895
+ this.path = path8;
1886
1896
  this.name = "UnsafeOutboundSourceError";
1887
1897
  }
1888
1898
  };
@@ -1892,6 +1902,7 @@ var init_outbound_source_policy = __esm({
1892
1902
  });
1893
1903
 
1894
1904
  // src/server/rasterize-route.ts
1905
+ import path6 from "path";
1895
1906
  import { Type as Type2 } from "@sinclair/typebox";
1896
1907
  import { bodyLimit } from "hono/body-limit";
1897
1908
  import { HTTPException as HTTPException3 } from "hono/http-exception";
@@ -1922,7 +1933,18 @@ function registerRasterizeRoute(router, options = {}) {
1922
1933
  jsonOnly,
1923
1934
  tbValidator(RasterizeRequestSchema),
1924
1935
  async (c) => {
1925
- const { presentation, dpi } = getValidated(c, "json");
1936
+ const { presentation, dpi, baseDir } = getValidated(c, "json");
1937
+ let safeBaseDir;
1938
+ if (baseDir !== void 0) {
1939
+ const resolved = path6.resolve(baseDir);
1940
+ const cwd = process.cwd();
1941
+ if (resolved !== cwd && !resolved.startsWith(cwd + path6.sep)) {
1942
+ throw new HTTPException3(400, {
1943
+ message: "baseDir must be inside the server working directory"
1944
+ });
1945
+ }
1946
+ safeBaseDir = resolved;
1947
+ }
1926
1948
  try {
1927
1949
  if (options.sourcePolicy) {
1928
1950
  assertSafeOutboundSources(
@@ -1933,7 +1955,8 @@ function registerRasterizeRoute(router, options = {}) {
1933
1955
  }
1934
1956
  const result = await getRasterizer()({
1935
1957
  presentation,
1936
- dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI)
1958
+ dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI),
1959
+ baseDir: safeBaseDir
1937
1960
  });
1938
1961
  return c.json(result);
1939
1962
  } catch (error) {
@@ -1968,7 +1991,8 @@ var init_rasterize_route = __esm({
1968
1991
  presentation: Type2.Object({}, { additionalProperties: true }),
1969
1992
  dpi: Type2.Optional(
1970
1993
  Type2.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
1971
- )
1994
+ ),
1995
+ baseDir: Type2.Optional(Type2.String())
1972
1996
  },
1973
1997
  { additionalProperties: false }
1974
1998
  );
@@ -1984,16 +2008,132 @@ var init_rasterize_route = __esm({
1984
2008
  }
1985
2009
  });
1986
2010
 
2011
+ // src/server/services/template-media-inliner.ts
2012
+ import { promises as fs5 } from "fs";
2013
+ import path7 from "path";
2014
+ function isRecord2(value) {
2015
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2016
+ }
2017
+ function isInlineCandidate(source) {
2018
+ if (!source || source.includes("\0")) return false;
2019
+ if (source.startsWith("data:")) return false;
2020
+ if (path7.isAbsolute(source)) return false;
2021
+ try {
2022
+ new URL(source);
2023
+ return false;
2024
+ } catch {
2025
+ return true;
2026
+ }
2027
+ }
2028
+ async function toDataUrl(source, baseDir, state) {
2029
+ let cacheKey = source;
2030
+ let result = null;
2031
+ try {
2032
+ const resolved = await fs5.realpath(path7.resolve(baseDir, source));
2033
+ cacheKey = resolved;
2034
+ const cached2 = state.cache.get(cacheKey);
2035
+ if (cached2 !== void 0) return cached2;
2036
+ const contained = resolved === state.baseDirReal || resolved.startsWith(state.baseDirReal + path7.sep);
2037
+ if (contained) {
2038
+ const mime = MIME_BY_EXTENSION[path7.extname(resolved).toLowerCase()];
2039
+ if (mime) {
2040
+ const stat = await fs5.stat(resolved);
2041
+ if (stat.isFile() && stat.size <= state.maxFileBytes && stat.size <= state.totalBudget) {
2042
+ const buffer = await fs5.readFile(resolved);
2043
+ state.totalBudget -= buffer.length;
2044
+ result = `data:${mime};base64,${buffer.toString("base64")}`;
2045
+ }
2046
+ }
2047
+ }
2048
+ } catch {
2049
+ }
2050
+ state.cache.set(cacheKey, result);
2051
+ return result;
2052
+ }
2053
+ async function visit2(value, containerKey, baseDir, state, seen) {
2054
+ if (!value || typeof value !== "object" || seen.has(value)) return;
2055
+ seen.add(value);
2056
+ if (Array.isArray(value)) {
2057
+ for (const entry of value) {
2058
+ await visit2(entry, containerKey, baseDir, state, seen);
2059
+ }
2060
+ return;
2061
+ }
2062
+ const record = value;
2063
+ const componentName = typeof record.name === "string" ? record.name.toLowerCase() : void 0;
2064
+ const props = isRecord2(record.props) ? record.props : void 0;
2065
+ if (componentName === "image" && props && typeof props.path === "string" && isInlineCandidate(props.path)) {
2066
+ const inlined = await toDataUrl(props.path, baseDir, state);
2067
+ if (inlined !== null) props.path = inlined;
2068
+ }
2069
+ if (containerKey === "image" && typeof record.path === "string" && isInlineCandidate(record.path)) {
2070
+ const inlined = await toDataUrl(record.path, baseDir, state);
2071
+ if (inlined !== null) record.path = inlined;
2072
+ }
2073
+ for (const [key, child] of Object.entries(record)) {
2074
+ await visit2(child, key, baseDir, state, seen);
2075
+ }
2076
+ }
2077
+ async function inlineTemplateMedia(jsonDefinition, baseDir, options = {}) {
2078
+ if (typeof jsonDefinition === "string") {
2079
+ try {
2080
+ jsonDefinition = JSON.parse(jsonDefinition);
2081
+ } catch {
2082
+ return jsonDefinition;
2083
+ }
2084
+ }
2085
+ if (!jsonDefinition || typeof jsonDefinition !== "object") {
2086
+ return jsonDefinition;
2087
+ }
2088
+ let baseDirReal;
2089
+ try {
2090
+ baseDirReal = await fs5.realpath(baseDir);
2091
+ } catch {
2092
+ return jsonDefinition;
2093
+ }
2094
+ const clone = structuredClone(jsonDefinition);
2095
+ const state = {
2096
+ baseDirReal,
2097
+ maxFileBytes: options.maxFileBytes ?? 10 * 1024 * 1024,
2098
+ totalBudget: options.maxTotalBytes ?? 32 * 1024 * 1024,
2099
+ cache: /* @__PURE__ */ new Map()
2100
+ };
2101
+ await visit2(clone, void 0, baseDir, state, /* @__PURE__ */ new WeakSet());
2102
+ return clone;
2103
+ }
2104
+ var MIME_BY_EXTENSION;
2105
+ var init_template_media_inliner = __esm({
2106
+ "src/server/services/template-media-inliner.ts"() {
2107
+ "use strict";
2108
+ init_esm_shims();
2109
+ MIME_BY_EXTENSION = {
2110
+ ".png": "image/png",
2111
+ ".jpg": "image/jpeg",
2112
+ ".jpeg": "image/jpeg",
2113
+ ".gif": "image/gif",
2114
+ ".svg": "image/svg+xml",
2115
+ ".webp": "image/webp",
2116
+ ".bmp": "image/bmp",
2117
+ ".tiff": "image/tiff",
2118
+ ".tif": "image/tiff"
2119
+ };
2120
+ }
2121
+ });
2122
+
1987
2123
  // src/server/routes/format.ts
1988
2124
  import { Hono as Hono2 } from "hono";
1989
2125
  import { HTTPException as HTTPException4 } from "hono/http-exception";
1990
2126
  import { bodyLimit as bodyLimit2 } from "hono/body-limit";
1991
- import { PluginRegistry as PluginRegistry2 } from "@json-to-office/jto-cli";
2127
+ import {
2128
+ PluginRegistry as PluginRegistry2,
2129
+ PluginDiscoveryService
2130
+ } from "@json-to-office/jto-cli";
2131
+ import { dirname } from "path";
1992
2132
  function createFormatRouter(adapter) {
1993
2133
  const router = new Hono2();
1994
- const assertRequestSources = (value, path6) => {
2134
+ const assertRequestSources = (value, path8) => {
1995
2135
  try {
1996
- assertSafeOutboundSources(value, config.outboundSources, path6);
2136
+ assertSafeOutboundSources(value, config.outboundSources, path8);
1997
2137
  } catch (error) {
1998
2138
  if (error instanceof UnsafeOutboundSourceError) {
1999
2139
  throw new HTTPException4(400, { message: error.message });
@@ -2001,6 +2141,35 @@ function createFormatRouter(adapter) {
2001
2141
  throw error;
2002
2142
  }
2003
2143
  };
2144
+ const resolveSourceBaseDir = async (options) => {
2145
+ const sourceName = options?.sourceName;
2146
+ if (typeof sourceName !== "string" || sourceName.length === 0) {
2147
+ return void 0;
2148
+ }
2149
+ try {
2150
+ const discovery = new PluginDiscoveryService({
2151
+ maxDepth: 10,
2152
+ includeNodeModules: false,
2153
+ verbose: false
2154
+ });
2155
+ const documents = await discovery.discoverDocuments(
2156
+ adapter.name
2157
+ );
2158
+ const match = documents.find((doc) => doc.name === sourceName);
2159
+ return match ? dirname(match.path) : void 0;
2160
+ } catch {
2161
+ return void 0;
2162
+ }
2163
+ };
2164
+ const inlineDiscoveredMedia = async (jsonDefinition, baseDir) => {
2165
+ if (baseDir === void 0 || config.outboundSources.mode === "development") {
2166
+ return jsonDefinition;
2167
+ }
2168
+ return inlineTemplateMedia(jsonDefinition, baseDir, {
2169
+ maxFileBytes: config.requestLimits.maxFileSize,
2170
+ maxTotalBytes: config.requestLimits.maxBodySize
2171
+ });
2172
+ };
2004
2173
  const contentTypeMw = async (c, next) => {
2005
2174
  const contentType = c.req.header("content-type");
2006
2175
  if (!contentType || !contentType.includes("application/json")) {
@@ -2024,7 +2193,12 @@ function createFormatRouter(adapter) {
2024
2193
  const { jsonDefinition, customThemes, options } = getValidated(c, "json");
2025
2194
  const requestId = c.get("requestId");
2026
2195
  try {
2027
- assertRequestSources(jsonDefinition, "jsonDefinition");
2196
+ const baseDir = await resolveSourceBaseDir(options);
2197
+ const effectiveDefinition = await inlineDiscoveredMedia(
2198
+ jsonDefinition,
2199
+ baseDir
2200
+ );
2201
+ assertRequestSources(effectiveDefinition, "jsonDefinition");
2028
2202
  assertRequestSources(customThemes, "customThemes");
2029
2203
  assertRequestSources(options, "options");
2030
2204
  const bypassCache = c.req.header("X-Bypass-Cache") === "true" || c.req.query("bypass-cache") === "true" || options?.bypassCache === true;
@@ -2036,12 +2210,15 @@ function createFormatRouter(adapter) {
2036
2210
  } else if (rawFonts) {
2037
2211
  sanitizedFonts = rawFonts;
2038
2212
  }
2213
+ const clientOptions = { ...options };
2214
+ delete clientOptions.baseDir;
2039
2215
  const result = await generatorService.generate({
2040
- jsonDefinition,
2216
+ jsonDefinition: effectiveDefinition,
2041
2217
  customThemes,
2042
2218
  options: {
2043
- ...options,
2219
+ ...clientOptions,
2044
2220
  ...sanitizedFonts !== void 0 && { fonts: sanitizedFonts },
2221
+ ...baseDir !== void 0 && { baseDir },
2045
2222
  bypassCache
2046
2223
  }
2047
2224
  });
@@ -2298,14 +2475,23 @@ function createFormatRouter(adapter) {
2298
2475
  const libreOfficeService = getContainer().get(
2299
2476
  "libreOfficeConverterService"
2300
2477
  );
2301
- const { jsonDefinition, customThemes } = getValidated(c, "json");
2478
+ const { jsonDefinition, customThemes, options } = getValidated(c, "json");
2302
2479
  try {
2303
- assertRequestSources(jsonDefinition, "jsonDefinition");
2480
+ const baseDir = await resolveSourceBaseDir(options);
2481
+ const effectiveDefinition = await inlineDiscoveredMedia(
2482
+ jsonDefinition,
2483
+ baseDir
2484
+ );
2485
+ assertRequestSources(effectiveDefinition, "jsonDefinition");
2304
2486
  assertRequestSources(customThemes, "customThemes");
2487
+ assertRequestSources(options, "options");
2305
2488
  const generated = await generatorService.generate({
2306
- jsonDefinition,
2489
+ jsonDefinition: effectiveDefinition,
2307
2490
  customThemes,
2308
- options: { bypassCache: true }
2491
+ options: {
2492
+ bypassCache: true,
2493
+ ...baseDir !== void 0 && { baseDir }
2494
+ }
2309
2495
  });
2310
2496
  const pdfBuffer = await libreOfficeService.convertToPdf(
2311
2497
  generated.buffer,
@@ -2473,13 +2659,14 @@ var init_format = __esm({
2473
2659
  init_config();
2474
2660
  init_outbound_source_policy();
2475
2661
  init_libreoffice_converter();
2662
+ init_template_media_inliner();
2476
2663
  }
2477
2664
  });
2478
2665
 
2479
2666
  // src/server/routes/discovery.ts
2480
2667
  import { Hono as Hono3 } from "hono";
2481
2668
  import {
2482
- PluginDiscoveryService,
2669
+ PluginDiscoveryService as PluginDiscoveryService2,
2483
2670
  PluginRegistry as PluginRegistry3
2484
2671
  } from "@json-to-office/jto-cli";
2485
2672
  import { latestVersion } from "@json-to-office/shared";
@@ -2590,7 +2777,7 @@ var init_discovery = __esm({
2590
2777
  discoveryRouter.get("/all", async (c) => {
2591
2778
  try {
2592
2779
  const format = Container.getAdapter().name;
2593
- const discovery = new PluginDiscoveryService({
2780
+ const discovery = new PluginDiscoveryService2({
2594
2781
  maxDepth: 10,
2595
2782
  includeNodeModules: false,
2596
2783
  verbose: false
@@ -2620,7 +2807,7 @@ var init_discovery = __esm({
2620
2807
  const includeSchemas = c.req.query("schemas") === "true";
2621
2808
  const includeExamples = c.req.query("examples") === "true";
2622
2809
  const format = Container.getAdapter().name;
2623
- const discovery = new PluginDiscoveryService({
2810
+ const discovery = new PluginDiscoveryService2({
2624
2811
  maxDepth: 10,
2625
2812
  includeNodeModules: false,
2626
2813
  verbose: false
@@ -2644,7 +2831,7 @@ var init_discovery = __esm({
2644
2831
  discoveryRouter.get("/documents", async (c) => {
2645
2832
  try {
2646
2833
  const format = Container.getAdapter().name;
2647
- const discovery = new PluginDiscoveryService({
2834
+ const discovery = new PluginDiscoveryService2({
2648
2835
  maxDepth: 10,
2649
2836
  includeNodeModules: false,
2650
2837
  verbose: false
@@ -2659,7 +2846,7 @@ var init_discovery = __esm({
2659
2846
  discoveryRouter.get("/themes", async (c) => {
2660
2847
  try {
2661
2848
  const format = Container.getAdapter().name;
2662
- const discovery = new PluginDiscoveryService({
2849
+ const discovery = new PluginDiscoveryService2({
2663
2850
  maxDepth: 10,
2664
2851
  includeNodeModules: false,
2665
2852
  verbose: false
@@ -2675,7 +2862,7 @@ var init_discovery = __esm({
2675
2862
  try {
2676
2863
  const pluginName = c.req.param("name");
2677
2864
  const format = Container.getAdapter().name;
2678
- const discovery = new PluginDiscoveryService({
2865
+ const discovery = new PluginDiscoveryService2({
2679
2866
  maxDepth: 10,
2680
2867
  includeNodeModules: false,
2681
2868
  verbose: false
@@ -2711,7 +2898,7 @@ var init_discovery = __esm({
2711
2898
  try {
2712
2899
  const name = c.req.param("name");
2713
2900
  const format = Container.getAdapter().name;
2714
- const discovery = new PluginDiscoveryService({
2901
+ const discovery = new PluginDiscoveryService2({
2715
2902
  maxDepth: 10,
2716
2903
  includeNodeModules: false,
2717
2904
  verbose: false
@@ -2727,7 +2914,7 @@ var init_discovery = __esm({
2727
2914
  try {
2728
2915
  const name = c.req.param("name");
2729
2916
  const format = Container.getAdapter().name;
2730
- const discovery = new PluginDiscoveryService({
2917
+ const discovery = new PluginDiscoveryService2({
2731
2918
  maxDepth: 10,
2732
2919
  includeNodeModules: false,
2733
2920
  verbose: false
@@ -2850,7 +3037,7 @@ var init_ai_schema = __esm({
2850
3037
 
2851
3038
  // src/server/services/prompt-loader.ts
2852
3039
  import { readFileSync, existsSync } from "fs";
2853
- import { join, dirname } from "path";
3040
+ import { join, dirname as dirname2 } from "path";
2854
3041
  import { fileURLToPath as fileURLToPath2 } from "url";
2855
3042
  function resolvePromptsDir() {
2856
3043
  const candidates = [
@@ -2885,7 +3072,7 @@ var init_prompt_loader = __esm({
2885
3072
  "src/server/services/prompt-loader.ts"() {
2886
3073
  "use strict";
2887
3074
  init_esm_shims();
2888
- __dirname2 = dirname(fileURLToPath2(import.meta.url));
3075
+ __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2889
3076
  PROMPTS_DIR = resolvePromptsDir();
2890
3077
  cache = /* @__PURE__ */ new Map();
2891
3078
  }
@@ -3760,7 +3947,7 @@ import { Hono as Hono7 } from "hono";
3760
3947
  import { serve } from "@hono/node-server";
3761
3948
  import { logger as logger2 } from "hono/logger";
3762
3949
  import { compress } from "hono/compress";
3763
- import { resolve, dirname as dirname2 } from "path";
3950
+ import { resolve, dirname as dirname3 } from "path";
3764
3951
  import { fileURLToPath as fileURLToPath3 } from "url";
3765
3952
  var __filename2, __dirname3, UnifiedServer;
3766
3953
  var init_unified_server = __esm({
@@ -3770,7 +3957,7 @@ var init_unified_server = __esm({
3770
3957
  init_app();
3771
3958
  init_logger();
3772
3959
  __filename2 = fileURLToPath3(import.meta.url);
3773
- __dirname3 = dirname2(__filename2);
3960
+ __dirname3 = dirname3(__filename2);
3774
3961
  UnifiedServer = class {
3775
3962
  app;
3776
3963
  config;
@@ -3806,9 +3993,9 @@ var init_unified_server = __esm({
3806
3993
  return await apiApp.fetch(c.req.raw, c.env);
3807
3994
  });
3808
3995
  this.app.use("*", async (c, next) => {
3809
- const path6 = c.req.path;
3810
- if (path6.startsWith("/api")) {
3811
- if (/\.(ts|tsx|js|jsx|css|map)$/.test(path6)) {
3996
+ const path8 = c.req.path;
3997
+ if (path8.startsWith("/api")) {
3998
+ if (/\.(ts|tsx|js|jsx|css|map)$/.test(path8)) {
3812
3999
  return next();
3813
4000
  }
3814
4001
  const response = await apiApp.fetch(c.req.raw, c.env);
@@ -3911,8 +4098,8 @@ var init_unified_server = __esm({
3911
4098
  }
3912
4099
  if (this.viteServer) {
3913
4100
  this.app.use("*", async (c, next) => {
3914
- const path6 = c.req.path;
3915
- if ((path6.startsWith("/api") || path6 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path6)) {
4101
+ const path8 = c.req.path;
4102
+ if ((path8.startsWith("/api") || path8 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path8)) {
3916
4103
  return next();
3917
4104
  }
3918
4105
  const env = c.env;
@@ -3931,7 +4118,7 @@ var init_unified_server = __esm({
3931
4118
  }
3932
4119
  async setupBuiltClient(clientPath) {
3933
4120
  const { serveStatic } = await import("@hono/node-server/serve-static");
3934
- const fs5 = await import("fs");
4121
+ const fs6 = await import("fs");
3935
4122
  const { extname } = await import("path");
3936
4123
  const mime = await import("mime-types");
3937
4124
  const format = this.adapter.name.replace(/[^a-zA-Z0-9]/g, "");
@@ -3939,7 +4126,7 @@ var init_unified_server = __esm({
3939
4126
  "/assets/*",
3940
4127
  serveStatic({
3941
4128
  root: clientPath,
3942
- rewriteRequestPath: (path6) => path6.replace(/^\/assets/, "/assets")
4129
+ rewriteRequestPath: (path8) => path8.replace(/^\/assets/, "/assets")
3943
4130
  })
3944
4131
  );
3945
4132
  this.app.use("/*", async (c, next) => {
@@ -3948,9 +4135,9 @@ var init_unified_server = __esm({
3948
4135
  const ext = extname(reqPath);
3949
4136
  if (ext) {
3950
4137
  const filePath = resolve(clientPath, reqPath.slice(1));
3951
- if (fs5.existsSync(filePath)) {
4138
+ if (fs6.existsSync(filePath)) {
3952
4139
  const mimeType = mime.lookup(filePath) || "application/octet-stream";
3953
- const content = fs5.readFileSync(filePath);
4140
+ const content = fs6.readFileSync(filePath);
3954
4141
  c.header("Content-Type", mimeType);
3955
4142
  return c.body(content);
3956
4143
  }
@@ -3962,8 +4149,8 @@ var init_unified_server = __esm({
3962
4149
  if (reqPath.startsWith("/api") || reqPath === "/health")
3963
4150
  return c.notFound();
3964
4151
  const indexPath = resolve(clientPath, "index.html");
3965
- if (fs5.existsSync(indexPath)) {
3966
- let html = fs5.readFileSync(indexPath, "utf-8");
4152
+ if (fs6.existsSync(indexPath)) {
4153
+ let html = fs6.readFileSync(indexPath, "utf-8");
3967
4154
  html = html.replace(
3968
4155
  "</head>",
3969
4156
  `<script>window.__JTO_FORMAT__ = '${format}';</script>
@@ -4124,7 +4311,7 @@ function createDevCommand(adapter) {
4124
4311
  }
4125
4312
 
4126
4313
  // src/cli.ts
4127
- var PACKAGE_VERSION = true ? "0.27.0" : "dev-mode";
4314
+ var PACKAGE_VERSION = true ? "0.28.1" : "dev-mode";
4128
4315
  var program = new Command2();
4129
4316
  program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
4130
4317
  registerCoreCommands(program, {