@json-to-office/jto 0.19.0 → 0.21.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 (26) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +691 -128
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{HomePage-pKThPCNk.js → HomePage-CpxJ_KFq.js} +3 -3
  5. package/dist/client/assets/{HomePage-pKThPCNk.js.map → HomePage-CpxJ_KFq.js.map} +1 -1
  6. package/dist/client/assets/{JsonEditorPage-gjZt0Su7.js → JsonEditorPage-BMH043CA.js} +3 -3
  7. package/dist/client/assets/{JsonEditorPage-gjZt0Su7.js.map → JsonEditorPage-BMH043CA.js.map} +1 -1
  8. package/dist/client/assets/{MonacoPluginProvider-jnV12xEd.js → MonacoPluginProvider-Doo7BbuJ.js} +3 -3
  9. package/dist/client/assets/{MonacoPluginProvider-jnV12xEd.js.map → MonacoPluginProvider-Doo7BbuJ.js.map} +1 -1
  10. package/dist/client/assets/{button-Cm395qUD.js → button--7gFS6Cb.js} +2 -2
  11. package/dist/client/assets/{button-Cm395qUD.js.map → button--7gFS6Cb.js.map} +1 -1
  12. package/dist/client/assets/{editor-DdtOB1D_.js → editor-CJxA-PmD.js} +2 -2
  13. package/dist/client/assets/{editor-DdtOB1D_.js.map → editor-CJxA-PmD.js.map} +1 -1
  14. package/dist/client/assets/{editor-monaco-json-njJSLBLf.js → editor-monaco-json-CTqecbMp.js} +2 -2
  15. package/dist/client/assets/{editor-monaco-json-njJSLBLf.js.map → editor-monaco-json-CTqecbMp.js.map} +1 -1
  16. package/dist/client/assets/index-C-7wxCe7.js +5 -0
  17. package/dist/client/assets/index-C-7wxCe7.js.map +1 -0
  18. package/dist/client/assets/{preview-CsmK-gMw.js → preview-DRq49DTu.js} +2 -2
  19. package/dist/client/assets/{preview-CsmK-gMw.js.map → preview-DRq49DTu.js.map} +1 -1
  20. package/dist/client/index.html +1 -1
  21. package/dist/render-server.d.ts +39 -1
  22. package/dist/render-server.js +847 -75
  23. package/dist/render-server.js.map +1 -1
  24. package/package.json +10 -14
  25. package/dist/client/assets/index-CqeUqd5x.js +0 -5
  26. package/dist/client/assets/index-CqeUqd5x.js.map +0 -1
