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