@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.
- package/README.md +2 -2
- package/dist/cli.js +691 -128
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{HomePage-pKThPCNk.js → HomePage-CpxJ_KFq.js} +3 -3
- package/dist/client/assets/{HomePage-pKThPCNk.js.map → HomePage-CpxJ_KFq.js.map} +1 -1
- package/dist/client/assets/{JsonEditorPage-gjZt0Su7.js → JsonEditorPage-BMH043CA.js} +3 -3
- package/dist/client/assets/{JsonEditorPage-gjZt0Su7.js.map → JsonEditorPage-BMH043CA.js.map} +1 -1
- package/dist/client/assets/{MonacoPluginProvider-jnV12xEd.js → MonacoPluginProvider-Doo7BbuJ.js} +3 -3
- package/dist/client/assets/{MonacoPluginProvider-jnV12xEd.js.map → MonacoPluginProvider-Doo7BbuJ.js.map} +1 -1
- package/dist/client/assets/{button-Cm395qUD.js → button--7gFS6Cb.js} +2 -2
- package/dist/client/assets/{button-Cm395qUD.js.map → button--7gFS6Cb.js.map} +1 -1
- package/dist/client/assets/{editor-DdtOB1D_.js → editor-CJxA-PmD.js} +2 -2
- package/dist/client/assets/{editor-DdtOB1D_.js.map → editor-CJxA-PmD.js.map} +1 -1
- package/dist/client/assets/{editor-monaco-json-njJSLBLf.js → editor-monaco-json-CTqecbMp.js} +2 -2
- package/dist/client/assets/{editor-monaco-json-njJSLBLf.js.map → editor-monaco-json-CTqecbMp.js.map} +1 -1
- package/dist/client/assets/index-C-7wxCe7.js +5 -0
- package/dist/client/assets/index-C-7wxCe7.js.map +1 -0
- package/dist/client/assets/{preview-CsmK-gMw.js → preview-DRq49DTu.js} +2 -2
- package/dist/client/assets/{preview-CsmK-gMw.js.map → preview-DRq49DTu.js.map} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/render-server.d.ts +39 -1
- package/dist/render-server.js +847 -75
- package/dist/render-server.js.map +1 -1
- package/package.json +10 -14
- package/dist/client/assets/index-CqeUqd5x.js +0 -5
- 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:
|
|
26
|
-
PORT:
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 ?
|
|
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:
|
|
39
|
-
CACHE_TTL_SECONDS:
|
|
40
|
-
CACHE_MAX_ITEMS:
|
|
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:
|
|
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
|
|
236
|
+
const visit2 = (node) => {
|
|
191
237
|
if (node == null) return;
|
|
192
238
|
if (Array.isArray(node)) {
|
|
193
|
-
for (const item of node)
|
|
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
|
-
|
|
247
|
+
visit2(v);
|
|
202
248
|
}
|
|
203
249
|
}
|
|
204
250
|
}
|
|
205
251
|
};
|
|
206
|
-
|
|
207
|
-
for (const theme of Object.values(customThemes ?? {}))
|
|
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
|
|
258
|
+
const visit2 = (node) => {
|
|
213
259
|
if (found || node == null) return;
|
|
214
260
|
if (Array.isArray(node)) {
|
|
215
|
-
for (const item of node)
|
|
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
|
-
|
|
270
|
+
visit2(v);
|
|
225
271
|
}
|
|
226
272
|
}
|
|
227
273
|
};
|
|
228
|
-
|
|
274
|
+
visit2(config2);
|
|
229
275
|
if (!found) {
|
|
230
276
|
for (const theme of Object.values(customThemes ?? {})) {
|
|
231
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
rateLimitStore.delete(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2247
|
+
originalName
|
|
1827
2248
|
);
|
|
1828
|
-
const pdfName = (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
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
|
|
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
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
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
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
${
|
|
3569
|
-
$ jto docx generate doc.json ${
|
|
3570
|
-
$ jto pptx generate slides.json ${
|
|
3571
|
-
$ jto docx dev ${
|
|
3572
|
-
$ jto pptx validate slides.json ${
|
|
3573
|
-
$ jto docx schemas ${
|
|
3574
|
-
$ jto pptx discover ${
|
|
3575
|
-
$ jto docx fonts install Inter ${
|
|
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
|
-
|
|
4149
|
+
await renderLines2(
|
|
4150
|
+
[{ text: `Error: ${error.message}`, tone: "error" }],
|
|
4151
|
+
process.stderr
|
|
4152
|
+
);
|
|
3590
4153
|
process.exit(1);
|
|
3591
4154
|
}
|
|
3592
4155
|
})();
|