package/dist/cli.js CHANGED
@@ -20,24 +20,60 @@ var init_esm_shims = __esm({
20
20
 
21
21
  // src/server/config/index.ts
22
22
  import dotenv from "dotenv";
23
+ function positiveInteger(value, fallback) {
24
+ const parsed = Number.parseInt(value ?? "", 10);
25
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
26
+ }
27
+ function parseAuthMode(value, nodeEnv) {
28
+ if (value === "auto" || value === "required" || value === "disabled") {
29
+ return value;
30
+ }
31
+ return nodeEnv === "production" ? "required" : "auto";
32
+ }
33
+ function parseOutboundSourceMode(value, nodeEnv) {
34
+ if (value === "development" || value === "safe") return value;
35
+ return nodeEnv === "production" ? "safe" : "development";
36
+ }
37
+ function normalizeNodeEnv(value) {
38
+ return value === "development" || value === "test" ? value : "production";
39
+ }
23
40
  function parseEnv(env) {
41
+ const nodeEnv = normalizeNodeEnv(env.NODE_ENV || "development");
24
42
  return {
25
- NODE_ENV: env.NODE_ENV || "development",
26
- PORT: parseInt(env.PORT || "3003", 10),
43
+ NODE_ENV: nodeEnv,
44
+ PORT: positiveInteger(env.PORT, 3003),
27
45
  CORS_ORIGIN: env.CORS_ORIGIN || "*",
28
46
  API_KEY: env.API_KEY,
29
47
  API_KEY_HEADER: env.API_KEY_HEADER || "x-api-key",
30
- RATE_LIMIT_WINDOW_MS: parseInt(env.RATE_LIMIT_WINDOW_MS || "900000", 10),
31
- RATE_LIMIT_MAX: parseInt(env.RATE_LIMIT_MAX || "100", 10),
32
- MAX_FILE_SIZE: parseInt(env.MAX_FILE_SIZE || "10485760", 10),
48
+ API_AUTH_MODE: parseAuthMode(env.API_AUTH_MODE, nodeEnv),
49
+ RATE_LIMIT_WINDOW_MS: positiveInteger(env.RATE_LIMIT_WINDOW_MS, 9e5),
50
+ RATE_LIMIT_MAX: positiveInteger(
51
+ env.RATE_LIMIT_MAX,
52
+ nodeEnv === "production" ? 100 : 1e3
53
+ ),
54
+ TRUST_PROXY_HEADERS: env.TRUST_PROXY_HEADERS === "true",
55
+ MAX_FILE_SIZE: positiveInteger(env.MAX_FILE_SIZE, 10 * 1024 * 1024),
56
+ MAX_REQUEST_BODY_SIZE: positiveInteger(
57
+ env.MAX_REQUEST_BODY_SIZE,
58
+ 32 * 1024 * 1024
59
+ ),
60
+ MAX_CONCURRENT_REQUESTS: positiveInteger(
61
+ env.MAX_CONCURRENT_REQUESTS,
62
+ nodeEnv === "production" ? 8 : 64
63
+ ),
64
+ OUTBOUND_SOURCE_MODE: parseOutboundSourceMode(
65
+ env.OUTBOUND_SOURCE_MODE,
66
+ nodeEnv
67
+ ),
68
+ OUTBOUND_HOST_ALLOWLIST: (env.OUTBOUND_HOST_ALLOWLIST || "").split(",").map((host) => host.trim().toLowerCase()).filter(Boolean),
33
69
  UPLOAD_DIR: env.UPLOAD_DIR || "uploads",
34
70
  LIBREOFFICE_PATH: env.LIBREOFFICE_PATH,
35
- LIBREOFFICE_TIMEOUT_MS: env.LIBREOFFICE_TIMEOUT_MS ? parseInt(env.LIBREOFFICE_TIMEOUT_MS, 10) : 3e4,
71
+ LIBREOFFICE_TIMEOUT_MS: env.LIBREOFFICE_TIMEOUT_MS ? positiveInteger(env.LIBREOFFICE_TIMEOUT_MS, 3e4) : 3e4,
36
72
  LOG_LEVEL: env.LOG_LEVEL || "info",
37
73
  CACHE_ENABLED: env.CACHE_ENABLED !== "false",
38
- CACHE_MAX_SIZE_MB: parseInt(env.CACHE_MAX_SIZE_MB || "100", 10),
39
- CACHE_TTL_SECONDS: parseInt(env.CACHE_TTL_SECONDS || "3600", 10),
40
- CACHE_MAX_ITEMS: parseInt(env.CACHE_MAX_ITEMS || "1000", 10)
74
+ CACHE_MAX_SIZE_MB: positiveInteger(env.CACHE_MAX_SIZE_MB, 100),
75
+ CACHE_TTL_SECONDS: positiveInteger(env.CACHE_TTL_SECONDS, 3600),
76
+ CACHE_MAX_ITEMS: positiveInteger(env.CACHE_MAX_ITEMS, 1e3)
41
77
  };
42
78
  }
43
79
  var parsedEnv, config;
@@ -53,7 +89,7 @@ var init_config = __esm({
53
89
  isProduction: parsedEnv.NODE_ENV === "production",
54
90
  isTest: parsedEnv.NODE_ENV === "test",
55
91
  features: {
56
- apiKey: Boolean(parsedEnv.API_KEY),
92
+ apiKey: parsedEnv.API_AUTH_MODE !== "disabled",
57
93
  cache: parsedEnv.CACHE_ENABLED
58
94
  },
59
95
  cors: {
@@ -62,7 +98,17 @@ var init_config = __esm({
62
98
  },
63
99
  rateLimit: {
64
100
  windowMs: parsedEnv.RATE_LIMIT_WINDOW_MS,
65
- max: parsedEnv.RATE_LIMIT_MAX
101
+ max: parsedEnv.RATE_LIMIT_MAX,
102
+ trustProxy: parsedEnv.TRUST_PROXY_HEADERS
103
+ },
104
+ requestLimits: {
105
+ maxBodySize: parsedEnv.MAX_REQUEST_BODY_SIZE,
106
+ maxConcurrent: parsedEnv.MAX_CONCURRENT_REQUESTS,
107
+ maxFileSize: parsedEnv.MAX_FILE_SIZE
108
+ },
109
+ outboundSources: {
110
+ mode: parsedEnv.OUTBOUND_SOURCE_MODE,
111
+ allowedHosts: parsedEnv.OUTBOUND_HOST_ALLOWLIST
66
112
  },
67
113
  cache: {
68
114
  enabled: parsedEnv.CACHE_ENABLED,
@@ -187,10 +233,10 @@ function collectReferencedNames(config2, customThemes, adapterName) {
187
233
  }
188
234
  function collectReferencedWeights(config2, customThemes) {
189
235
  const weights = /* @__PURE__ */ new Set();
190
- const visit = (node) => {
236
+ const visit2 = (node) => {
191
237
  if (node == null) return;
192
238
  if (Array.isArray(node)) {
193
- for (const item of node) visit(item);
239
+ for (const item of node) visit2(item);
194
240
  return;
195
241
  }
196
242
  if (typeof node === "object") {
@@ -198,21 +244,21 @@ function collectReferencedWeights(config2, customThemes) {
198
244
  if (k === "fontWeight" && typeof v === "number" && v >= 100 && v <= 900) {
199
245
  weights.add(v);
200
246
  } else {
201
- visit(v);
247
+ visit2(v);
202
248
  }
203
249
  }
204
250
  }
205
251
  };
206
- visit(config2);
207
- for (const theme of Object.values(customThemes ?? {})) visit(theme);
252
+ visit2(config2);
253
+ for (const theme of Object.values(customThemes ?? {})) visit2(theme);
208
254
  return weights;
209
255
  }
210
256
  function collectReferencedItalic(config2, customThemes) {
211
257
  let found = false;
212
- const visit = (node) => {
258
+ const visit2 = (node) => {
213
259
  if (found || node == null) return;
214
260
  if (Array.isArray(node)) {
215
- for (const item of node) visit(item);
261
+ for (const item of node) visit2(item);
216
262
  return;
217
263
  }
218
264
  if (typeof node === "object") {
@@ -221,14 +267,14 @@ function collectReferencedItalic(config2, customThemes) {
221
267
  found = true;
222
268
  return;
223
269
  }
224
- visit(v);
270
+ visit2(v);
225
271
  }
226
272
  }
227
273
  };
228
- visit(config2);
274
+ visit2(config2);
229
275
  if (!found) {
230
276
  for (const theme of Object.values(customThemes ?? {})) {
231
- visit(theme);
277
+ visit2(theme);
232
278
  if (found) break;
233
279
  }
234
280
  }
@@ -1467,29 +1513,46 @@ var init_typebox_validator = __esm({
1467
1513
 
1468
1514
  // src/server/middleware/hono/rate-limit.ts
1469
1515
  import { HTTPException as HTTPException2 } from "hono/http-exception";
1470
- var rateLimitStore, rateLimiter;
1516
+ function clientAddress(c, trustProxy) {
1517
+ if (trustProxy) {
1518
+ const forwarded = c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",")[0]?.trim();
1519
+ if (forwarded) return forwarded;
1520
+ }
1521
+ const incoming = c.env?.incoming || c.env?.req;
1522
+ return incoming?.socket?.remoteAddress || "anonymous";
1523
+ }
1524
+ var rateLimiter;
1471
1525
  var init_rate_limit = __esm({
1472
1526
  "src/server/middleware/hono/rate-limit.ts"() {
1473
1527
  "use strict";
1474
1528
  init_esm_shims();
1475
- rateLimitStore = /* @__PURE__ */ new Map();
1476
1529
  rateLimiter = (options) => {
1477
1530
  const { limit, window, keyGenerator } = options;
1531
+ const maxEntries = options.maxEntries ?? 1e4;
1532
+ const rateLimitStore = /* @__PURE__ */ new Map();
1533
+ let lastCleanup = 0;
1478
1534
  return async (c, next) => {
1479
- const key = keyGenerator ? keyGenerator(c) : c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous";
1535
+ const clientKey = keyGenerator ? keyGenerator(c) : clientAddress(c, options.trustProxy === true);
1536
+ const namespace = typeof options.namespace === "function" ? options.namespace(c) : options.namespace || `${c.req.method}:${c.req.path}`;
1537
+ const key = `${namespace}:${String(clientKey).slice(0, 256)}`;
1480
1538
  const now = Date.now();
1481
- for (const [k, v] of rateLimitStore.entries()) {
1482
- if (v.resetTime < now) {
1483
- rateLimitStore.delete(k);
1539
+ if (now - lastCleanup >= Math.min(window, 6e4)) {
1540
+ for (const [storedKey, value] of rateLimitStore.entries()) {
1541
+ if (value.resetTime <= now) rateLimitStore.delete(storedKey);
1484
1542
  }
1543
+ lastCleanup = now;
1485
1544
  }
1486
1545
  const record = rateLimitStore.get(key);
1487
1546
  if (!record) {
1547
+ if (rateLimitStore.size >= maxEntries) {
1548
+ const oldestKey = rateLimitStore.keys().next().value;
1549
+ if (oldestKey) rateLimitStore.delete(oldestKey);
1550
+ }
1488
1551
  rateLimitStore.set(key, {
1489
1552
  count: 1,
1490
1553
  resetTime: now + window
1491
1554
  });
1492
- } else if (record.resetTime < now) {
1555
+ } else if (record.resetTime <= now) {
1493
1556
  record.count = 1;
1494
1557
  record.resetTime = now + window;
1495
1558
  } else if (record.count >= limit) {
@@ -1517,6 +1580,316 @@ var init_rate_limit = __esm({
1517
1580
  }
1518
1581
  });
1519
1582
 
1583
+ // src/server/security/outbound-source-policy.ts
1584
+ function isRecord(value) {
1585
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1586
+ }
1587
+ function hostMatches(host, pattern) {
1588
+ const normalized = pattern.toLowerCase().replace(/\.$/, "");
1589
+ if (normalized.startsWith("*.")) {
1590
+ const suffix = normalized.slice(1);
1591
+ return host.endsWith(suffix) && host.length > suffix.length;
1592
+ }
1593
+ return host === normalized;
1594
+ }
1595
+ function isPrivateIpv4(host) {
1596
+ const parts = host.split(".");
1597
+ if (parts.length !== 4) return false;
1598
+ const octets = parts.map(Number);
1599
+ if (octets.some(
1600
+ (part, index) => !Number.isInteger(part) || part < 0 || part > 255 || String(part) !== parts[index]
1601
+ )) {
1602
+ return false;
1603
+ }
1604
+ const [a, b] = octets;
1605
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && (b === 0 || b === 168) || a === 198 && (b === 18 || b === 19) || a >= 224;
1606
+ }
1607
+ function isPrivateHost(host) {
1608
+ const normalized = host.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1609
+ if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::" || normalized === "::1") {
1610
+ return true;
1611
+ }
1612
+ if (isPrivateIpv4(normalized)) return true;
1613
+ if (normalized.startsWith("::ffff:")) {
1614
+ const mapped = normalized.slice("::ffff:".length);
1615
+ if (isPrivateIpv4(mapped)) return true;
1616
+ const groups = mapped.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
1617
+ if (groups) {
1618
+ const value = Number.parseInt(groups[1], 16) * 65536 + Number.parseInt(groups[2], 16);
1619
+ return isPrivateIpv4(
1620
+ [
1621
+ value >>> 24,
1622
+ value >>> 16 & 255,
1623
+ value >>> 8 & 255,
1624
+ value & 255
1625
+ ].join(".")
1626
+ );
1627
+ }
1628
+ }
1629
+ const firstIpv6Group = Number.parseInt(normalized.split(":")[0] || "0", 16);
1630
+ return firstIpv6Group >= 64512 && firstIpv6Group <= 65023 || firstIpv6Group >= 65152 && firstIpv6Group <= 65215;
1631
+ }
1632
+ function assertAllowedUrl(source, path6, allowedHosts) {
1633
+ if (source.startsWith("data:")) return;
1634
+ let url;
1635
+ try {
1636
+ url = new URL(source);
1637
+ } catch {
1638
+ throw new UnsafeOutboundSourceError(
1639
+ path6,
1640
+ "local and relative file paths are disabled for HTTP requests"
1641
+ );
1642
+ }
1643
+ if (url.protocol !== "https:") {
1644
+ throw new UnsafeOutboundSourceError(path6, "only HTTPS URLs are allowed");
1645
+ }
1646
+ if (url.username || url.password) {
1647
+ throw new UnsafeOutboundSourceError(
1648
+ path6,
1649
+ "URLs containing credentials are not allowed"
1650
+ );
1651
+ }
1652
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
1653
+ if (isPrivateHost(host)) {
1654
+ throw new UnsafeOutboundSourceError(
1655
+ path6,
1656
+ `private or local host "${host}" is not allowed`
1657
+ );
1658
+ }
1659
+ if (!allowedHosts.some((pattern) => hostMatches(host, pattern))) {
1660
+ throw new UnsafeOutboundSourceError(
1661
+ path6,
1662
+ `host "${host}" is not in OUTBOUND_HOST_ALLOWLIST`
1663
+ );
1664
+ }
1665
+ }
1666
+ function extractCssReferences(input) {
1667
+ const references = [];
1668
+ const urlPattern = /url\(\s*(['"]?)([^)'"\s]+)\1\s*\)/gi;
1669
+ const importPattern = /@import\s+(?:url\(\s*)?(['"])([^'"]+)\1/gi;
1670
+ for (const pattern of [urlPattern, importPattern]) {
1671
+ let match;
1672
+ while (match = pattern.exec(input)) references.push(match[2]);
1673
+ }
1674
+ return references;
1675
+ }
1676
+ function extractAbsoluteUrls(input) {
1677
+ return input.match(/(?:https?|file):\/\/[^\s'"<>)}\]]+/gi) ?? [];
1678
+ }
1679
+ function assertSafeSvg(svg, path6, allowedHosts) {
1680
+ if (/<\s*script\b/i.test(svg) || /<\s*(?:foreignObject|iframe|object|embed)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg)) {
1681
+ throw new UnsafeOutboundSourceError(path6, "active SVG content is disabled");
1682
+ }
1683
+ if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svg)) {
1684
+ throw new UnsafeOutboundSourceError(
1685
+ path6,
1686
+ "SVG document types and entities are disabled"
1687
+ );
1688
+ }
1689
+ const refPattern = /\b(?:href|src)\s*=\s*(['"])(.*?)\1/gi;
1690
+ let match;
1691
+ while (match = refPattern.exec(svg)) {
1692
+ const ref = match[2].trim();
1693
+ if (!ref || ref.startsWith("#") || ref.startsWith("data:")) continue;
1694
+ assertAllowedUrl(ref, path6, allowedHosts);
1695
+ }
1696
+ for (const ref of extractCssReferences(svg)) {
1697
+ if (ref.startsWith("#") || ref.startsWith("data:")) continue;
1698
+ assertAllowedUrl(ref, path6, allowedHosts);
1699
+ }
1700
+ }
1701
+ function assertSafeEncodedImage(source, path6, allowedHosts) {
1702
+ let candidate = source.trim();
1703
+ let declaredSvg = false;
1704
+ if (candidate.startsWith("data:")) {
1705
+ const comma = candidate.indexOf(",");
1706
+ if (comma < 0) {
1707
+ throw new UnsafeOutboundSourceError(path6, "malformed image data URL");
1708
+ }
1709
+ const metadata = candidate.slice(5, comma);
1710
+ declaredSvg = metadata.split(";")[0].toLowerCase() === "image/svg+xml";
1711
+ if (!declaredSvg) return;
1712
+ const payload = candidate.slice(comma + 1);
1713
+ try {
1714
+ candidate = /(?:^|;)base64(?:;|$)/i.test(metadata) ? Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
1715
+ } catch {
1716
+ throw new UnsafeOutboundSourceError(path6, "malformed SVG data URL");
1717
+ }
1718
+ } else if (!/<\s*svg\b/i.test(candidate)) {
1719
+ candidate = Buffer.from(candidate, "base64").toString("utf8");
1720
+ }
1721
+ const svgStart = candidate.search(/<\s*svg\b/i);
1722
+ if (svgStart < 0) {
1723
+ if (declaredSvg) {
1724
+ throw new UnsafeOutboundSourceError(path6, "malformed SVG image data");
1725
+ }
1726
+ return;
1727
+ }
1728
+ assertSafeSvg(candidate, path6, allowedHosts);
1729
+ }
1730
+ function assertSafeResources(resources, path6, allowedHosts) {
1731
+ if (typeof resources.js === "string" && resources.js.trim()) {
1732
+ throw new UnsafeOutboundSourceError(
1733
+ `${path6}.js`,
1734
+ "remote renderer JavaScript resources are disabled"
1735
+ );
1736
+ }
1737
+ if (typeof resources.css === "string") {
1738
+ for (const ref of extractCssReferences(resources.css)) {
1739
+ if (ref.startsWith("data:")) continue;
1740
+ assertAllowedUrl(ref, `${path6}.css`, allowedHosts);
1741
+ }
1742
+ }
1743
+ if (Array.isArray(resources.files) && resources.files.length > 0) {
1744
+ throw new UnsafeOutboundSourceError(
1745
+ `${path6}.files`,
1746
+ "remote renderer JavaScript resources are disabled"
1747
+ );
1748
+ }
1749
+ }
1750
+ function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1751
+ if (typeof value === "string") {
1752
+ if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource)\s*\(/i.test(value) || /\b(?:javascript|file):/i.test(value)) {
1753
+ throw new UnsafeOutboundSourceError(
1754
+ path6,
1755
+ "network-capable JavaScript and file URLs are disabled"
1756
+ );
1757
+ }
1758
+ for (const url of [
1759
+ ...extractAbsoluteUrls(value),
1760
+ ...extractCssReferences(value)
1761
+ ]) {
1762
+ if (url.startsWith("data:")) continue;
1763
+ assertAllowedUrl(url, path6, allowedHosts);
1764
+ }
1765
+ if (/^\/\//.test(value.trim())) {
1766
+ throw new UnsafeOutboundSourceError(
1767
+ path6,
1768
+ "protocol-relative URLs are disabled"
1769
+ );
1770
+ }
1771
+ return;
1772
+ }
1773
+ if (!value || typeof value !== "object" || seen.has(value)) return;
1774
+ seen.add(value);
1775
+ if (Array.isArray(value)) {
1776
+ value.forEach(
1777
+ (entry, index) => assertNoRemoteReferences(entry, `${path6}[${index}]`, allowedHosts, seen)
1778
+ );
1779
+ return;
1780
+ }
1781
+ for (const [key, child] of Object.entries(value)) {
1782
+ assertNoRemoteReferences(child, `${path6}.${key}`, allowedHosts, seen);
1783
+ }
1784
+ }
1785
+ function visit(value, path6, containerKey, allowedHosts, seen) {
1786
+ if (!value || typeof value !== "object" || seen.has(value)) return;
1787
+ seen.add(value);
1788
+ if (Array.isArray(value)) {
1789
+ value.forEach(
1790
+ (entry, index) => visit(entry, `${path6}[${index}]`, containerKey, allowedHosts, seen)
1791
+ );
1792
+ return;
1793
+ }
1794
+ const record = value;
1795
+ for (const key of Object.keys(record)) {
1796
+ if (DANGEROUS_KEYS.has(key)) {
1797
+ throw new UnsafeOutboundSourceError(
1798
+ `${path6}.${key}`,
1799
+ "prototype mutation keys are disabled"
1800
+ );
1801
+ }
1802
+ }
1803
+ const kind = typeof record.kind === "string" ? record.kind : void 0;
1804
+ if (kind === "file" && typeof record.path === "string") {
1805
+ throw new UnsafeOutboundSourceError(
1806
+ `${path6}.path`,
1807
+ "local file sources are disabled for HTTP requests"
1808
+ );
1809
+ }
1810
+ if ((kind === "url" || kind === "variable") && typeof record.url === "string") {
1811
+ assertAllowedUrl(record.url, `${path6}.url`, allowedHosts);
1812
+ }
1813
+ const componentName = typeof record.name === "string" ? record.name.toLowerCase() : void 0;
1814
+ const props = isRecord(record.props) ? record.props : void 0;
1815
+ if (componentName && props) {
1816
+ if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.path === "string") {
1817
+ assertAllowedUrl(props.path, `${path6}.props.path`, allowedHosts);
1818
+ assertSafeEncodedImage(props.path, `${path6}.props.path`, allowedHosts);
1819
+ }
1820
+ if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.base64 === "string") {
1821
+ assertSafeEncodedImage(
1822
+ props.base64,
1823
+ `${path6}.props.base64`,
1824
+ allowedHosts
1825
+ );
1826
+ }
1827
+ if ((componentName === "highcharts" || componentName === "visual") && typeof props.serverUrl === "string") {
1828
+ assertAllowedUrl(
1829
+ props.serverUrl,
1830
+ `${path6}.props.serverUrl`,
1831
+ allowedHosts
1832
+ );
1833
+ }
1834
+ if (componentName === "highcharts") {
1835
+ if (isRecord(props.resources)) {
1836
+ assertSafeResources(
1837
+ props.resources,
1838
+ `${path6}.props.resources`,
1839
+ allowedHosts
1840
+ );
1841
+ }
1842
+ assertNoRemoteReferences(
1843
+ props.options,
1844
+ `${path6}.props.options`,
1845
+ allowedHosts,
1846
+ /* @__PURE__ */ new WeakSet()
1847
+ );
1848
+ }
1849
+ }
1850
+ if (containerKey === "image" && typeof record.path === "string") {
1851
+ assertAllowedUrl(record.path, `${path6}.path`, allowedHosts);
1852
+ assertSafeEncodedImage(record.path, `${path6}.path`, allowedHosts);
1853
+ }
1854
+ if (typeof record.svg === "string") {
1855
+ assertSafeSvg(record.svg, `${path6}.svg`, allowedHosts);
1856
+ }
1857
+ if (containerKey === "resources") {
1858
+ assertSafeResources(record, path6, allowedHosts);
1859
+ }
1860
+ for (const [key, child] of Object.entries(record)) {
1861
+ visit(child, `${path6}.${key}`, key, allowedHosts, seen);
1862
+ }
1863
+ }
1864
+ function assertSafeOutboundSources(value, policy, rootPath = "request") {
1865
+ if (policy.mode === "development") return;
1866
+ let parsed = value;
1867
+ if (typeof value === "string") {
1868
+ try {
1869
+ parsed = JSON.parse(value);
1870
+ } catch {
1871
+ return;
1872
+ }
1873
+ }
1874
+ visit(parsed, rootPath, void 0, policy.allowedHosts, /* @__PURE__ */ new WeakSet());
1875
+ }
1876
+ var UnsafeOutboundSourceError, DANGEROUS_KEYS, COMPONENT_SOURCE_NAMES;
1877
+ var init_outbound_source_policy = __esm({
1878
+ "src/server/security/outbound-source-policy.ts"() {
1879
+ "use strict";
1880
+ init_esm_shims();
1881
+ UnsafeOutboundSourceError = class extends Error {
1882
+ constructor(path6, reason) {
1883
+ super(`Unsafe outbound source at ${path6}: ${reason}`);
1884
+ this.path = path6;
1885
+ this.name = "UnsafeOutboundSourceError";
1886
+ }
1887
+ };
1888
+ DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1889
+ COMPONENT_SOURCE_NAMES = /* @__PURE__ */ new Set(["image"]);
1890
+ }
1891
+ });
1892
+
1520
1893
  // src/server/rasterize-route.ts
1521
1894
  import { Type as Type2 } from "@sinclair/typebox";
1522
1895
  import { bodyLimit } from "hono/body-limit";
@@ -1550,6 +1923,13 @@ function registerRasterizeRoute(router, options = {}) {
1550
1923
  async (c) => {
1551
1924
  const { presentation, dpi } = getValidated(c, "json");
1552
1925
  try {
1926
+ if (options.sourcePolicy) {
1927
+ assertSafeOutboundSources(
1928
+ presentation,
1929
+ options.sourcePolicy,
1930
+ "presentation"
1931
+ );
1932
+ }
1553
1933
  const result = await getRasterizer()({
1554
1934
  presentation,
1555
1935
  dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI)
@@ -1558,6 +1938,9 @@ function registerRasterizeRoute(router, options = {}) {
1558
1938
  } catch (error) {
1559
1939
  options.onError?.(error);
1560
1940
  if (error instanceof HTTPException3) throw error;
1941
+ if (error instanceof UnsafeOutboundSourceError) {
1942
+ throw new HTTPException3(400, { message: error.message });
1943
+ }
1561
1944
  const msg = error instanceof Error ? error.message.toLowerCase() : String(error);
1562
1945
  if (msg.includes("not found") || msg.includes("rasterization needs")) {
1563
1946
  throw new HTTPException3(503, { message: error.message });
@@ -1578,6 +1961,7 @@ var init_rasterize_route = __esm({
1578
1961
  "use strict";
1579
1962
  init_esm_shims();
1580
1963
  init_typebox_validator();
1964
+ init_outbound_source_policy();
1581
1965
  RasterizeRequestSchema = Type2.Object(
1582
1966
  {
1583
1967
  presentation: Type2.Object({}, { additionalProperties: true }),
@@ -1606,6 +1990,16 @@ import { bodyLimit as bodyLimit2 } from "hono/body-limit";
1606
1990
  import { PluginRegistry as PluginRegistry2 } from "@json-to-office/jto-cli";
1607
1991
  function createFormatRouter(adapter) {
1608
1992
  const router = new Hono2();
1993
+ const assertRequestSources = (value, path6) => {
1994
+ try {
1995
+ assertSafeOutboundSources(value, config.outboundSources, path6);
1996
+ } catch (error) {
1997
+ if (error instanceof UnsafeOutboundSourceError) {
1998
+ throw new HTTPException4(400, { message: error.message });
1999
+ }
2000
+ throw error;
2001
+ }
2002
+ };
1609
2003
  const contentTypeMw = async (c, next) => {
1610
2004
  const contentType = c.req.header("content-type");
1611
2005
  if (!contentType || !contentType.includes("application/json")) {
@@ -1620,7 +2014,7 @@ function createFormatRouter(adapter) {
1620
2014
  rateLimiter({
1621
2015
  limit: process.env.NODE_ENV === "production" ? 10 : 1e3,
1622
2016
  window: 15 * 60 * 1e3,
1623
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
2017
+ trustProxy: config.rateLimit.trustProxy
1624
2018
  }),
1625
2019
  contentTypeMw,
1626
2020
  tbValidator(LooseDocumentGenerationRequestSchema),
@@ -1629,6 +2023,9 @@ function createFormatRouter(adapter) {
1629
2023
  const { jsonDefinition, customThemes, options } = getValidated(c, "json");
1630
2024
  const requestId = c.get("requestId");
1631
2025
  try {
2026
+ assertRequestSources(jsonDefinition, "jsonDefinition");
2027
+ assertRequestSources(customThemes, "customThemes");
2028
+ assertRequestSources(options, "options");
1632
2029
  const bypassCache = c.req.header("X-Bypass-Cache") === "true" || c.req.query("bypass-cache") === "true" || options?.bypassCache === true;
1633
2030
  let sanitizedFonts;
1634
2031
  const rawFonts = options?.fonts;
@@ -1721,7 +2118,7 @@ function createFormatRouter(adapter) {
1721
2118
  rateLimiter({
1722
2119
  limit: process.env.NODE_ENV === "production" ? 30 : 1e3,
1723
2120
  window: 15 * 60 * 1e3,
1724
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
2121
+ trustProxy: config.rateLimit.trustProxy
1725
2122
  }),
1726
2123
  contentTypeMw,
1727
2124
  tbValidator(LooseDocumentDiffRequestSchema),
@@ -1796,10 +2193,17 @@ function createFormatRouter(adapter) {
1796
2193
  }
1797
2194
  router.post(
1798
2195
  "/preview/libreoffice",
2196
+ bodyLimit2({
2197
+ // Multipart framing adds a small amount around the configured file cap.
2198
+ maxSize: config.requestLimits.maxFileSize + 64 * 1024,
2199
+ onError: () => {
2200
+ throw new HTTPException4(413, { message: "Request body too large" });
2201
+ }
2202
+ }),
1799
2203
  rateLimiter({
1800
2204
  limit: process.env.NODE_ENV === "production" ? 20 : 1e3,
1801
2205
  window: 15 * 60 * 1e3,
1802
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
2206
+ trustProxy: config.rateLimit.trustProxy
1803
2207
  }),
1804
2208
  async (c) => {
1805
2209
  const requestId = c.get("requestId");
@@ -1819,13 +2223,30 @@ function createFormatRouter(adapter) {
1819
2223
  message: `${adapter.name.toUpperCase()} file is empty`
1820
2224
  });
1821
2225
  }
2226
+ if (file.size > config.requestLimits.maxFileSize) {
2227
+ throw new HTTPException4(413, {
2228
+ message: `File exceeds ${config.requestLimits.maxFileSize} bytes`
2229
+ });
2230
+ }
2231
+ const expectedExtension = `.${adapter.name}`;
2232
+ const originalName = file.name || `preview${expectedExtension}`;
2233
+ if (!originalName.toLowerCase().endsWith(expectedExtension)) {
2234
+ throw new HTTPException4(400, {
2235
+ message: `Expected a ${adapter.name.toUpperCase()} file`
2236
+ });
2237
+ }
1822
2238
  const arrayBuffer = await file.arrayBuffer();
1823
2239
  const inputBuffer = Buffer.from(arrayBuffer);
2240
+ if (inputBuffer.length < 4 || inputBuffer[0] !== 80 || inputBuffer[1] !== 75) {
2241
+ throw new HTTPException4(400, {
2242
+ message: `Invalid ${adapter.name.toUpperCase()} file`
2243
+ });
2244
+ }
1824
2245
  const pdfBuffer = await libreOfficeService.convertToPdf(
1825
2246
  inputBuffer,
1826
- file.name
2247
+ originalName
1827
2248
  );
1828
- const pdfName = (file.name || "preview").replace(/\.[^.]+$/i, "") + ".pdf";
2249
+ const pdfName = originalName.replace(/\.[^.]+$/i, "").replace(/[^a-zA-Z0-9._-]/g, "_") + ".pdf";
1829
2250
  c.header("Content-Type", "application/pdf");
1830
2251
  c.header("Content-Disposition", `inline; filename="${pdfName}"`);
1831
2252
  c.header("Content-Length", String(pdfBuffer.length));
@@ -1866,7 +2287,7 @@ function createFormatRouter(adapter) {
1866
2287
  rateLimiter({
1867
2288
  limit: process.env.NODE_ENV === "production" ? 20 : 1e3,
1868
2289
  window: 15 * 60 * 1e3,
1869
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
2290
+ trustProxy: config.rateLimit.trustProxy
1870
2291
  }),
1871
2292
  contentTypeMw,
1872
2293
  tbValidator(LooseDocumentGenerationRequestSchema),
@@ -1878,6 +2299,8 @@ function createFormatRouter(adapter) {
1878
2299
  );
1879
2300
  const { jsonDefinition, customThemes } = getValidated(c, "json");
1880
2301
  try {
2302
+ assertRequestSources(jsonDefinition, "jsonDefinition");
2303
+ assertRequestSources(customThemes, "customThemes");
1881
2304
  const generated = await generatorService.generate({
1882
2305
  jsonDefinition,
1883
2306
  customThemes,
@@ -1923,6 +2346,8 @@ function createFormatRouter(adapter) {
1923
2346
  const { jsonDefinition, customThemes } = getValidated(c, "json");
1924
2347
  const requestId = c.get("requestId");
1925
2348
  try {
2349
+ assertRequestSources(jsonDefinition, "jsonDefinition");
2350
+ assertRequestSources(customThemes, "customThemes");
1926
2351
  const config2 = typeof jsonDefinition === "string" ? JSON.parse(jsonDefinition) : jsonDefinition;
1927
2352
  const registry = PluginRegistry2.getInstance();
1928
2353
  if (registry.hasPlugins()) {
@@ -2026,9 +2451,10 @@ function createFormatRouter(adapter) {
2026
2451
  rateLimiter({
2027
2452
  limit: process.env.NODE_ENV === "production" ? 10 : 1e3,
2028
2453
  window: 15 * 60 * 1e3,
2029
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
2454
+ trustProxy: config.rateLimit.trustProxy
2030
2455
  })
2031
2456
  ],
2457
+ sourcePolicy: config.outboundSources,
2032
2458
  onError: (error) => logger.error("Visual rasterization failed", { error })
2033
2459
  });
2034
2460
  return router;
@@ -2043,6 +2469,8 @@ var init_format = __esm({
2043
2469
  init_logger();
2044
2470
  init_rate_limit();
2045
2471
  init_rasterize_route();
2472
+ init_config();
2473
+ init_outbound_source_policy();
2046
2474
  init_libreoffice_converter();
2047
2475
  }
2048
2476
  });
@@ -2578,7 +3006,7 @@ function createAiRouter() {
2578
3006
  rateLimiter({
2579
3007
  limit: process.env.NODE_ENV === "production" ? 30 : 1e3,
2580
3008
  window: 15 * 60 * 1e3,
2581
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
3009
+ trustProxy: config.rateLimit.trustProxy
2582
3010
  }),
2583
3011
  async (c) => {
2584
3012
  try {
@@ -2793,6 +3221,7 @@ var init_ai = __esm({
2793
3221
  init_prompt_loader();
2794
3222
  init_logger();
2795
3223
  init_rate_limit();
3224
+ init_config();
2796
3225
  TEXT_MIME_TYPES = /* @__PURE__ */ new Set([
2797
3226
  "text/plain",
2798
3227
  "text/markdown",
@@ -2818,6 +3247,7 @@ var init_fonts = __esm({
2818
3247
  "use strict";
2819
3248
  init_esm_shims();
2820
3249
  init_rate_limit();
3250
+ init_config();
2821
3251
  fontsRouter = new Hono5();
2822
3252
  MATERIALIZE_FAMILY_MAX = 64;
2823
3253
  MATERIALIZE_WEIGHTS_MAX = 9;
@@ -2850,7 +3280,7 @@ var init_fonts = __esm({
2850
3280
  rateLimiter({
2851
3281
  limit: process.env.NODE_ENV === "production" ? 20 : 1e3,
2852
3282
  window: 15 * 60 * 1e3,
2853
- keyGenerator: (c) => c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous"
3283
+ trustProxy: config.rateLimit.trustProxy
2854
3284
  }),
2855
3285
  async (c) => {
2856
3286
  let body;
@@ -2923,39 +3353,78 @@ var init_request_id = __esm({
2923
3353
  });
2924
3354
 
2925
3355
  // src/server/middleware/hono/auth.ts
3356
+ import { createHash as createHash2, timingSafeEqual } from "crypto";
3357
+ function keysEqual(received, expected) {
3358
+ const receivedDigest = createHash2("sha256").update(received).digest();
3359
+ const expectedDigest = createHash2("sha256").update(expected).digest();
3360
+ return timingSafeEqual(receivedDigest, expectedDigest);
3361
+ }
3362
+ function readCredential(headers, headerName) {
3363
+ const direct = headers.get(headerName)?.trim();
3364
+ if (direct) {
3365
+ if (headerName.toLowerCase() === "authorization" && direct.toLowerCase().startsWith("bearer ")) {
3366
+ return direct.slice(7).trim() || void 0;
3367
+ }
3368
+ return direct;
3369
+ }
3370
+ const authorization = headers.get("authorization")?.trim();
3371
+ if (authorization?.toLowerCase().startsWith("bearer ")) {
3372
+ return authorization.slice(7).trim() || void 0;
3373
+ }
3374
+ return void 0;
3375
+ }
3376
+ function createApiKeyAuthMiddleware(options) {
3377
+ const headerName = options.headerName || "x-api-key";
3378
+ return async (c, next) => {
3379
+ if (c.req.method === "OPTIONS" || options.mode === "disabled") {
3380
+ return next();
3381
+ }
3382
+ if (!options.apiKey) {
3383
+ if (options.mode === "auto") return next();
3384
+ return c.json(
3385
+ {
3386
+ success: false,
3387
+ error: "API authentication is not configured",
3388
+ code: "AUTH_CONFIGURATION_ERROR"
3389
+ },
3390
+ 503
3391
+ );
3392
+ }
3393
+ const apiKey = readCredential(c.req.raw.headers, headerName);
3394
+ if (!apiKey) {
3395
+ return c.json(
3396
+ {
3397
+ success: false,
3398
+ error: "API key required",
3399
+ code: "UNAUTHORIZED"
3400
+ },
3401
+ 401
3402
+ );
3403
+ }
3404
+ if (!keysEqual(apiKey, options.apiKey)) {
3405
+ return c.json(
3406
+ {
3407
+ success: false,
3408
+ error: "Invalid API key",
3409
+ code: "UNAUTHORIZED"
3410
+ },
3411
+ 401
3412
+ );
3413
+ }
3414
+ await next();
3415
+ };
3416
+ }
2926
3417
  var apiKeyAuthMiddleware;
2927
3418
  var init_auth = __esm({
2928
3419
  "src/server/middleware/hono/auth.ts"() {
2929
3420
  "use strict";
2930
3421
  init_esm_shims();
2931
3422
  init_config();
2932
- apiKeyAuthMiddleware = async (c, next) => {
2933
- if (!config.features.apiKey || !config.API_KEY) {
2934
- return next();
2935
- }
2936
- const apiKey = c.req.header(config.API_KEY_HEADER);
2937
- if (!apiKey) {
2938
- return c.json(
2939
- {
2940
- success: false,
2941
- error: "API key required",
2942
- code: "UNAUTHORIZED"
2943
- },
2944
- 401
2945
- );
2946
- }
2947
- if (apiKey !== config.API_KEY) {
2948
- return c.json(
2949
- {
2950
- success: false,
2951
- error: "Invalid API key",
2952
- code: "UNAUTHORIZED"
2953
- },
2954
- 401
2955
- );
2956
- }
2957
- await next();
2958
- };
3423
+ apiKeyAuthMiddleware = createApiKeyAuthMiddleware({
3424
+ mode: config.API_AUTH_MODE,
3425
+ apiKey: config.API_KEY,
3426
+ headerName: config.API_KEY_HEADER
3427
+ });
2959
3428
  }
2960
3429
  });
2961
3430
 
@@ -3137,12 +3606,48 @@ var init_request_logger = __esm({
3137
3606
  }
3138
3607
  });
3139
3608
 
3609
+ // src/server/middleware/hono/concurrency-limit.ts
3610
+ function concurrencyLimiter(options) {
3611
+ const limit = Math.max(1, Math.floor(options.limit));
3612
+ const retryAfterSeconds = options.retryAfterSeconds ?? 1;
3613
+ let active = 0;
3614
+ return async (c, next) => {
3615
+ c.header("X-Concurrency-Limit", String(limit));
3616
+ if (active >= limit) {
3617
+ c.header("Retry-After", String(retryAfterSeconds));
3618
+ return c.json(
3619
+ {
3620
+ success: false,
3621
+ error: "Server is at capacity",
3622
+ code: "CONCURRENCY_LIMIT_EXCEEDED"
3623
+ },
3624
+ 503
3625
+ );
3626
+ }
3627
+ active += 1;
3628
+ c.header("X-Concurrency-Remaining", String(Math.max(0, limit - active)));
3629
+ try {
3630
+ await next();
3631
+ } finally {
3632
+ active -= 1;
3633
+ }
3634
+ };
3635
+ }
3636
+ var init_concurrency_limit = __esm({
3637
+ "src/server/middleware/hono/concurrency-limit.ts"() {
3638
+ "use strict";
3639
+ init_esm_shims();
3640
+ }
3641
+ });
3642
+
3140
3643
  // src/server/app.ts
3141
3644
  import { Hono as Hono6 } from "hono";
3142
3645
  import { cors } from "hono/cors";
3143
3646
  import { secureHeaders } from "hono/secure-headers";
3144
3647
  import { logger as honoLogger } from "hono/logger";
3145
3648
  import { timing } from "hono/timing";
3649
+ import { bodyLimit as bodyLimit4 } from "hono/body-limit";
3650
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
3146
3651
  function createAPIApp(adapter) {
3147
3652
  Container.initialize(adapter);
3148
3653
  const honoApp = new Hono6();
@@ -3152,7 +3657,7 @@ function createAPIApp(adapter) {
3152
3657
  "*",
3153
3658
  cors({
3154
3659
  origin: config.cors.origin,
3155
- credentials: config.cors.credentials,
3660
+ credentials: config.cors.origin === "*" ? false : config.cors.credentials,
3156
3661
  allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
3157
3662
  allowHeaders: ["Content-Type", "Authorization", config.API_KEY_HEADER],
3158
3663
  exposeHeaders: ["X-Request-Id", "X-File-Id"],
@@ -3164,6 +3669,28 @@ function createAPIApp(adapter) {
3164
3669
  honoApp.use("*", errorRecoveryMiddleware);
3165
3670
  honoApp.use("*", requestLoggerMiddleware);
3166
3671
  honoApp.use("*", securityMiddleware);
3672
+ honoApp.use(
3673
+ "/api/*",
3674
+ bodyLimit4({
3675
+ maxSize: config.requestLimits.maxBodySize,
3676
+ onError: () => {
3677
+ throw new HTTPException7(413, { message: "Request body too large" });
3678
+ }
3679
+ })
3680
+ );
3681
+ honoApp.use(
3682
+ "/api/*",
3683
+ rateLimiter({
3684
+ limit: config.rateLimit.max,
3685
+ window: config.rateLimit.windowMs,
3686
+ namespace: (c) => `${c.req.method}:${c.req.path}`,
3687
+ trustProxy: config.rateLimit.trustProxy
3688
+ })
3689
+ );
3690
+ honoApp.use(
3691
+ "/api/*",
3692
+ concurrencyLimiter({ limit: config.requestLimits.maxConcurrent })
3693
+ );
3167
3694
  if (config.features.apiKey) {
3168
3695
  honoApp.use("/api/*", apiKeyAuthMiddleware);
3169
3696
  }
@@ -3217,6 +3744,8 @@ var init_app = __esm({
3217
3744
  init_security();
3218
3745
  init_error_recovery();
3219
3746
  init_request_logger();
3747
+ init_rate_limit();
3748
+ init_concurrency_limit();
3220
3749
  init_container();
3221
3750
  }
3222
3751
  });
@@ -3273,7 +3802,7 @@ var init_unified_server = __esm({
3273
3802
  );
3274
3803
  const apiApp = createAPIApp(this.adapter);
3275
3804
  this.app.get("/api", async (c) => {
3276
- return await apiApp.fetch(c.req.raw);
3805
+ return await apiApp.fetch(c.req.raw, c.env);
3277
3806
  });
3278
3807
  this.app.use("*", async (c, next) => {
3279
3808
  const path6 = c.req.path;
@@ -3281,7 +3810,7 @@ var init_unified_server = __esm({
3281
3810
  if (/\.(ts|tsx|js|jsx|css|map)$/.test(path6)) {
3282
3811
  return next();
3283
3812
  }
3284
- const response = await apiApp.fetch(c.req.raw);
3813
+ const response = await apiApp.fetch(c.req.raw, c.env);
3285
3814
  if (response.status !== 404) {
3286
3815
  return response;
3287
3816
  }
@@ -3478,20 +4007,52 @@ var init_unified_server = __esm({
3478
4007
  // src/cli.ts
3479
4008
  init_esm_shims();
3480
4009
  import { Command as Command2 } from "commander";
3481
- import chalk2 from "chalk";
3482
- import { registerCoreCommands } from "@json-to-office/jto-cli";
4010
+ import chalk from "chalk";
4011
+ import { registerCoreCommands, renderLines as renderLines2 } from "@json-to-office/jto-cli";
3483
4012
 
3484
4013
  // src/commands/dev.ts
3485
4014
  init_esm_shims();
3486
4015
  import { Command } from "commander";
3487
- import chalk from "chalk";
3488
- import ora from "ora";
3489
- import boxen from "boxen";
3490
4016
  import {
3491
4017
  loadConfig,
3492
4018
  formatError,
4019
+ renderLines,
4020
+ runTask,
3493
4021
  EXIT_CODES
3494
4022
  } from "@json-to-office/jto-cli";
4023
+ function installShutdownHandlers(server) {
4024
+ let shutdownPromise;
4025
+ const removeHandlers = () => {
4026
+ process.off("SIGINT", handleShutdown);
4027
+ process.off("SIGTERM", handleShutdown);
4028
+ };
4029
+ const handleShutdown = () => {
4030
+ shutdownPromise ??= (async () => {
4031
+ let exitCode = EXIT_CODES.OK;
4032
+ try {
4033
+ await runTask("Shutting down...", () => server.stop(), {
4034
+ success: "Server stopped",
4035
+ failure: "Failed to stop server"
4036
+ });
4037
+ } catch (error) {
4038
+ exitCode = EXIT_CODES.FAIL;
4039
+ await formatError(error);
4040
+ } finally {
4041
+ removeHandlers();
4042
+ }
4043
+ process.exit(exitCode);
4044
+ })();
4045
+ };
4046
+ process.once("SIGINT", handleShutdown);
4047
+ process.once("SIGTERM", handleShutdown);
4048
+ }
4049
+ async function openBrowser(url) {
4050
+ const { execFile: execFile2 } = await import("child_process");
4051
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
4052
+ await new Promise((resolve2, reject) => {
4053
+ execFile2(command, args, (error) => error ? reject(error) : resolve2());
4054
+ });
4055
+ }
3495
4056
  function createDevCommand(adapter) {
3496
4057
  const dev = new Command("dev");
3497
4058
  return dev.description("Start development server with web UI").option(
@@ -3499,64 +4060,63 @@ function createDevCommand(adapter) {
3499
4060
  "Port to run server on",
3500
4061
  String(adapter.defaultPort)
3501
4062
  ).option("-H, --host <host>", "Host to bind to", "localhost").option("-o, --open", "Open browser automatically").option("-c, --config <path>", "Path to config file").action(async (options) => {
3502
- const spinner = ora(
3503
- `Starting ${adapter.name.toUpperCase()} dev server...`
3504
- ).start();
3505
4063
  try {
3506
- const config2 = await loadConfig(options.config);
3507
- const portSource = dev.getOptionValueSource("port");
3508
- const hostSource = dev.getOptionValueSource("host");
3509
- if (portSource === "cli") {
3510
- config2.server.port = parseInt(options.port, 10);
3511
- } else if (config2.server.port === 3003 && adapter.defaultPort !== 3003) {
3512
- config2.server.port = adapter.defaultPort;
3513
- }
3514
- if (hostSource === "cli") {
3515
- config2.server.host = options.host;
3516
- }
3517
- const { UnifiedServer: UnifiedServer2 } = await Promise.resolve().then(() => (init_unified_server(), unified_server_exports));
3518
- const server = new UnifiedServer2(adapter, config2);
3519
- await server.start();
3520
- const url = `http://${config2.server.host}:${config2.server.port}`;
3521
- spinner.succeed("Server ready");
3522
- console.log(
3523
- boxen(
3524
- chalk.bold(`${adapter.name.toUpperCase()} Dev Server
3525
-
3526
- `) + `${chalk.cyan("Local:")} ${chalk.bold(url)}
3527
- ${chalk.cyan("API:")} ${url}/api/${adapter.name}/generate
3528
- ${chalk.cyan("Health:")} ${url}/health
3529
-
3530
- ` + chalk.gray("Press Ctrl+C to stop"),
3531
- {
3532
- padding: 1,
3533
- borderColor: "green",
3534
- borderStyle: "round"
4064
+ const { server, url } = await runTask(
4065
+ `Starting ${adapter.name.toUpperCase()} dev server...`,
4066
+ async () => {
4067
+ const config2 = await loadConfig(options.config);
4068
+ const portSource = dev.getOptionValueSource("port");
4069
+ const hostSource = dev.getOptionValueSource("host");
4070
+ if (portSource === "cli") {
4071
+ config2.server.port = parseInt(options.port, 10);
4072
+ } else if (config2.server.port === 3003 && adapter.defaultPort !== 3003) {
4073
+ config2.server.port = adapter.defaultPort;
3535
4074
  }
3536
- )
4075
+ if (hostSource === "cli") {
4076
+ config2.server.host = options.host;
4077
+ }
4078
+ const { UnifiedServer: UnifiedServer2 } = await Promise.resolve().then(() => (init_unified_server(), unified_server_exports));
4079
+ const server2 = new UnifiedServer2(adapter, config2);
4080
+ await server2.start();
4081
+ return {
4082
+ server: server2,
4083
+ url: `http://${config2.server.host}:${config2.server.port}`
4084
+ };
4085
+ },
4086
+ { success: "Server ready", failure: "Failed to start dev server" }
3537
4087
  );
4088
+ installShutdownHandlers(server);
4089
+ await renderLines([
4090
+ {
4091
+ text: `${adapter.name.toUpperCase()} Dev Server`,
4092
+ tone: "success"
4093
+ },
4094
+ { text: `Local: ${url}` },
4095
+ { text: `API: ${url}/api/${adapter.name}/generate` },
4096
+ { text: `Health: ${url}/health` },
4097
+ { text: "Press Ctrl+C to stop", tone: "muted" }
4098
+ ]);
3538
4099
  if (options.open) {
3539
- const { execFile: execFile2 } = await import("child_process");
3540
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3541
- execFile2(cmd, [url]);
3542
- }
3543
- const shutdown = async () => {
3544
- console.log(chalk.yellow("\nShutting down..."));
3545
- await server.stop();
3546
- process.exit(EXIT_CODES.OK);
3547
- };
3548
- process.on("SIGINT", shutdown);
3549
- process.on("SIGTERM", shutdown);
4100
+ try {
4101
+ await openBrowser(url);
4102
+ } catch (error) {
4103
+ await renderLines([
4104
+ {
4105
+ text: `Could not open browser: ${error instanceof Error ? error.message : String(error)}`,
4106
+ tone: "warning"
4107
+ }
4108
+ ]);
4109
+ }
4110
+ }
3550
4111
  } catch (error) {
3551
- spinner.fail("Failed to start dev server");
3552
- formatError(error);
4112
+ await formatError(error);
3553
4113
  process.exit(EXIT_CODES.FAIL);
3554
4114
  }
3555
4115
  });
3556
4116
  }
3557
4117
 
3558
4118
  // src/cli.ts
3559
- var PACKAGE_VERSION = true ? "0.19.0" : "dev-mode";
4119
+ var PACKAGE_VERSION = true ? "0.21.0" : "dev-mode";
3560
4120
  var program = new Command2();
3561
4121
  program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
3562
4122
  registerCoreCommands(program, {
@@ -3565,14 +4125,14 @@ registerCoreCommands(program, {
3565
4125
  program.addHelpText(
3566
4126
  "after",
3567
4127
  `
3568
- ${chalk2.gray("Examples:")}
3569
- $ jto docx generate doc.json ${chalk2.dim("# Generate DOCX from JSON")}
3570
- $ jto pptx generate slides.json ${chalk2.dim("# Generate PPTX from JSON")}
3571
- $ jto docx dev ${chalk2.dim("# Start DOCX dev server")}
3572
- $ jto pptx validate slides.json ${chalk2.dim("# Validate PPTX JSON")}
3573
- $ jto docx schemas ${chalk2.dim("# Export DOCX JSON schemas")}
3574
- $ jto pptx discover ${chalk2.dim("# Discover PPTX plugins")}
3575
- $ jto docx fonts install Inter ${chalk2.dim("# Download a Google Font into ./fonts")}
4128
+ ${chalk.gray("Examples:")}
4129
+ $ jto docx generate doc.json ${chalk.dim("# Generate DOCX from JSON")}
4130
+ $ jto pptx generate slides.json ${chalk.dim("# Generate PPTX from JSON")}
4131
+ $ jto docx dev ${chalk.dim("# Start DOCX dev server")}
4132
+ $ jto pptx validate slides.json ${chalk.dim("# Validate PPTX JSON")}
4133
+ $ jto docx schemas ${chalk.dim("# Export DOCX JSON schemas")}
4134
+ $ jto pptx discover ${chalk.dim("# Discover PPTX plugins")}
4135
+ $ jto docx fonts install Inter ${chalk.dim("# Download a Google Font into ./fonts")}
3576
4136
  `
3577
4137
  );
3578
4138
  program.exitOverride();
@@ -3586,7 +4146,10 @@ program.exitOverride();
3586
4146
  if (error.code === "commander.executeSubCommandAsync") {
3587
4147
  process.exit(error.exitCode);
3588
4148
  }
3589
- console.error(chalk2.red("Error:"), error.message);
4149
+ await renderLines2(
4150
+ [{ text: `Error: ${error.message}`, tone: "error" }],
4151
+ process.stderr
4152
+ );
3590
4153
  process.exit(1);
3591
4154
  }
3592
4155
  })();