@json-to-office/jto 0.14.0 → 0.16.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/dist/cli.js +130 -37
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{HomePage-BTmw2QAf.js → HomePage-Czwc5NK6.js} +3 -3
- package/dist/client/assets/{HomePage-BTmw2QAf.js.map → HomePage-Czwc5NK6.js.map} +1 -1
- package/dist/client/assets/{JsonEditorPage-CRONWw-J.js → JsonEditorPage-CwOeiOy6.js} +3 -3
- package/dist/client/assets/{JsonEditorPage-CRONWw-J.js.map → JsonEditorPage-CwOeiOy6.js.map} +1 -1
- package/dist/client/assets/{MonacoPluginProvider-DhOIgTne.js → MonacoPluginProvider-DQcmmuko.js} +3 -3
- package/dist/client/assets/{MonacoPluginProvider-DhOIgTne.js.map → MonacoPluginProvider-DQcmmuko.js.map} +1 -1
- package/dist/client/assets/{button-ebCBEwlw.js → button-DfjYrtU-.js} +2 -2
- package/dist/client/assets/{button-ebCBEwlw.js.map → button-DfjYrtU-.js.map} +1 -1
- package/dist/client/assets/{editor-CB9BVJGn.js → editor-BPfMyucg.js} +2 -2
- package/dist/client/assets/{editor-CB9BVJGn.js.map → editor-BPfMyucg.js.map} +1 -1
- package/dist/client/assets/{editor-monaco-json-DaGk8WIJ.js → editor-monaco-json-cWylTaeJ.js} +2 -2
- package/dist/client/assets/{editor-monaco-json-DaGk8WIJ.js.map → editor-monaco-json-cWylTaeJ.js.map} +1 -1
- package/dist/client/assets/index-Dd4Xxzh5.js +5 -0
- package/dist/client/assets/index-Dd4Xxzh5.js.map +1 -0
- package/dist/client/assets/{preview-B4bcm38l.js → preview-Cdpjm_5u.js} +2 -2
- package/dist/client/assets/{preview-B4bcm38l.js.map → preview-Cdpjm_5u.js.map} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/render-server.d.ts +2 -0
- package/dist/render-server.js +255 -0
- package/dist/render-server.js.map +1 -0
- package/package.json +9 -9
- package/dist/client/assets/index-Cy63RxyT.js +0 -5
- package/dist/client/assets/index-Cy63RxyT.js.map +0 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// src/render-server.ts
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
|
|
5
|
+
// src/server/rasterize-route.ts
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { bodyLimit } from "hono/body-limit";
|
|
8
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
9
|
+
import {
|
|
10
|
+
clampVisualDpi,
|
|
11
|
+
DEFAULT_VISUAL_DPI,
|
|
12
|
+
MIN_VISUAL_DPI,
|
|
13
|
+
MAX_VISUAL_DPI
|
|
14
|
+
} from "@json-to-office/shared";
|
|
15
|
+
import { createLibreOfficePptxRasterizer } from "@json-to-office/jto-cli";
|
|
16
|
+
|
|
17
|
+
// src/server/lib/typebox-validator.ts
|
|
18
|
+
import { Value } from "@sinclair/typebox/value";
|
|
19
|
+
import { HTTPException } from "hono/http-exception";
|
|
20
|
+
function autoWrapDocumentBody(value) {
|
|
21
|
+
if (typeof value === "object" && value !== null && "name" in value && "children" in value && !("jsonDefinition" in value)) {
|
|
22
|
+
return { jsonDefinition: value };
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function tbValidator(schema) {
|
|
27
|
+
return (async (c, next) => {
|
|
28
|
+
let value;
|
|
29
|
+
try {
|
|
30
|
+
value = await c.req.json();
|
|
31
|
+
} catch {
|
|
32
|
+
throw new HTTPException(400, {
|
|
33
|
+
message: "Invalid JSON in request body"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
value = autoWrapDocumentBody(value);
|
|
37
|
+
const isValid = Value.Check(schema, value);
|
|
38
|
+
if (isValid) {
|
|
39
|
+
c.req.validatedData = {
|
|
40
|
+
json: value
|
|
41
|
+
};
|
|
42
|
+
await next();
|
|
43
|
+
} else {
|
|
44
|
+
const errors = [...Value.Errors(schema, value)].map((error) => ({
|
|
45
|
+
path: error.path,
|
|
46
|
+
message: error.message,
|
|
47
|
+
value: error.value
|
|
48
|
+
}));
|
|
49
|
+
const errorMessages = errors.map((e) => `${e.path || "/"}: ${e.message}`);
|
|
50
|
+
throw new HTTPException(400, {
|
|
51
|
+
message: errorMessages.length === 1 ? errorMessages[0] : `Validation failed:
|
|
52
|
+
${errorMessages.join("\n")}`,
|
|
53
|
+
cause: { errors }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function getValidated(c, target) {
|
|
59
|
+
const validatedData = c.req.validatedData;
|
|
60
|
+
if (!validatedData || !validatedData[target]) {
|
|
61
|
+
throw new Error(`No validated data found for target: ${target}`);
|
|
62
|
+
}
|
|
63
|
+
return validatedData[target];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/server/rasterize-route.ts
|
|
67
|
+
var RasterizeRequestSchema = Type.Object(
|
|
68
|
+
{
|
|
69
|
+
presentation: Type.Object({}, { additionalProperties: true }),
|
|
70
|
+
dpi: Type.Optional(
|
|
71
|
+
Type.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
{ additionalProperties: false }
|
|
75
|
+
);
|
|
76
|
+
var sharedRasterizer;
|
|
77
|
+
function getSharedRasterizer() {
|
|
78
|
+
if (!sharedRasterizer) {
|
|
79
|
+
sharedRasterizer = createLibreOfficePptxRasterizer();
|
|
80
|
+
}
|
|
81
|
+
return sharedRasterizer;
|
|
82
|
+
}
|
|
83
|
+
var jsonOnly = async (c, next) => {
|
|
84
|
+
const contentType = c.req.header("content-type");
|
|
85
|
+
if (!contentType || !contentType.includes("application/json")) {
|
|
86
|
+
throw new HTTPException2(400, {
|
|
87
|
+
message: "Content-Type must be application/json"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
await next();
|
|
91
|
+
};
|
|
92
|
+
function registerRasterizeRoute(router, options = {}) {
|
|
93
|
+
const getRasterizer = options.getRasterizer ?? getSharedRasterizer;
|
|
94
|
+
router.post(
|
|
95
|
+
"/rasterize",
|
|
96
|
+
...options.preMiddleware ?? [],
|
|
97
|
+
bodyLimit({
|
|
98
|
+
maxSize: 32 * 1024 * 1024,
|
|
99
|
+
onError: () => {
|
|
100
|
+
throw new HTTPException2(413, { message: "Request body too large" });
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
jsonOnly,
|
|
104
|
+
tbValidator(RasterizeRequestSchema),
|
|
105
|
+
async (c) => {
|
|
106
|
+
const { presentation, dpi } = getValidated(c, "json");
|
|
107
|
+
try {
|
|
108
|
+
const result = await getRasterizer()({
|
|
109
|
+
presentation,
|
|
110
|
+
dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI)
|
|
111
|
+
});
|
|
112
|
+
return c.json(result);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
options.onError?.(error);
|
|
115
|
+
if (error instanceof HTTPException2) throw error;
|
|
116
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error);
|
|
117
|
+
if (msg.includes("not found") || msg.includes("rasterization needs")) {
|
|
118
|
+
throw new HTTPException2(503, { message: error.message });
|
|
119
|
+
}
|
|
120
|
+
if (msg.includes("invalid") || msg.includes("validation")) {
|
|
121
|
+
throw new HTTPException2(400, { message: error.message });
|
|
122
|
+
}
|
|
123
|
+
throw new HTTPException2(500, {
|
|
124
|
+
message: "Internal server error during rasterization"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/server/middleware/hono/rate-limit.ts
|
|
132
|
+
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
133
|
+
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
134
|
+
var rateLimiter = (options) => {
|
|
135
|
+
const { limit, window, keyGenerator } = options;
|
|
136
|
+
return async (c, next) => {
|
|
137
|
+
const key = keyGenerator ? keyGenerator(c) : c.req.header("X-Real-IP") || c.req.header("X-Forwarded-For")?.split(",").pop()?.trim() || "anonymous";
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const [k, v] of rateLimitStore.entries()) {
|
|
140
|
+
if (v.resetTime < now) {
|
|
141
|
+
rateLimitStore.delete(k);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const record = rateLimitStore.get(key);
|
|
145
|
+
if (!record) {
|
|
146
|
+
rateLimitStore.set(key, {
|
|
147
|
+
count: 1,
|
|
148
|
+
resetTime: now + window
|
|
149
|
+
});
|
|
150
|
+
} else if (record.resetTime < now) {
|
|
151
|
+
record.count = 1;
|
|
152
|
+
record.resetTime = now + window;
|
|
153
|
+
} else if (record.count >= limit) {
|
|
154
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
155
|
+
c.header("X-RateLimit-Limit", String(limit));
|
|
156
|
+
c.header("X-RateLimit-Remaining", "0");
|
|
157
|
+
c.header("X-RateLimit-Reset", String(record.resetTime));
|
|
158
|
+
c.header("Retry-After", String(retryAfter));
|
|
159
|
+
throw new HTTPException3(429, {
|
|
160
|
+
message: "Too many requests, please try again later"
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
record.count++;
|
|
164
|
+
}
|
|
165
|
+
const currentRecord = rateLimitStore.get(key);
|
|
166
|
+
c.header("X-RateLimit-Limit", String(limit));
|
|
167
|
+
c.header(
|
|
168
|
+
"X-RateLimit-Remaining",
|
|
169
|
+
String(Math.max(0, limit - currentRecord.count))
|
|
170
|
+
);
|
|
171
|
+
c.header("X-RateLimit-Reset", String(currentRecord.resetTime));
|
|
172
|
+
await next();
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/render-server.ts
|
|
177
|
+
var UPSTREAM = (process.env.HIGHCHARTS_UPSTREAM_URL || "http://127.0.0.1:7801").replace(/\/$/, "");
|
|
178
|
+
var PORT = Number(process.env.PORT || 1e4);
|
|
179
|
+
var PROXY_TIMEOUT_MS = Number(process.env.PROXY_TIMEOUT_MS || 3e4);
|
|
180
|
+
var HEALTH_TIMEOUT_MS = 2e3;
|
|
181
|
+
var app = new Hono();
|
|
182
|
+
function isTimeout(error) {
|
|
183
|
+
const name = error?.name;
|
|
184
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
185
|
+
}
|
|
186
|
+
app.get("/health", async (c) => {
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch(`${UPSTREAM}/health`, {
|
|
189
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
190
|
+
});
|
|
191
|
+
if (res.ok) return c.text("ok");
|
|
192
|
+
return c.json({ status: "degraded", upstream: res.status }, 503);
|
|
193
|
+
} catch {
|
|
194
|
+
return c.json({ status: "degraded", upstream: "unreachable" }, 503);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
registerRasterizeRoute(app, {
|
|
198
|
+
preMiddleware: [
|
|
199
|
+
rateLimiter({
|
|
200
|
+
limit: process.env.NODE_ENV === "production" ? 30 : 1e3,
|
|
201
|
+
window: 15 * 60 * 1e3
|
|
202
|
+
})
|
|
203
|
+
],
|
|
204
|
+
onError: (error) => (
|
|
205
|
+
// eslint-disable-next-line no-console
|
|
206
|
+
console.error(
|
|
207
|
+
"[jto-render-server] rasterize failed:",
|
|
208
|
+
error instanceof Error ? error.message : error
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
});
|
|
212
|
+
app.all("*", async (c) => {
|
|
213
|
+
const url = new URL(c.req.url);
|
|
214
|
+
const target = `${UPSTREAM}${url.pathname}${url.search}`;
|
|
215
|
+
const reqHeaders = new Headers(c.req.raw.headers);
|
|
216
|
+
reqHeaders.delete("host");
|
|
217
|
+
reqHeaders.delete("connection");
|
|
218
|
+
reqHeaders.delete("content-length");
|
|
219
|
+
const init = {
|
|
220
|
+
method: c.req.method,
|
|
221
|
+
headers: reqHeaders,
|
|
222
|
+
signal: AbortSignal.timeout(PROXY_TIMEOUT_MS)
|
|
223
|
+
};
|
|
224
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") {
|
|
225
|
+
init.body = await c.req.arrayBuffer();
|
|
226
|
+
}
|
|
227
|
+
let res;
|
|
228
|
+
try {
|
|
229
|
+
res = await fetch(target, init);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return isTimeout(error) ? c.json(
|
|
232
|
+
{
|
|
233
|
+
error: `Highcharts upstream timed out after ${PROXY_TIMEOUT_MS}ms`
|
|
234
|
+
},
|
|
235
|
+
504
|
|
236
|
+
) : c.json(
|
|
237
|
+
{ error: `Highcharts upstream unreachable at ${UPSTREAM}` },
|
|
238
|
+
502
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const payload = await res.arrayBuffer();
|
|
242
|
+
const headers = {};
|
|
243
|
+
res.headers.forEach((value, key) => {
|
|
244
|
+
if (key !== "content-encoding" && key !== "content-length" && key !== "transfer-encoding") {
|
|
245
|
+
headers[key] = value;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return c.body(payload, res.status, headers);
|
|
249
|
+
});
|
|
250
|
+
serve({ fetch: app.fetch, port: PORT, hostname: "0.0.0.0" }, (info) => {
|
|
251
|
+
console.log(
|
|
252
|
+
`[jto-render-server] listening on :${info.port} \u2014 POST /rasterize (local), proxy \u2192 ${UPSTREAM}`
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
//# sourceMappingURL=render-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/render-server.ts","../src/server/rasterize-route.ts","../src/server/lib/typebox-validator.ts","../src/server/middleware/hono/rate-limit.ts"],"sourcesContent":["/**\n * jto-render-server — a single public endpoint that serves both rendering\n * back-ends used by json-to-office documents:\n *\n * POST /rasterize → pptx slide → PNG (LibreOffice + poppler, in-process)\n * GET /health → liveness, reflecting the highcharts upstream's readiness\n * everything else → reverse-proxied to the co-located Highcharts Export\n * Server (chart rendering, Chromium)\n *\n * It is the front process of the combined Render image\n * (services/jto-render-server). Highcharts runs internally on :7801; this\n * server owns the public port and adds visual rasterization beside it, so one\n * Render instance backs both `services.highcharts` and `services.pptx`.\n *\n * /rasterize shares the validated handler (body-size limit, dpi clamp, rate\n * limit, error mapping) with the in-app route via rasterize-route.ts so the two\n * surfaces can't drift.\n */\n\nimport { serve } from '@hono/node-server';\nimport { Hono } from 'hono';\nimport type { StatusCode } from 'hono/utils/http-status';\nimport { registerRasterizeRoute } from './server/rasterize-route.js';\nimport { rateLimiter } from './server/middleware/hono/rate-limit.js';\n\nconst UPSTREAM = (\n process.env.HIGHCHARTS_UPSTREAM_URL || 'http://127.0.0.1:7801'\n).replace(/\\/$/, '');\nconst PORT = Number(process.env.PORT || 10000);\nconst PROXY_TIMEOUT_MS = Number(process.env.PROXY_TIMEOUT_MS || 30000);\nconst HEALTH_TIMEOUT_MS = 2000;\n\nconst app = new Hono();\n\nfunction isTimeout(error: unknown): boolean {\n const name = (error as { name?: string })?.name;\n return name === 'TimeoutError' || name === 'AbortError';\n}\n\n// Liveness — reflects the highcharts upstream's readiness. The instance backs\n// both /rasterize and /export; reporting 503 while highcharts is down/warming\n// keeps Render from routing /export traffic to a half-up instance and surfaces\n// a crash (the entrypoint restarts highcharts; health recovers when it's back).\napp.get('/health', async (c) => {\n try {\n const res = await fetch(`${UPSTREAM}/health`, {\n signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),\n });\n if (res.ok) return c.text('ok');\n return c.json({ status: 'degraded', upstream: res.status }, 503);\n } catch {\n return c.json({ status: 'degraded', upstream: 'unreachable' }, 503);\n }\n});\n\n// pptx slide → PNG. Shared validated handler (body limit + dpi clamp + rate\n// limit) — this is the public-facing surface, so it must be protected.\nregisterRasterizeRoute(app, {\n preMiddleware: [\n rateLimiter({\n limit: process.env.NODE_ENV === 'production' ? 30 : 1000,\n window: 15 * 60 * 1000,\n }),\n ],\n onError: (error) =>\n // eslint-disable-next-line no-console\n console.error(\n '[jto-render-server] rasterize failed:',\n error instanceof Error ? error.message : error\n ),\n});\n\n// Everything else → the Highcharts Export Server (chart `/export`, etc.).\napp.all('*', async (c) => {\n const url = new URL(c.req.url);\n const target = `${UPSTREAM}${url.pathname}${url.search}`;\n const reqHeaders = new Headers(c.req.raw.headers);\n reqHeaders.delete('host');\n reqHeaders.delete('connection');\n reqHeaders.delete('content-length');\n\n const init: RequestInit = {\n method: c.req.method,\n headers: reqHeaders,\n signal: AbortSignal.timeout(PROXY_TIMEOUT_MS),\n };\n if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {\n init.body = await c.req.arrayBuffer();\n }\n\n let res: Response;\n try {\n res = await fetch(target, init);\n } catch (error) {\n return isTimeout(error)\n ? c.json(\n {\n error: `Highcharts upstream timed out after ${PROXY_TIMEOUT_MS}ms`,\n },\n 504\n )\n : c.json(\n { error: `Highcharts upstream unreachable at ${UPSTREAM}` },\n 502\n );\n }\n\n // Buffer the upstream response (chart PNGs are small) and hand the node\n // adapter a concrete body. Drop content-encoding/length — fetch already\n // decoded the body.\n const payload = await res.arrayBuffer();\n const headers: Record<string, string> = {};\n res.headers.forEach((value, key) => {\n if (\n key !== 'content-encoding' &&\n key !== 'content-length' &&\n key !== 'transfer-encoding'\n ) {\n headers[key] = value;\n }\n });\n return c.body(payload, res.status as StatusCode, headers);\n});\n\nserve({ fetch: app.fetch, port: PORT, hostname: '0.0.0.0' }, (info) => {\n // eslint-disable-next-line no-console\n console.log(\n `[jto-render-server] listening on :${info.port} — POST /rasterize (local), proxy → ${UPSTREAM}`\n );\n});\n","/**\n * Shared POST /rasterize route — one validated handler mounted on BOTH the\n * playground format router and the standalone jto-render-server, so the public\n * and in-app rasterize surfaces can't drift in validation, limits, or error\n * mapping. Renders a single-slide pptx presentation to a PNG.\n */\n\nimport type { Hono, MiddlewareHandler } from 'hono';\nimport { Type } from '@sinclair/typebox';\nimport { bodyLimit } from 'hono/body-limit';\nimport { HTTPException } from 'hono/http-exception';\nimport {\n clampVisualDpi,\n DEFAULT_VISUAL_DPI,\n MIN_VISUAL_DPI,\n MAX_VISUAL_DPI,\n type PptxRasterizer,\n} from '@json-to-office/shared';\nimport { createLibreOfficePptxRasterizer } from '@json-to-office/jto-cli';\nimport { tbValidator, getValidated } from './lib/typebox-validator.js';\n\n/** Body for POST /rasterize: a single-slide pptx presentation + optional dpi. */\nexport const RasterizeRequestSchema = Type.Object(\n {\n presentation: Type.Object({}, { additionalProperties: true }),\n dpi: Type.Optional(\n Type.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })\n ),\n },\n { additionalProperties: false }\n);\n\n// One rasterizer per process — shares the on-disk content-addressed cache\n// across every /rasterize surface in the process.\nlet sharedRasterizer: PptxRasterizer | undefined;\nexport function getSharedRasterizer(): PptxRasterizer {\n if (!sharedRasterizer) {\n sharedRasterizer = createLibreOfficePptxRasterizer();\n }\n return sharedRasterizer;\n}\n\nconst jsonOnly: MiddlewareHandler = async (c, next) => {\n const contentType = c.req.header('content-type');\n if (!contentType || !contentType.includes('application/json')) {\n throw new HTTPException(400, {\n message: 'Content-Type must be application/json',\n });\n }\n await next();\n};\n\n/**\n * Register `POST /rasterize` on a Hono router with body-size limit, content-type\n * + schema validation (dpi clamped to [MIN,MAX]_VISUAL_DPI), the shared\n * rasterizer, and structured 400/413/503/500 error mapping.\n *\n * @param preMiddleware - extra middleware (e.g. a rate limiter) run first.\n */\nexport function registerRasterizeRoute(\n router: Hono<any>,\n options: {\n getRasterizer?: () => PptxRasterizer;\n preMiddleware?: MiddlewareHandler[];\n onError?: (error: unknown) => void;\n } = {}\n): void {\n const getRasterizer = options.getRasterizer ?? getSharedRasterizer;\n\n router.post(\n '/rasterize',\n ...(options.preMiddleware ?? []),\n bodyLimit({\n maxSize: 32 * 1024 * 1024,\n onError: () => {\n throw new HTTPException(413, { message: 'Request body too large' });\n },\n }),\n jsonOnly,\n tbValidator(RasterizeRequestSchema),\n async (c) => {\n const { presentation, dpi } = getValidated<{\n presentation: unknown;\n dpi?: number;\n }>(c, 'json');\n\n try {\n const result = await getRasterizer()({\n presentation,\n dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI),\n });\n return c.json(result);\n } catch (error) {\n options.onError?.(error);\n if (error instanceof HTTPException) throw error;\n const msg =\n error instanceof Error ? error.message.toLowerCase() : String(error);\n if (msg.includes('not found') || msg.includes('rasterization needs')) {\n throw new HTTPException(503, { message: (error as Error).message });\n }\n if (msg.includes('invalid') || msg.includes('validation')) {\n throw new HTTPException(400, { message: (error as Error).message });\n }\n throw new HTTPException(500, {\n message: 'Internal server error during rasterization',\n });\n }\n }\n );\n}\n","import { TSchema } from '@sinclair/typebox';\nimport { Value } from '@sinclair/typebox/value';\nimport { Context, Env, ValidationTargets, MiddlewareHandler } from 'hono';\nimport { HTTPException } from 'hono/http-exception';\n\n/**\n * Auto-detect raw document JSON (has `name` + `children` but no `jsonDefinition`)\n * and wrap it so callers can POST the document tree directly.\n */\nfunction autoWrapDocumentBody(value: unknown): unknown {\n if (\n typeof value === 'object' &&\n value !== null &&\n 'name' in value &&\n 'children' in value &&\n !('jsonDefinition' in value)\n ) {\n return { jsonDefinition: value };\n }\n return value;\n}\n\nexport function tbValidator<\n T extends TSchema,\n E extends Env = Env,\n P extends string = string,\n>(schema: T) {\n return (async (c: Context<E, P>, next: () => Promise<void>) => {\n let value: unknown;\n\n try {\n value = await c.req.json();\n } catch {\n throw new HTTPException(400, {\n message: 'Invalid JSON in request body',\n });\n }\n\n value = autoWrapDocumentBody(value);\n\n const isValid = Value.Check(schema, value);\n\n if (isValid) {\n (c.req as { validatedData?: Record<string, unknown> }).validatedData = {\n json: value,\n };\n await next();\n } else {\n const errors = [...Value.Errors(schema, value)].map((error) => ({\n path: error.path,\n message: error.message,\n value: error.value,\n }));\n\n const errorMessages = errors.map((e) => `${e.path || '/'}: ${e.message}`);\n\n throw new HTTPException(400, {\n message:\n errorMessages.length === 1\n ? errorMessages[0]\n : `Validation failed:\\n${errorMessages.join('\\n')}`,\n cause: { errors },\n });\n }\n }) as MiddlewareHandler<E, P, Record<string, unknown>>;\n}\n\nexport function getValidated<T>(\n c: Context,\n target: keyof ValidationTargets\n): T {\n const validatedData = (c.req as { validatedData?: Record<string, unknown> })\n .validatedData;\n if (!validatedData || !validatedData[target]) {\n throw new Error(`No validated data found for target: ${target}`);\n }\n return validatedData[target] as T;\n}\n","import { MiddlewareHandler } from 'hono';\nimport { HTTPException } from 'hono/http-exception';\n\ninterface RateLimitOptions {\n limit: number;\n window: number; // milliseconds\n keyGenerator?: (c: any) => string;\n}\n\n// In-memory store for rate limiting (use Redis in production)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\n/**\n * Rate limiting middleware for Hono\n */\nexport const rateLimiter = (options: RateLimitOptions): MiddlewareHandler => {\n const { limit, window, keyGenerator } = options;\n\n return async (c, next) => {\n const key = keyGenerator\n ? keyGenerator(c)\n : c.req.header('X-Real-IP') || c.req.header('X-Forwarded-For')?.split(',').pop()?.trim() || 'anonymous';\n const now = Date.now();\n\n // Clean up expired entries\n for (const [k, v] of rateLimitStore.entries()) {\n if (v.resetTime < now) {\n rateLimitStore.delete(k);\n }\n }\n\n const record = rateLimitStore.get(key);\n\n if (!record) {\n // First request\n rateLimitStore.set(key, {\n count: 1,\n resetTime: now + window,\n });\n } else if (record.resetTime < now) {\n // Window expired, reset\n record.count = 1;\n record.resetTime = now + window;\n } else if (record.count >= limit) {\n // Rate limit exceeded\n const retryAfter = Math.ceil((record.resetTime - now) / 1000);\n\n c.header('X-RateLimit-Limit', String(limit));\n c.header('X-RateLimit-Remaining', '0');\n c.header('X-RateLimit-Reset', String(record.resetTime));\n c.header('Retry-After', String(retryAfter));\n\n throw new HTTPException(429, {\n message: 'Too many requests, please try again later',\n });\n } else {\n // Increment count\n record.count++;\n }\n\n // Add rate limit headers\n const currentRecord = rateLimitStore.get(key)!;\n c.header('X-RateLimit-Limit', String(limit));\n c.header(\n 'X-RateLimit-Remaining',\n String(Math.max(0, limit - currentRecord.count))\n );\n c.header('X-RateLimit-Reset', String(currentRecord.resetTime));\n\n await next();\n };\n};\n"],"mappings":"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","names":["HTTPException","HTTPException","HTTPException"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@json-to-office/jto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "JSON to Office CLI - Unified document and presentation generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -87,12 +87,12 @@
|
|
|
87
87
|
"vite": "6.0.5",
|
|
88
88
|
"zod": "^4.0.0",
|
|
89
89
|
"zustand": "5.0.2",
|
|
90
|
-
"@json-to-office/core-docx": "^0.
|
|
91
|
-
"@json-to-office/core-pptx": "^0.
|
|
92
|
-
"@json-to-office/jto-cli": "^0.
|
|
93
|
-
"@json-to-office/shared": "^0.
|
|
94
|
-
"@json-to-office/shared-docx": "^0.
|
|
95
|
-
"@json-to-office/shared-pptx": "^0.
|
|
90
|
+
"@json-to-office/core-docx": "^0.16.0",
|
|
91
|
+
"@json-to-office/core-pptx": "^0.16.0",
|
|
92
|
+
"@json-to-office/jto-cli": "^0.16.0",
|
|
93
|
+
"@json-to-office/shared": "^0.16.0",
|
|
94
|
+
"@json-to-office/shared-docx": "^0.16.0",
|
|
95
|
+
"@json-to-office/shared-pptx": "^0.16.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@types/lodash.debounce": "4.0.9",
|
|
@@ -132,8 +132,8 @@
|
|
|
132
132
|
"presentation"
|
|
133
133
|
],
|
|
134
134
|
"scripts": {
|
|
135
|
-
"build": "tsup && vite build --config src/client/vite.config.ts",
|
|
136
|
-
"build:server": "tsup",
|
|
135
|
+
"build": "rm -rf dist && tsup && vite build --config src/client/vite.config.ts",
|
|
136
|
+
"build:server": "rm -rf dist && tsup",
|
|
137
137
|
"build:client": "vite build --config src/client/vite.config.ts",
|
|
138
138
|
"dev": "tsup --watch",
|
|
139
139
|
"clean": "rm -rf dist",
|