@broadpaper/server 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,398 @@
1
+ // src/service.ts
2
+ import { createRegistry } from "@broadpaper/blocks";
3
+ import { migrateTemplate, parseTemplate } from "@broadpaper/core";
4
+ import { renderPdfPaginated } from "@broadpaper/forme";
5
+ var RenderError = class extends Error {
6
+ constructor(message, code, status) {
7
+ super(message);
8
+ this.code = code;
9
+ this.status = status;
10
+ this.name = "RenderError";
11
+ }
12
+ code;
13
+ status;
14
+ };
15
+ var DEFAULTS = { concurrency: 4, queueTimeoutMs: 3e4, timeoutMs: 6e4, maxPages: 500 };
16
+ var RenderService = class {
17
+ registry;
18
+ opts;
19
+ active = 0;
20
+ waiting = [];
21
+ constructor(options = {}) {
22
+ this.opts = { ...DEFAULTS, ...options };
23
+ this.registry = createRegistry(options.blocks ?? []);
24
+ }
25
+ /** Backends this service can actually use, in preference order. */
26
+ get backends() {
27
+ const all = this.opts.chromium ? ["forme", "chromium"] : ["forme"];
28
+ const allowed = this.opts.allowedBackends;
29
+ return allowed ? all.filter((b) => allowed.includes(b)) : all;
30
+ }
31
+ get defaultBackend() {
32
+ const wanted = this.opts.defaultBackend ?? "forme";
33
+ return this.backends.includes(wanted) ? wanted : this.backends[0] ?? "forme";
34
+ }
35
+ get load() {
36
+ return { active: this.active, queued: this.waiting.length };
37
+ }
38
+ /**
39
+ * Renders one document.
40
+ *
41
+ * Throws {@link RenderError} for anything a caller can act on — a backend
42
+ * they may not use, a queue that never cleared, a document longer than the
43
+ * service permits — carrying the code and status the wire protocol names.
44
+ */
45
+ async render(request) {
46
+ if (!request || typeof request !== "object" || !request.template) throw new RenderError("Missing template", "BAD_REQUEST", 400);
47
+ const template = this.parse(request.template);
48
+ const backend = this.pickBackend(request.backend);
49
+ await this.acquire();
50
+ const started = Date.now();
51
+ try {
52
+ const checked = { ...request, template };
53
+ const out = await this.withTimeout(backend === "chromium" ? this.renderChromium(checked) : this.renderForme(checked), backend);
54
+ this.opts.log?.("render", { backend, pages: out.pages, ms: out.durationMs, warnings: out.warnings.length });
55
+ return out;
56
+ } catch (e) {
57
+ const err = e;
58
+ this.opts.log?.("render failed", { backend, code: err.code ?? "RENDER_FAILED", message: err.message, ms: Date.now() - started });
59
+ throw err;
60
+ } finally {
61
+ this.release();
62
+ }
63
+ }
64
+ /**
65
+ * Renders one template against many data sets — a monthly run, a mail merge.
66
+ *
67
+ * One failure does not fail the batch: each result carries its own outcome,
68
+ * because a caller sending five hundred clients wants the four hundred and
69
+ * ninety-nine that worked and a list of the one that did not.
70
+ */
71
+ async renderBatch(request) {
72
+ if (!Array.isArray(request?.items) || request.items.length === 0) throw new RenderError("A batch needs at least one item", "BAD_REQUEST", 400);
73
+ const started = Date.now();
74
+ const backend = this.pickBackend(request.backend);
75
+ const { items, ...shared } = request;
76
+ const results = await Promise.all(
77
+ items.map(async (item) => {
78
+ try {
79
+ const out = await this.render({ ...shared, ...item, backend });
80
+ return { id: item.id, ok: true, pdfBase64: Buffer.from(out.pdf).toString("base64"), pages: out.pages, warnings: out.warnings, durationMs: out.durationMs };
81
+ } catch (e) {
82
+ const err = e;
83
+ return { id: item.id, ok: false, error: err.message, code: err.code ?? "RENDER_FAILED" };
84
+ }
85
+ })
86
+ );
87
+ return { results, backend, durationMs: Date.now() - started };
88
+ }
89
+ async close() {
90
+ for (const w of this.waiting.splice(0)) {
91
+ clearTimeout(w.timer);
92
+ w.reject(new RenderError("The service is shutting down.", "BUSY", 503));
93
+ }
94
+ await this.opts.chromium?.close?.();
95
+ }
96
+ /* ------------------------------ internals ------------------------------ */
97
+ /**
98
+ * Migrates and validates the incoming template.
99
+ *
100
+ * A service takes JSON from over the wire — hand-written, generated, saved by
101
+ * a version of the designer nobody here has seen. Handing that straight to
102
+ * the renderer turns a missing `styles` block into "Cannot read properties of
103
+ * undefined (reading 'height')", which tells the caller nothing at all.
104
+ * Parsing first fills in every default the schema declares and names the
105
+ * field when it cannot.
106
+ */
107
+ parse(input) {
108
+ const migrated = migrateTemplate(input);
109
+ const parsed = parseTemplate(migrated.template);
110
+ if (!parsed.ok || !parsed.template) {
111
+ const detail = parsed.errors.slice(0, 5).map((e) => `${e.path || "(root)"}: ${e.message}`).join("; ");
112
+ throw new RenderError(`The template is not valid. ${detail}${parsed.errors.length > 5 ? ` (+${parsed.errors.length - 5} more)` : ""}`, "BAD_REQUEST", 400);
113
+ }
114
+ return parsed.template;
115
+ }
116
+ pickBackend(requested) {
117
+ if (!requested) return this.defaultBackend;
118
+ if (!this.backends.includes(requested)) {
119
+ throw new RenderError(`Backend "${requested}" is not available here. Available: ${this.backends.join(", ")}.`, "BACKEND_UNAVAILABLE", 400);
120
+ }
121
+ return requested;
122
+ }
123
+ /**
124
+ * Narrows the network policy. A request may restrict what a render fetches;
125
+ * it may not reach anywhere the service was not already willing to go.
126
+ */
127
+ allowedHosts(requested) {
128
+ const server = this.opts.allowedHosts ?? [];
129
+ if (!requested) return server;
130
+ return server.includes("*") ? requested : requested.filter((h) => server.includes(h));
131
+ }
132
+ fonts(request) {
133
+ return [...this.opts.fonts ?? [], ...request.fonts ?? []].map((f) => ({
134
+ family: f.family,
135
+ // Base64 for a client that cannot share a filesystem with the service.
136
+ src: f.bytes ? new Uint8Array(Buffer.from(f.bytes, "base64")) : f.src ?? "",
137
+ weight: f.weight ?? 400,
138
+ style: f.style ?? "normal"
139
+ }));
140
+ }
141
+ async renderForme(request) {
142
+ const fonts = this.fonts(request);
143
+ const result = await renderPdfPaginated({
144
+ template: request.template,
145
+ registry: this.registry,
146
+ data: request.data,
147
+ theme: request.theme,
148
+ dataSources: request.dataSources,
149
+ now: request.now ? new Date(request.now) : void 0,
150
+ locale: request.locale,
151
+ currency: request.currency,
152
+ timeZone: request.timeZone,
153
+ metadata: request.metadata,
154
+ ...this.opts.renderer ? { renderer: this.opts.renderer } : {},
155
+ ...fonts.length ? { fonts } : {},
156
+ ...request.tagged ? { tagged: true } : {},
157
+ ...request.pdfUa ? { pdfUa: true } : {},
158
+ ...request.pdfA ? { pdfA: request.pdfA } : {}
159
+ });
160
+ if (result.paged.totalPages > this.opts.maxPages) {
161
+ throw new RenderError(`Document is ${result.paged.totalPages} pages; this service allows ${this.opts.maxPages}.`, "TOO_MANY_PAGES", 413);
162
+ }
163
+ return { pdf: result.pdf, pages: result.paged.totalPages, warnings: result.warnings, durationMs: result.durationMs, backend: "forme" };
164
+ }
165
+ async renderChromium(request) {
166
+ const result = await this.opts.chromium.render({
167
+ template: request.template,
168
+ data: request.data,
169
+ theme: request.theme,
170
+ dataSources: request.dataSources,
171
+ now: request.now ? new Date(request.now) : void 0,
172
+ locale: request.locale,
173
+ currency: request.currency,
174
+ timeZone: request.timeZone,
175
+ metadata: request.metadata,
176
+ network: { allowedHosts: this.allowedHosts(request.network?.allowedHosts) },
177
+ customBlocksScript: this.opts.customBlocksScript,
178
+ timeoutMs: this.opts.timeoutMs,
179
+ maxPages: this.opts.maxPages
180
+ });
181
+ return {
182
+ pdf: result.pdf,
183
+ pages: result.pages,
184
+ warnings: result.warnings.map((w) => typeof w === "string" ? w : w.message),
185
+ durationMs: result.durationMs,
186
+ backend: "chromium"
187
+ };
188
+ }
189
+ async withTimeout(work, backend) {
190
+ let timer;
191
+ try {
192
+ return await Promise.race([
193
+ work,
194
+ new Promise((_, reject) => {
195
+ timer = setTimeout(() => reject(new RenderError(`Render timed out after ${this.opts.timeoutMs} ms on the ${backend} backend.`, "TIMEOUT", 504)), this.opts.timeoutMs);
196
+ })
197
+ ]);
198
+ } catch (e) {
199
+ if (e instanceof RenderError) throw e;
200
+ throw new RenderError(e.message || "Render failed", "RENDER_FAILED", 500);
201
+ } finally {
202
+ if (timer) clearTimeout(timer);
203
+ }
204
+ }
205
+ /**
206
+ * Waits for a slot, but not forever. An unbounded queue turns one busy minute
207
+ * into a pile of work whose callers have all given up, and then does it
208
+ * anyway.
209
+ */
210
+ acquire() {
211
+ if (this.active < this.opts.concurrency) {
212
+ this.active++;
213
+ return Promise.resolve();
214
+ }
215
+ return new Promise((resolve, reject) => {
216
+ const entry = {
217
+ resolve: () => {
218
+ this.active++;
219
+ resolve();
220
+ },
221
+ reject,
222
+ timer: setTimeout(() => {
223
+ const i = this.waiting.indexOf(entry);
224
+ if (i >= 0) this.waiting.splice(i, 1);
225
+ reject(new RenderError(`Waited ${this.opts.queueTimeoutMs} ms for a render slot.`, "BUSY", 503));
226
+ }, this.opts.queueTimeoutMs)
227
+ };
228
+ this.waiting.push(entry);
229
+ });
230
+ }
231
+ release() {
232
+ this.active--;
233
+ const next = this.waiting.shift();
234
+ if (!next) return;
235
+ clearTimeout(next.timer);
236
+ next.resolve();
237
+ }
238
+ };
239
+
240
+ // src/protocol.ts
241
+ var PROTOCOL_VERSION = 1;
242
+ var HEADERS = {
243
+ pages: "x-broadpaper-pages",
244
+ warnings: "x-broadpaper-warnings",
245
+ backend: "x-broadpaper-backend",
246
+ durationMs: "x-broadpaper-duration-ms"
247
+ };
248
+
249
+ // src/http.ts
250
+ import { createServer } from "http";
251
+ import { resolveLicenseStatus } from "@broadpaper/license";
252
+ function readJson(req, limit) {
253
+ return new Promise((resolve, reject) => {
254
+ const chunks = [];
255
+ let size = 0;
256
+ req.on("data", (c) => {
257
+ size += c.length;
258
+ if (size > limit) {
259
+ req.pause();
260
+ reject(new RenderError(`Request body is larger than ${limit} bytes.`, "PAYLOAD_TOO_LARGE", 413));
261
+ return;
262
+ }
263
+ chunks.push(c);
264
+ });
265
+ req.on("end", () => {
266
+ try {
267
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
268
+ } catch {
269
+ reject(new RenderError("Body is not valid JSON.", "BAD_REQUEST", 400));
270
+ }
271
+ });
272
+ req.on("error", (e) => reject(new RenderError(e.message, "BAD_REQUEST", 400)));
273
+ });
274
+ }
275
+ function sendJson(res, status, body) {
276
+ const json = JSON.stringify(body);
277
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(json) });
278
+ res.end(json);
279
+ }
280
+ function fail(res, e) {
281
+ const err = e instanceof RenderError ? e : new RenderError(e?.message || "Render failed", "RENDER_FAILED", 500);
282
+ if (err.code === "PAYLOAD_TOO_LARGE") res.setHeader("connection", "close");
283
+ const body = { error: err.message, code: err.code };
284
+ sendJson(res, err.status, body);
285
+ }
286
+ function filename(title) {
287
+ const safe = (title ?? "report").replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "");
288
+ return `${safe || "report"}.pdf`;
289
+ }
290
+ function createRenderHandler(options = {}) {
291
+ const service = new RenderService(options);
292
+ const limit = options.maxBodyBytes ?? 25 * 1024 * 1024;
293
+ const base = (options.basePath ?? "").replace(/\/$/, "");
294
+ const cors = options.cors === void 0 ? "*" : options.cors;
295
+ const handler = (async (req, res) => {
296
+ const url = new URL(req.url ?? "/", "http://localhost");
297
+ if (base && !url.pathname.startsWith(base)) return false;
298
+ const path = url.pathname.slice(base.length) || "/";
299
+ const known = path === "/health" || path === "/render" || path === "/render/batch";
300
+ if (!known) return false;
301
+ res.setHeader("x-content-type-options", "nosniff");
302
+ if (cors) res.setHeader("access-control-allow-origin", cors);
303
+ if (req.method === "OPTIONS") {
304
+ res.writeHead(204, { "access-control-allow-headers": "authorization, content-type", "access-control-allow-methods": "POST, GET, OPTIONS", "access-control-max-age": "600" });
305
+ res.end();
306
+ return true;
307
+ }
308
+ if (path === "/health") {
309
+ if (req.method !== "GET") {
310
+ fail(res, new RenderError("Use GET for /health.", "BAD_REQUEST", 405));
311
+ return true;
312
+ }
313
+ const license = await resolveLicenseStatus();
314
+ const body = {
315
+ ok: true,
316
+ backends: service.backends,
317
+ defaultBackend: service.defaultBackend,
318
+ ...service.load,
319
+ version: options.version ?? "0.1.1",
320
+ protocol: PROTOCOL_VERSION,
321
+ license: { state: license.state, entitlement: license.entitlement, expiresAt: license.expiresAt?.toISOString() ?? null }
322
+ };
323
+ sendJson(res, 200, body);
324
+ return true;
325
+ }
326
+ if (req.method !== "POST") {
327
+ fail(res, new RenderError(`Use POST for ${path}.`, "BAD_REQUEST", 405));
328
+ return true;
329
+ }
330
+ if (options.token && req.headers.authorization !== `Bearer ${options.token}`) {
331
+ fail(res, new RenderError("Missing or invalid bearer token.", "UNAUTHORISED", 401));
332
+ return true;
333
+ }
334
+ try {
335
+ const body = await readJson(req, limit);
336
+ if (path === "/render/batch") {
337
+ sendJson(res, 200, await service.renderBatch(body));
338
+ return true;
339
+ }
340
+ const request = body;
341
+ const out = await service.render(request);
342
+ res.setHeader(HEADERS.pages, String(out.pages));
343
+ res.setHeader(HEADERS.warnings, String(out.warnings.length));
344
+ res.setHeader(HEADERS.backend, out.backend);
345
+ res.setHeader(HEADERS.durationMs, String(out.durationMs));
346
+ if (out.warnings.length && request.format !== "json") res.setHeader("x-broadpaper-warning-1", out.warnings[0].slice(0, 300).replace(/[^\x20-\x7e]/g, " "));
347
+ if (request.format === "json") {
348
+ sendJson(res, 200, { pdfBase64: Buffer.from(out.pdf).toString("base64"), pages: out.pages, warnings: out.warnings, durationMs: out.durationMs, backend: out.backend });
349
+ return true;
350
+ }
351
+ res.writeHead(200, {
352
+ "content-type": "application/pdf",
353
+ "content-length": out.pdf.byteLength,
354
+ "content-disposition": `inline; filename="${filename(request.metadata?.title)}"`
355
+ });
356
+ res.end(Buffer.from(out.pdf));
357
+ return true;
358
+ } catch (e) {
359
+ fail(res, e);
360
+ return true;
361
+ }
362
+ });
363
+ handler.service = service;
364
+ handler.close = () => service.close();
365
+ return handler;
366
+ }
367
+ async function createRenderServer(options = {}) {
368
+ const handler = createRenderHandler(options);
369
+ const server = createServer((req, res) => {
370
+ void handler(req, res).then((handled) => {
371
+ if (!handled) sendJson(res, 404, { error: "Not found", code: "BAD_REQUEST" });
372
+ });
373
+ });
374
+ const port = options.port ?? 4780;
375
+ await new Promise((resolve) => server.listen(port, options.host ?? "0.0.0.0", resolve));
376
+ const address = server.address();
377
+ const actual = typeof address === "object" && address ? address.port : port;
378
+ options.log?.(`listening on http://${options.host ?? "0.0.0.0"}:${actual}${options.basePath ?? ""}`, { backends: handler.service.backends });
379
+ return {
380
+ server,
381
+ port: actual,
382
+ service: handler.service,
383
+ close: async () => {
384
+ await new Promise((resolve) => server.close(() => resolve()));
385
+ await handler.close();
386
+ }
387
+ };
388
+ }
389
+
390
+ export {
391
+ RenderError,
392
+ RenderService,
393
+ PROTOCOL_VERSION,
394
+ HEADERS,
395
+ createRenderHandler,
396
+ createRenderServer
397
+ };
398
+ //# sourceMappingURL=chunk-5GQOM7MG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/service.ts","../src/protocol.ts","../src/http.ts"],"sourcesContent":["/**\n * The render service, with the transport left out.\n *\n * `@broadpaper/pdf` could already serve HTTP, but only over Chromium — which\n * means a 700 MB browser on every machine that renders, and nothing at all on\n * a runtime that cannot start one. This service picks a backend instead: the\n * browserless engine by default, Chromium when a caller wants the browser's own\n * text shaping. Both are handed pages BroadPaper's paginator has already\n * decided, so the choice changes fidelity, not pagination.\n *\n * Nothing here knows about HTTP. `http.ts` is a thin adapter over it, which is\n * what lets a host mount the same service inside Express, Fastify, Hono or a\n * serverless handler rather than running a second process.\n */\nimport { createRegistry } from \"@broadpaper/blocks\";\nimport { migrateTemplate, parseTemplate } from \"@broadpaper/core\";\nimport type { BlockDefinition, BlockRegistry, ReportTemplate } from \"@broadpaper/core\";\nimport { renderPdfPaginated, type FormeFont } from \"@broadpaper/forme\";\nimport type { BatchRequest, BatchResponse, RenderBackend, RenderErrorCode, RenderFont, RenderRequest } from \"./protocol.js\";\n\nexport class RenderError extends Error {\n constructor(\n message: string,\n readonly code: RenderErrorCode,\n readonly status: number\n ) {\n super(message);\n this.name = \"RenderError\";\n }\n}\n\n/**\n * What a Chromium backend has to provide.\n *\n * Injected rather than imported, so this package never depends on Playwright\n * and a service that only wants the browserless engine does not drag a browser\n * into its image.\n */\nexport interface ChromiumBackend {\n render(opts: {\n template: unknown;\n data?: unknown;\n theme?: unknown;\n dataSources?: unknown;\n now?: Date;\n locale?: string;\n currency?: string;\n timeZone?: string;\n metadata?: unknown;\n network?: { allowedHosts?: string[]; blockPrivateNetworks?: boolean };\n customBlocksScript?: string;\n timeoutMs?: number;\n maxPages?: number;\n }): Promise<{ pdf: Uint8Array; pages: number; warnings: Array<string | { message: string }>; durationMs: number }>;\n close?(): Promise<void>;\n}\n\nexport interface RenderServiceOptions {\n /** Which backend to use when a request does not name one. Default \"forme\". */\n defaultBackend?: RenderBackend;\n /**\n * Backends a request may ask for. Defaults to whatever is available. Narrow\n * it when you do not want callers choosing the expensive one.\n */\n allowedBackends?: RenderBackend[];\n /** Custom blocks for the browserless backend: the host's own code, in process. */\n blocks?: Array<BlockDefinition<never>>;\n /** Custom blocks bundle for Chromium (an IIFE calling `BroadPaper.registerBlocks`). */\n customBlocksScript?: string;\n /** Fonts embedded in every render. A request may add to these, never replace them. */\n fonts?: RenderFont[];\n /** Chromium backend. Without one, only the browserless backend is available. */\n chromium?: ChromiumBackend;\n /** Concurrent renders. Default 4. */\n concurrency?: number;\n /** How long a request may wait for a slot before being refused. Default 30s. */\n queueTimeoutMs?: number;\n /** Per-render timeout. Default 60s. */\n timeoutMs?: number;\n /** Refuse documents longer than this. Default 500 pages. */\n maxPages?: number;\n /** Hosts remote assets may be fetched from. A request may narrow this, never widen it. */\n allowedHosts?: string[];\n /** Injected Forme entry point, so a host can pick the node, browser or worker build. */\n renderer?: (doc: Record<string, unknown>) => Promise<{ pdf: Uint8Array; layout?: unknown; warnings?: string[] }>;\n log?: (message: string, detail?: Record<string, unknown>) => void;\n}\n\nexport interface RenderOutcome {\n pdf: Uint8Array;\n pages: number;\n warnings: string[];\n durationMs: number;\n backend: RenderBackend;\n}\n\nconst DEFAULTS = { concurrency: 4, queueTimeoutMs: 30_000, timeoutMs: 60_000, maxPages: 500 };\n\nexport class RenderService {\n private readonly registry: BlockRegistry;\n private readonly opts: RenderServiceOptions & Required<Pick<RenderServiceOptions, \"concurrency\" | \"queueTimeoutMs\" | \"timeoutMs\" | \"maxPages\">>;\n private active = 0;\n private readonly waiting: Array<{ resolve: () => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }> = [];\n\n constructor(options: RenderServiceOptions = {}) {\n this.opts = { ...DEFAULTS, ...options };\n this.registry = createRegistry((options.blocks ?? []) as never);\n }\n\n /** Backends this service can actually use, in preference order. */\n get backends(): RenderBackend[] {\n const all: RenderBackend[] = this.opts.chromium ? [\"forme\", \"chromium\"] : [\"forme\"];\n const allowed = this.opts.allowedBackends;\n return allowed ? all.filter((b) => allowed.includes(b)) : all;\n }\n\n get defaultBackend(): RenderBackend {\n const wanted = this.opts.defaultBackend ?? \"forme\";\n return this.backends.includes(wanted) ? wanted : this.backends[0] ?? \"forme\";\n }\n\n get load(): { active: number; queued: number } {\n return { active: this.active, queued: this.waiting.length };\n }\n\n /**\n * Renders one document.\n *\n * Throws {@link RenderError} for anything a caller can act on — a backend\n * they may not use, a queue that never cleared, a document longer than the\n * service permits — carrying the code and status the wire protocol names.\n */\n async render(request: RenderRequest): Promise<RenderOutcome> {\n if (!request || typeof request !== \"object\" || !request.template) throw new RenderError(\"Missing template\", \"BAD_REQUEST\", 400);\n const template = this.parse(request.template);\n const backend = this.pickBackend(request.backend);\n await this.acquire();\n const started = Date.now();\n try {\n const checked = { ...request, template };\n const out = await this.withTimeout(backend === \"chromium\" ? this.renderChromium(checked) : this.renderForme(checked), backend);\n this.opts.log?.(\"render\", { backend, pages: out.pages, ms: out.durationMs, warnings: out.warnings.length });\n return out;\n } catch (e) {\n const err = e as RenderError;\n this.opts.log?.(\"render failed\", { backend, code: err.code ?? \"RENDER_FAILED\", message: err.message, ms: Date.now() - started });\n throw err;\n } finally {\n this.release();\n }\n }\n\n /**\n * Renders one template against many data sets — a monthly run, a mail merge.\n *\n * One failure does not fail the batch: each result carries its own outcome,\n * because a caller sending five hundred clients wants the four hundred and\n * ninety-nine that worked and a list of the one that did not.\n */\n async renderBatch(request: BatchRequest): Promise<BatchResponse> {\n if (!Array.isArray(request?.items) || request.items.length === 0) throw new RenderError(\"A batch needs at least one item\", \"BAD_REQUEST\", 400);\n const started = Date.now();\n const backend = this.pickBackend(request.backend);\n const { items, ...shared } = request;\n const results: BatchResponse[\"results\"] = await Promise.all(\n items.map(async (item) => {\n try {\n const out = await this.render({ ...shared, ...item, backend } as RenderRequest);\n return { id: item.id, ok: true as const, pdfBase64: Buffer.from(out.pdf).toString(\"base64\"), pages: out.pages, warnings: out.warnings, durationMs: out.durationMs };\n } catch (e) {\n const err = e as RenderError;\n return { id: item.id, ok: false as const, error: err.message, code: err.code ?? \"RENDER_FAILED\" };\n }\n })\n );\n return { results, backend, durationMs: Date.now() - started };\n }\n\n async close(): Promise<void> {\n for (const w of this.waiting.splice(0)) {\n clearTimeout(w.timer);\n w.reject(new RenderError(\"The service is shutting down.\", \"BUSY\", 503));\n }\n await this.opts.chromium?.close?.();\n }\n\n /* ------------------------------ internals ------------------------------ */\n\n /**\n * Migrates and validates the incoming template.\n *\n * A service takes JSON from over the wire — hand-written, generated, saved by\n * a version of the designer nobody here has seen. Handing that straight to\n * the renderer turns a missing `styles` block into \"Cannot read properties of\n * undefined (reading 'height')\", which tells the caller nothing at all.\n * Parsing first fills in every default the schema declares and names the\n * field when it cannot.\n */\n private parse(input: RenderRequest[\"template\"]): ReportTemplate {\n const migrated = migrateTemplate(input as never);\n const parsed = parseTemplate(migrated.template);\n if (!parsed.ok || !parsed.template) {\n const detail = parsed.errors\n .slice(0, 5)\n .map((e) => `${e.path || \"(root)\"}: ${e.message}`)\n .join(\"; \");\n throw new RenderError(`The template is not valid. ${detail}${parsed.errors.length > 5 ? ` (+${parsed.errors.length - 5} more)` : \"\"}`, \"BAD_REQUEST\", 400);\n }\n return parsed.template;\n }\n\n private pickBackend(requested: RenderBackend | undefined): RenderBackend {\n if (!requested) return this.defaultBackend;\n if (!this.backends.includes(requested)) {\n throw new RenderError(`Backend \"${requested}\" is not available here. Available: ${this.backends.join(\", \")}.`, \"BACKEND_UNAVAILABLE\", 400);\n }\n return requested;\n }\n\n /**\n * Narrows the network policy. A request may restrict what a render fetches;\n * it may not reach anywhere the service was not already willing to go.\n */\n private allowedHosts(requested: string[] | undefined): string[] {\n const server = this.opts.allowedHosts ?? [];\n if (!requested) return server;\n return server.includes(\"*\") ? requested : requested.filter((h) => server.includes(h));\n }\n\n private fonts(request: RenderRequest): FormeFont[] {\n return [...(this.opts.fonts ?? []), ...(request.fonts ?? [])].map((f) => ({\n family: f.family,\n // Base64 for a client that cannot share a filesystem with the service.\n src: f.bytes ? new Uint8Array(Buffer.from(f.bytes, \"base64\")) : f.src ?? \"\",\n weight: f.weight ?? 400,\n style: f.style ?? \"normal\"\n }));\n }\n\n private async renderForme(request: RenderRequest): Promise<RenderOutcome> {\n const fonts = this.fonts(request);\n const result = await renderPdfPaginated({\n template: request.template,\n registry: this.registry,\n data: request.data,\n theme: request.theme,\n dataSources: request.dataSources,\n now: request.now ? new Date(request.now) : undefined,\n locale: request.locale,\n currency: request.currency,\n timeZone: request.timeZone,\n metadata: request.metadata,\n ...(this.opts.renderer ? { renderer: this.opts.renderer } : {}),\n ...(fonts.length ? { fonts } : {}),\n ...(request.tagged ? { tagged: true } : {}),\n ...(request.pdfUa ? { pdfUa: true } : {}),\n ...(request.pdfA ? { pdfA: request.pdfA } : {})\n });\n if (result.paged.totalPages > this.opts.maxPages) {\n throw new RenderError(`Document is ${result.paged.totalPages} pages; this service allows ${this.opts.maxPages}.`, \"TOO_MANY_PAGES\", 413);\n }\n return { pdf: result.pdf, pages: result.paged.totalPages, warnings: result.warnings, durationMs: result.durationMs, backend: \"forme\" };\n }\n\n private async renderChromium(request: RenderRequest): Promise<RenderOutcome> {\n const result = await this.opts.chromium!.render({\n template: request.template,\n data: request.data,\n theme: request.theme,\n dataSources: request.dataSources,\n now: request.now ? new Date(request.now) : undefined,\n locale: request.locale,\n currency: request.currency,\n timeZone: request.timeZone,\n metadata: request.metadata,\n network: { allowedHosts: this.allowedHosts(request.network?.allowedHosts) },\n customBlocksScript: this.opts.customBlocksScript,\n timeoutMs: this.opts.timeoutMs,\n maxPages: this.opts.maxPages\n });\n return {\n pdf: result.pdf,\n pages: result.pages,\n warnings: result.warnings.map((w) => (typeof w === \"string\" ? w : w.message)),\n durationMs: result.durationMs,\n backend: \"chromium\"\n };\n }\n\n private async withTimeout(work: Promise<RenderOutcome>, backend: RenderBackend): Promise<RenderOutcome> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n work,\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new RenderError(`Render timed out after ${this.opts.timeoutMs} ms on the ${backend} backend.`, \"TIMEOUT\", 504)), this.opts.timeoutMs);\n })\n ]);\n } catch (e) {\n if (e instanceof RenderError) throw e;\n throw new RenderError((e as Error).message || \"Render failed\", \"RENDER_FAILED\", 500);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n /**\n * Waits for a slot, but not forever. An unbounded queue turns one busy minute\n * into a pile of work whose callers have all given up, and then does it\n * anyway.\n */\n private acquire(): Promise<void> {\n if (this.active < this.opts.concurrency) {\n this.active++;\n return Promise.resolve();\n }\n return new Promise<void>((resolve, reject) => {\n const entry = {\n resolve: () => {\n this.active++;\n resolve();\n },\n reject,\n timer: setTimeout(() => {\n const i = this.waiting.indexOf(entry);\n if (i >= 0) this.waiting.splice(i, 1);\n reject(new RenderError(`Waited ${this.opts.queueTimeoutMs} ms for a render slot.`, \"BUSY\", 503));\n }, this.opts.queueTimeoutMs)\n };\n this.waiting.push(entry);\n });\n }\n\n private release(): void {\n this.active--;\n const next = this.waiting.shift();\n if (!next) return;\n clearTimeout(next.timer);\n next.resolve();\n }\n}\n","/**\n * The wire protocol of the render service.\n *\n * This file is the contract between the service, the browser client and the\n * .NET client under `dotnet/`. Every field here is JSON — a template, a theme\n * and a data set are all host-defined documents — so the same shapes are\n * expressible in any language, and the .NET types mirror these names exactly.\n *\n * Anything added here must be optional, because a client is always older than\n * the service it is talking to.\n */\nimport type { DataSource, ReportData, ReportTemplate, Theme, AnyVersionTemplate } from \"@broadpaper/core\";\nimport type { Entitlement, LicenseState } from \"@broadpaper/license\";\n\n/** Which engine draws the pages. BroadPaper decides the pages either way. */\nexport type RenderBackend = \"forme\" | \"chromium\";\n\nexport interface RenderMetadata {\n title?: string;\n author?: string;\n subject?: string;\n keywords?: string[];\n /** Document language, e.g. \"en-GB\". Required for PDF/UA. */\n lang?: string;\n}\n\n/** A font to embed. Without one the engine falls back to its standard faces. */\nexport interface RenderFont {\n family: string;\n /** A path the service can read, a `data:` URI, or base64 in `bytes`. */\n src?: string;\n /** Base64-encoded TTF/OTF. Use this from a client that cannot share a filesystem. */\n bytes?: string;\n weight?: number;\n style?: \"normal\" | \"italic\";\n}\n\nexport interface RenderRequest {\n /** The saved template. Older schema versions are migrated on the way in. */\n template: ReportTemplate | AnyVersionTemplate;\n /** Runtime data, keyed by data source id. */\n data?: ReportData;\n theme?: Partial<Theme>;\n dataSources?: DataSource[];\n /** ISO-8601. Fixes the clock, which is one of the conditions for byte-identical output. */\n now?: string;\n locale?: string;\n currency?: string;\n timeZone?: string;\n metadata?: RenderMetadata;\n /** Overrides the service default, when the service permits it. */\n backend?: RenderBackend;\n fonts?: RenderFont[];\n /** Tagged (accessible) PDF. */\n tagged?: boolean;\n /** PDF/UA-1 conformance. Needs an embeddable font. */\n pdfUa?: boolean;\n /** PDF/A conformance level. Needs an embeddable font. */\n pdfA?: \"2b\" | \"2u\" | \"2a\" | \"3b\" | \"3u\" | \"3a\";\n /** Hosts remote assets may be fetched from. May narrow the service's list, never widen it. */\n network?: { allowedHosts?: string[] };\n /** \"pdf\" returns the bytes; \"json\" returns {@link RenderJsonResponse}. Default \"pdf\". */\n format?: \"pdf\" | \"json\";\n}\n\n/** One entry of a batch: the same template rendered against different data. */\nexport interface BatchItem {\n /** Echoed back on the result, so a caller can match them up. */\n id: string;\n data?: ReportData;\n theme?: Partial<Theme>;\n metadata?: RenderMetadata;\n now?: string;\n locale?: string;\n currency?: string;\n timeZone?: string;\n}\n\nexport interface BatchRequest extends Omit<RenderRequest, \"data\" | \"format\"> {\n items: BatchItem[];\n}\n\nexport interface RenderJsonResponse {\n pdfBase64: string;\n pages: number;\n warnings: string[];\n durationMs: number;\n backend: RenderBackend;\n}\n\nexport interface BatchResponse {\n results: Array<\n | { id: string; ok: true; pdfBase64: string; pages: number; warnings: string[]; durationMs: number }\n | { id: string; ok: false; error: string; code: RenderErrorCode }\n >;\n backend: RenderBackend;\n durationMs: number;\n}\n\nexport interface HealthResponse {\n ok: boolean;\n /** Backends this service can actually use right now. */\n backends: RenderBackend[];\n defaultBackend: RenderBackend;\n active: number;\n queued: number;\n version: string;\n /** The protocol revision, so a client can refuse a service it does not understand. */\n protocol: number;\n /**\n * What this service's PDFs will come out as.\n *\n * Reported because the alternative is an operator discovering the answer in a\n * finance report a customer has already been sent. `state: \"unlicensed\"` is a\n * perfectly good answer in a development environment and a serious one in\n * production, and only the operator can tell which they are looking at.\n */\n license?: {\n state: LicenseState;\n /** Null unless the licence is being honoured. */\n entitlement: Entitlement | null;\n /** ISO 8601, when a licence was readable. */\n expiresAt: string | null;\n };\n}\n\nexport type RenderErrorCode =\n | \"BAD_REQUEST\"\n | \"UNAUTHORISED\"\n | \"PAYLOAD_TOO_LARGE\"\n | \"BACKEND_UNAVAILABLE\"\n | \"TIMEOUT\"\n | \"TOO_MANY_PAGES\"\n | \"BUSY\"\n | \"RENDER_FAILED\";\n\nexport interface ErrorResponse {\n error: string;\n code: RenderErrorCode;\n}\n\n/** Bumped when a change would break an older client. */\nexport const PROTOCOL_VERSION = 1;\n\n/** Response headers a client can read without parsing the body. */\nexport const HEADERS = {\n pages: \"x-broadpaper-pages\",\n warnings: \"x-broadpaper-warnings\",\n backend: \"x-broadpaper-backend\",\n durationMs: \"x-broadpaper-duration-ms\"\n} as const;\n","/**\n * HTTP in front of the render service.\n *\n * Two layers, on purpose. `createRenderHandler` is a plain Node\n * `(req, res) => Promise<boolean>` that returns whether it handled the request,\n * so it drops into an existing Express, Fastify, Connect or raw `http` app —\n * most teams already have a service and do not want a second one to deploy.\n * `createRenderServer` is that handler with a listener around it, for the teams\n * that do.\n *\n * The routes are the whole API surface:\n *\n * POST /render → PDF bytes, or JSON with `format: \"json\"`\n * POST /render/batch → one template, many data sets, JSON\n * GET /health → readiness, backends, load, protocol version\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { resolveLicenseStatus } from \"@broadpaper/license\";\nimport { RenderError, RenderService, type RenderServiceOptions } from \"./service.js\";\nimport { HEADERS, PROTOCOL_VERSION, type BatchRequest, type ErrorResponse, type HealthResponse, type RenderRequest } from \"./protocol.js\";\n\nexport interface RenderHandlerOptions extends RenderServiceOptions {\n /** Shared-secret bearer token. Strongly recommended anywhere but a laptop. */\n token?: string;\n /** Maximum request body size in bytes. Default 25 MB. */\n maxBodyBytes?: number;\n /**\n * Origins allowed to call this from a browser. `\"*\"` is the default because\n * the token is what actually guards the endpoint; set it when you want the\n * browser to enforce an origin too.\n */\n cors?: string | false;\n /** Mount point, e.g. \"/api/pdf\". Default \"\". */\n basePath?: string;\n /** Reported by `/health`, so an operator can see what is deployed. */\n version?: string;\n}\n\nexport interface RenderHandler {\n (req: IncomingMessage, res: ServerResponse): Promise<boolean>;\n service: RenderService;\n close(): Promise<void>;\n}\n\nfunction readJson(req: IncomingMessage, limit: number): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on(\"data\", (c: Buffer) => {\n size += c.length;\n if (size > limit) {\n // Stop reading, but do not destroy the socket: a caller who is told\n // \"connection reset\" has to guess why, and 413 is the whole point of\n // having a limit. The response carries `connection: close`, which is\n // what lets the socket go without draining the rest of the body.\n req.pause();\n reject(new RenderError(`Request body is larger than ${limit} bytes.`, \"PAYLOAD_TOO_LARGE\", 413));\n return;\n }\n chunks.push(c);\n });\n req.on(\"end\", () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString(\"utf8\")));\n } catch {\n reject(new RenderError(\"Body is not valid JSON.\", \"BAD_REQUEST\", 400));\n }\n });\n req.on(\"error\", (e) => reject(new RenderError(e.message, \"BAD_REQUEST\", 400)));\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, { \"content-type\": \"application/json; charset=utf-8\", \"content-length\": Buffer.byteLength(json) });\n res.end(json);\n}\n\nfunction fail(res: ServerResponse, e: unknown): void {\n const err = e instanceof RenderError ? e : new RenderError((e as Error)?.message || \"Render failed\", \"RENDER_FAILED\", 500);\n // The rest of an over-long body is never going to be read, so the connection\n // cannot be reused for the next request on it.\n if (err.code === \"PAYLOAD_TOO_LARGE\") res.setHeader(\"connection\", \"close\");\n const body: ErrorResponse = { error: err.message, code: err.code };\n sendJson(res, err.status, body);\n}\n\n/** A filename a browser will accept, from whatever the caller called the report. */\nfunction filename(title: string | undefined): string {\n const safe = (title ?? \"report\").replace(/[^\\w.-]+/g, \"_\").replace(/^_+|_+$/g, \"\");\n return `${safe || \"report\"}.pdf`;\n}\n\n/**\n * Builds the request handler. Returns `false` when the request was not one of\n * ours, so a host can fall through to its own routes.\n */\nexport function createRenderHandler(options: RenderHandlerOptions = {}): RenderHandler {\n const service = new RenderService(options);\n const limit = options.maxBodyBytes ?? 25 * 1024 * 1024;\n const base = (options.basePath ?? \"\").replace(/\\/$/, \"\");\n const cors = options.cors === undefined ? \"*\" : options.cors;\n\n const handler = (async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n if (base && !url.pathname.startsWith(base)) return false;\n const path = url.pathname.slice(base.length) || \"/\";\n const known = path === \"/health\" || path === \"/render\" || path === \"/render/batch\";\n if (!known) return false;\n\n res.setHeader(\"x-content-type-options\", \"nosniff\");\n if (cors) res.setHeader(\"access-control-allow-origin\", cors);\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, { \"access-control-allow-headers\": \"authorization, content-type\", \"access-control-allow-methods\": \"POST, GET, OPTIONS\", \"access-control-max-age\": \"600\" });\n res.end();\n return true;\n }\n\n if (path === \"/health\") {\n if (req.method !== \"GET\") {\n fail(res, new RenderError(\"Use GET for /health.\", \"BAD_REQUEST\", 405));\n return true;\n }\n const license = await resolveLicenseStatus();\n const body: HealthResponse = {\n ok: true,\n backends: service.backends,\n defaultBackend: service.defaultBackend,\n ...service.load,\n version: options.version ?? \"0.1.1\",\n protocol: PROTOCOL_VERSION,\n license: { state: license.state, entitlement: license.entitlement, expiresAt: license.expiresAt?.toISOString() ?? null }\n };\n sendJson(res, 200, body);\n return true;\n }\n\n if (req.method !== \"POST\") {\n fail(res, new RenderError(`Use POST for ${path}.`, \"BAD_REQUEST\", 405));\n return true;\n }\n // Checked before the body is read, so an unauthorised caller cannot make\n // the service buffer 25 MB for them.\n if (options.token && req.headers.authorization !== `Bearer ${options.token}`) {\n fail(res, new RenderError(\"Missing or invalid bearer token.\", \"UNAUTHORISED\", 401));\n return true;\n }\n\n try {\n const body = await readJson(req, limit);\n if (path === \"/render/batch\") {\n sendJson(res, 200, await service.renderBatch(body as BatchRequest));\n return true;\n }\n\n const request = body as RenderRequest;\n const out = await service.render(request);\n res.setHeader(HEADERS.pages, String(out.pages));\n res.setHeader(HEADERS.warnings, String(out.warnings.length));\n res.setHeader(HEADERS.backend, out.backend);\n res.setHeader(HEADERS.durationMs, String(out.durationMs));\n // Warnings matter enough to be readable without asking for JSON, but a\n // header is not the place for a paragraph of them.\n if (out.warnings.length && request.format !== \"json\") res.setHeader(\"x-broadpaper-warning-1\", out.warnings[0]!.slice(0, 300).replace(/[^\\x20-\\x7e]/g, \" \"));\n\n if (request.format === \"json\") {\n sendJson(res, 200, { pdfBase64: Buffer.from(out.pdf).toString(\"base64\"), pages: out.pages, warnings: out.warnings, durationMs: out.durationMs, backend: out.backend });\n return true;\n }\n res.writeHead(200, {\n \"content-type\": \"application/pdf\",\n \"content-length\": out.pdf.byteLength,\n \"content-disposition\": `inline; filename=\"${filename(request.metadata?.title)}\"`\n });\n res.end(Buffer.from(out.pdf));\n return true;\n } catch (e) {\n fail(res, e);\n return true;\n }\n }) as RenderHandler;\n\n handler.service = service;\n handler.close = () => service.close();\n return handler;\n}\n\nexport interface RenderServer {\n server: Server;\n port: number;\n service: RenderService;\n close(): Promise<void>;\n}\n\n/** The handler, with a listener around it. */\nexport async function createRenderServer(options: RenderHandlerOptions & { port?: number; host?: string } = {}): Promise<RenderServer> {\n const handler = createRenderHandler(options);\n const server = createServer((req, res) => {\n void handler(req, res).then((handled) => {\n if (!handled) sendJson(res, 404, { error: \"Not found\", code: \"BAD_REQUEST\" } satisfies ErrorResponse);\n });\n });\n const port = options.port ?? 4780;\n await new Promise<void>((resolve) => server.listen(port, options.host ?? \"0.0.0.0\", resolve));\n const address = server.address();\n const actual = typeof address === \"object\" && address ? address.port : port;\n options.log?.(`listening on http://${options.host ?? \"0.0.0.0\"}:${actual}${options.basePath ?? \"\"}`, { backends: handler.service.backends });\n return {\n server,\n port: actual,\n service: handler.service,\n close: async () => {\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await handler.close();\n }\n };\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createRenderServer
4
+ } from "./chunk-5GQOM7MG.js";
5
+
6
+ // src/cli.ts
7
+ import { resolveLicenseStatus } from "@broadpaper/license";
8
+ var env = process.env;
9
+ var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
10
+ var server = await createRenderServer({
11
+ port: Number(env.PORT ?? env.BROADPAPER_PORT ?? 4780),
12
+ host: env.HOST ?? "0.0.0.0",
13
+ token: env.BROADPAPER_TOKEN,
14
+ basePath: env.BROADPAPER_BASE_PATH ?? "",
15
+ concurrency: Number(env.BROADPAPER_CONCURRENCY ?? 4),
16
+ queueTimeoutMs: Number(env.BROADPAPER_QUEUE_TIMEOUT_MS ?? 3e4),
17
+ timeoutMs: Number(env.BROADPAPER_TIMEOUT_MS ?? 6e4),
18
+ maxPages: Number(env.BROADPAPER_MAX_PAGES ?? 500),
19
+ maxBodyBytes: Number(env.BROADPAPER_MAX_BODY_BYTES ?? 25 * 1024 * 1024),
20
+ allowedHosts: list(env.BROADPAPER_ALLOWED_HOSTS),
21
+ cors: env.BROADPAPER_CORS === "off" ? false : env.BROADPAPER_CORS ?? "*",
22
+ version: env.BROADPAPER_VERSION,
23
+ log: (message, detail) => console.log(`[broadpaper] ${message}${detail ? " " + JSON.stringify(detail) : ""}`)
24
+ });
25
+ if (!env.BROADPAPER_TOKEN) console.warn("[broadpaper] no BROADPAPER_TOKEN set \u2014 the render endpoint is open to anyone who can reach it");
26
+ var license = await resolveLicenseStatus();
27
+ console.log(`[broadpaper] licence: ${license.state}${license.entitlement ? ` (${license.entitlement})` : ""}${license.expiresAt ? `, expires ${license.expiresAt.toISOString().slice(0, 10)}` : ""}`);
28
+ for (const signal of ["SIGINT", "SIGTERM"]) {
29
+ process.on(signal, () => {
30
+ console.log(`[broadpaper] ${signal}, closing`);
31
+ void server.close().then(() => process.exit(0));
32
+ });
33
+ }
34
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `broadpaper-server` — the render service as a container entry point.\n *\n * Everything is an environment variable because that is what a container\n * orchestrator can set; anything more elaborate belongs in a host that imports\n * `createRenderHandler` and configures it in code.\n */\nimport { resolveLicenseStatus } from \"@broadpaper/license\";\nimport { createRenderServer } from \"./http.js\";\n\nconst env = process.env;\nconst list = (v: string | undefined) => (v ?? \"\").split(\",\").map((s) => s.trim()).filter(Boolean);\n\nconst server = await createRenderServer({\n port: Number(env.PORT ?? env.BROADPAPER_PORT ?? 4780),\n host: env.HOST ?? \"0.0.0.0\",\n token: env.BROADPAPER_TOKEN,\n basePath: env.BROADPAPER_BASE_PATH ?? \"\",\n concurrency: Number(env.BROADPAPER_CONCURRENCY ?? 4),\n queueTimeoutMs: Number(env.BROADPAPER_QUEUE_TIMEOUT_MS ?? 30_000),\n timeoutMs: Number(env.BROADPAPER_TIMEOUT_MS ?? 60_000),\n maxPages: Number(env.BROADPAPER_MAX_PAGES ?? 500),\n maxBodyBytes: Number(env.BROADPAPER_MAX_BODY_BYTES ?? 25 * 1024 * 1024),\n allowedHosts: list(env.BROADPAPER_ALLOWED_HOSTS),\n cors: env.BROADPAPER_CORS === \"off\" ? false : env.BROADPAPER_CORS ?? \"*\",\n version: env.BROADPAPER_VERSION,\n log: (message, detail) => console.log(`[broadpaper] ${message}${detail ? \" \" + JSON.stringify(detail) : \"\"}`)\n});\n\nif (!env.BROADPAPER_TOKEN) console.warn(\"[broadpaper] no BROADPAPER_TOKEN set — the render endpoint is open to anyone who can reach it\");\n\n// Said at boot, not on the first render: an operator reads the startup log, and\n// \"every PDF this service produces is watermarked\" is not something to find out\n// from a customer. The licence itself is picked up from BROADPAPER_LICENSE.\nconst license = await resolveLicenseStatus();\nconsole.log(`[broadpaper] licence: ${license.state}${license.entitlement ? ` (${license.entitlement})` : \"\"}${license.expiresAt ? `, expires ${license.expiresAt.toISOString().slice(0, 10)}` : \"\"}`);\n\nfor (const signal of [\"SIGINT\", \"SIGTERM\"] as const) {\n process.on(signal, () => {\n console.log(`[broadpaper] ${signal}, closing`);\n void server.close().then(() => process.exit(0));\n });\n}\n"],"mappings":";;;;;;AAQA,SAAS,4BAA4B;AAGrC,IAAM,MAAM,QAAQ;AACpB,IAAM,OAAO,CAAC,OAA2B,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAEhG,IAAM,SAAS,MAAM,mBAAmB;AAAA,EACtC,MAAM,OAAO,IAAI,QAAQ,IAAI,mBAAmB,IAAI;AAAA,EACpD,MAAM,IAAI,QAAQ;AAAA,EAClB,OAAO,IAAI;AAAA,EACX,UAAU,IAAI,wBAAwB;AAAA,EACtC,aAAa,OAAO,IAAI,0BAA0B,CAAC;AAAA,EACnD,gBAAgB,OAAO,IAAI,+BAA+B,GAAM;AAAA,EAChE,WAAW,OAAO,IAAI,yBAAyB,GAAM;AAAA,EACrD,UAAU,OAAO,IAAI,wBAAwB,GAAG;AAAA,EAChD,cAAc,OAAO,IAAI,6BAA6B,KAAK,OAAO,IAAI;AAAA,EACtE,cAAc,KAAK,IAAI,wBAAwB;AAAA,EAC/C,MAAM,IAAI,oBAAoB,QAAQ,QAAQ,IAAI,mBAAmB;AAAA,EACrE,SAAS,IAAI;AAAA,EACb,KAAK,CAAC,SAAS,WAAW,QAAQ,IAAI,gBAAgB,OAAO,GAAG,SAAS,MAAM,KAAK,UAAU,MAAM,IAAI,EAAE,EAAE;AAC9G,CAAC;AAED,IAAI,CAAC,IAAI,iBAAkB,SAAQ,KAAK,oGAA+F;AAKvI,IAAM,UAAU,MAAM,qBAAqB;AAC3C,QAAQ,IAAI,yBAAyB,QAAQ,KAAK,GAAG,QAAQ,cAAc,KAAK,QAAQ,WAAW,MAAM,EAAE,GAAG,QAAQ,YAAY,aAAa,QAAQ,UAAU,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE;AAEpM,WAAW,UAAU,CAAC,UAAU,SAAS,GAAY;AACnD,UAAQ,GAAG,QAAQ,MAAM;AACvB,YAAQ,IAAI,gBAAgB,MAAM,WAAW;AAC7C,SAAK,OAAO,MAAM,EAAE,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EAChD,CAAC;AACH;","names":[]}