@broadpaper/server 0.2.1 → 0.3.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 +8 -6
- package/dist/{chunk-GKDUVUQ6.js → chunk-JOLUPQ2N.js} +139 -11
- package/dist/chunk-JOLUPQ2N.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +77 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-GKDUVUQ6.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
The BroadPaper render service: either PDF backend behind HTTP, with a queue, a bearer token and a Dockerfile.
|
|
8
8
|
|
|
9
|
-
Part of [**BroadPaper**](https://www.broadpaper.com) — an embeddable visual report designer and PDF renderer.
|
|
9
|
+
Part of [**BroadPaper**](https://www.broadpaper.com/developers/) — an embeddable visual report designer and PDF renderer.
|
|
10
10
|
Your users design the report in your application; BroadPaper owns the layout, the branding,
|
|
11
11
|
the pagination and the output.
|
|
12
12
|
|
|
@@ -32,14 +32,16 @@ wherever a request crosses a network you do not control.
|
|
|
32
32
|
## Licensing
|
|
33
33
|
|
|
34
34
|
BroadPaper is **free to evaluate, with no trial clock and nothing behind a feature gate**.
|
|
35
|
-
Install it, use every feature
|
|
35
|
+
Install it, use every feature and evaluate it against your real data for as long as you like.
|
|
36
|
+
Production use needs a licence.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
licence removes it
|
|
39
|
-
machine binding, no telemetry, and no call home at
|
|
38
|
+
In the software, the only difference without a licence is that generated PDFs carry an
|
|
39
|
+
evaluation mark, and a licence removes it. Contractually, the licence is also what permits
|
|
40
|
+
production use. No seats, no activation, no machine binding, no telemetry, and no call home at
|
|
41
|
+
any point.
|
|
40
42
|
|
|
41
43
|
- [How licensing works](https://www.broadpaper.com/docs/guide/licensing/)
|
|
42
|
-
- [Pricing](https://www.broadpaper.com/pricing/)
|
|
44
|
+
- [Pricing](https://www.broadpaper.com/developers/pricing/)
|
|
43
45
|
- [Licence agreement](https://www.broadpaper.com/legal/terms/)
|
|
44
46
|
|
|
45
47
|
## Documentation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/service.ts
|
|
2
2
|
import { createRegistry } from "@broadpaper/blocks";
|
|
3
3
|
import { migrateTemplate, parseTemplate } from "@broadpaper/core";
|
|
4
|
-
import {
|
|
4
|
+
import { layoutPaginated } from "@broadpaper/forme";
|
|
5
5
|
var RenderError = class extends Error {
|
|
6
6
|
constructor(message, code, status) {
|
|
7
7
|
super(message);
|
|
@@ -13,6 +13,27 @@ var RenderError = class extends Error {
|
|
|
13
13
|
status;
|
|
14
14
|
};
|
|
15
15
|
var DEFAULTS = { concurrency: 4, queueTimeoutMs: 3e4, timeoutMs: 6e4, maxPages: 500 };
|
|
16
|
+
var ESTIMATE_SAMPLE = 1e3;
|
|
17
|
+
var ESTIMATE_MARGIN = 1.25;
|
|
18
|
+
function longestList(data) {
|
|
19
|
+
let best = null;
|
|
20
|
+
const visit = (value, path, depth) => {
|
|
21
|
+
if (depth > 6 || value === null || typeof value !== "object") return;
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
if (!best || value.length > best.length) best = { path, list: value, length: value.length };
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (const [key, child] of Object.entries(value)) visit(child, [...path, key], depth + 1);
|
|
27
|
+
};
|
|
28
|
+
visit(data, [], 0);
|
|
29
|
+
return best;
|
|
30
|
+
}
|
|
31
|
+
function withList(data, path, list) {
|
|
32
|
+
if (!path.length) return list;
|
|
33
|
+
const [head, ...rest] = path;
|
|
34
|
+
const record = data;
|
|
35
|
+
return { ...record, [head]: withList(record[head], rest, list) };
|
|
36
|
+
}
|
|
16
37
|
var RenderService = class {
|
|
17
38
|
registry;
|
|
18
39
|
opts;
|
|
@@ -61,6 +82,38 @@ var RenderService = class {
|
|
|
61
82
|
this.release();
|
|
62
83
|
}
|
|
63
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Renders one document as one file or several, each of at most `maxPagesPerFile` pages.
|
|
87
|
+
*
|
|
88
|
+
* For a document that may come to more than anybody should get as one file. It is
|
|
89
|
+
* laid out once and drawn a range of pages at a time, so every file agrees about
|
|
90
|
+
* where the breaks fall and what the totals are, and a page in the third file
|
|
91
|
+
* still says which page of the whole it is. The length is estimated first, from a
|
|
92
|
+
* sample of the longest list, so a document far past `maxPages` is refused before
|
|
93
|
+
* the expensive work rather than after it.
|
|
94
|
+
*
|
|
95
|
+
* Browserless backend only: Chromium prints a finished document and has no
|
|
96
|
+
* layout to draw a range of pages from.
|
|
97
|
+
*/
|
|
98
|
+
async renderParts(request, split) {
|
|
99
|
+
if (!request || typeof request !== "object" || !request.template) throw new RenderError("Missing template", "BAD_REQUEST", 400);
|
|
100
|
+
const template = this.parse(request.template);
|
|
101
|
+
const backend = this.pickBackend(request.backend);
|
|
102
|
+
if (backend !== "forme") throw new RenderError("A document can only be split into several files on the forme backend.", "BACKEND_UNAVAILABLE", 400);
|
|
103
|
+
await this.acquire();
|
|
104
|
+
const started = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
const out = await this.withTimeout(this.formeParts({ ...request, template }, split), backend, split.timeoutMs);
|
|
107
|
+
this.opts.log?.("render", { backend, pages: out.pages, files: out.parts.length, ms: out.durationMs, warnings: out.warnings.length });
|
|
108
|
+
return out;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
const err = e;
|
|
111
|
+
this.opts.log?.("render failed", { backend, code: err.code ?? "RENDER_FAILED", message: err.message, ms: Date.now() - started });
|
|
112
|
+
throw err;
|
|
113
|
+
} finally {
|
|
114
|
+
this.release();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
64
117
|
/**
|
|
65
118
|
* Renders one template against many data sets — a monthly run, a mail merge.
|
|
66
119
|
*
|
|
@@ -138,9 +191,9 @@ var RenderService = class {
|
|
|
138
191
|
style: f.style ?? "normal"
|
|
139
192
|
}));
|
|
140
193
|
}
|
|
141
|
-
|
|
194
|
+
formeOptions(request) {
|
|
142
195
|
const fonts = this.fonts(request);
|
|
143
|
-
|
|
196
|
+
return {
|
|
144
197
|
template: request.template,
|
|
145
198
|
registry: this.registry,
|
|
146
199
|
data: request.data,
|
|
@@ -151,16 +204,77 @@ var RenderService = class {
|
|
|
151
204
|
currency: request.currency,
|
|
152
205
|
timeZone: request.timeZone,
|
|
153
206
|
metadata: request.metadata,
|
|
207
|
+
...request.watermark ? { watermark: request.watermark } : {},
|
|
154
208
|
...this.opts.renderer ? { renderer: this.opts.renderer } : {},
|
|
155
209
|
...fonts.length ? { fonts } : {},
|
|
156
210
|
...request.tagged ? { tagged: true } : {},
|
|
157
211
|
...request.pdfUa ? { pdfUa: true } : {},
|
|
158
212
|
...request.pdfA ? { pdfA: request.pdfA } : {}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
tooManyPages(pages, estimated = false) {
|
|
216
|
+
return new RenderError(
|
|
217
|
+
`Document is ${estimated ? "about " : ""}${pages} pages; this service allows ${this.opts.maxPages}.`,
|
|
218
|
+
"TOO_MANY_PAGES",
|
|
219
|
+
413
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
/*
|
|
223
|
+
* Laid out, counted, and only then drawn.
|
|
224
|
+
*
|
|
225
|
+
* The page cap used to be checked on the finished PDF, so a document ten times
|
|
226
|
+
* too long was measured, paginated and drawn in full — twenty-seven minutes for
|
|
227
|
+
* fifty thousand rows — and then refused. Pagination is decided before drawing,
|
|
228
|
+
* and drawing is the larger half of the work, so the count is checked between.
|
|
229
|
+
*/
|
|
230
|
+
async renderForme(request) {
|
|
231
|
+
const started = Date.now();
|
|
232
|
+
const options = this.formeOptions(request);
|
|
233
|
+
const estimate = await this.estimatePages(options);
|
|
234
|
+
if (estimate !== null && estimate > this.opts.maxPages * ESTIMATE_MARGIN) throw this.tooManyPages(estimate, true);
|
|
235
|
+
const layout = await layoutPaginated(options);
|
|
236
|
+
if (layout.paged.totalPages > this.opts.maxPages) throw this.tooManyPages(layout.paged.totalPages);
|
|
237
|
+
const drawn = await layout.draw();
|
|
238
|
+
return { pdf: drawn.pdf, pages: layout.paged.totalPages, warnings: drawn.warnings, durationMs: Date.now() - started, backend: "forme" };
|
|
239
|
+
}
|
|
240
|
+
async formeParts(request, split) {
|
|
241
|
+
const started = Date.now();
|
|
242
|
+
const per = Math.max(1, Math.floor(split.maxPagesPerFile));
|
|
243
|
+
const options = this.formeOptions(request);
|
|
244
|
+
const estimate = await this.estimatePages(options);
|
|
245
|
+
if (estimate !== null) {
|
|
246
|
+
split.onEstimate?.({ pages: estimate, files: Math.max(1, Math.ceil(estimate / per)), exact: false });
|
|
247
|
+
if (estimate > this.opts.maxPages * ESTIMATE_MARGIN) throw this.tooManyPages(estimate, true);
|
|
162
248
|
}
|
|
163
|
-
|
|
249
|
+
const layout = await layoutPaginated(options);
|
|
250
|
+
const total = layout.paged.totalPages;
|
|
251
|
+
if (total > this.opts.maxPages) throw this.tooManyPages(total);
|
|
252
|
+
split.onEstimate?.({ pages: total, files: Math.max(1, Math.ceil(total / per)), exact: true });
|
|
253
|
+
const parts = [];
|
|
254
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
255
|
+
for (let from = 0; from < total; from += per) {
|
|
256
|
+
const to = Math.min(total, from + per);
|
|
257
|
+
const drawn = await layout.draw({ from, to });
|
|
258
|
+
drawn.warnings.forEach((w) => warnings.add(w));
|
|
259
|
+
parts.push({ pdf: drawn.pdf, pages: to - from, fromPage: from + 1, toPage: to });
|
|
260
|
+
}
|
|
261
|
+
return { parts, pages: total, warnings: [...warnings], durationMs: Date.now() - started, backend: "forme" };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* About how many pages a document will come to, from a sample of its longest list.
|
|
265
|
+
*
|
|
266
|
+
* Null when no list is long enough to be worth sampling, because then laying the
|
|
267
|
+
* whole document out is about as quick as laying a sample out. It is an estimate
|
|
268
|
+
* — rows are not all the same height, and totals drawn from a sample differ —
|
|
269
|
+
* which is why it only ever refuses a document well past the ceiling, and why
|
|
270
|
+
* the exact count is still checked before anything is drawn.
|
|
271
|
+
*/
|
|
272
|
+
async estimatePages(options) {
|
|
273
|
+
const longest = longestList(options.data);
|
|
274
|
+
if (!longest || longest.length <= ESTIMATE_SAMPLE) return null;
|
|
275
|
+
const sampled = withList(options.data, longest.path, longest.list.slice(0, ESTIMATE_SAMPLE));
|
|
276
|
+
const layout = await layoutPaginated({ ...options, data: sampled });
|
|
277
|
+
return Math.ceil(layout.paged.totalPages * longest.length / ESTIMATE_SAMPLE);
|
|
164
278
|
}
|
|
165
279
|
async renderChromium(request) {
|
|
166
280
|
const result = await this.opts.chromium.render({
|
|
@@ -174,6 +288,10 @@ var RenderService = class {
|
|
|
174
288
|
timeZone: request.timeZone,
|
|
175
289
|
metadata: request.metadata,
|
|
176
290
|
network: { allowedHosts: this.allowedHosts(request.network?.allowedHosts) },
|
|
291
|
+
// Passed to both backends so that changing backend cannot quietly drop a
|
|
292
|
+
// caller's mark. For a free tier that depends on one, silence would be
|
|
293
|
+
// the expensive kind.
|
|
294
|
+
...request.watermark ? { watermark: request.watermark } : {},
|
|
177
295
|
customBlocksScript: this.opts.customBlocksScript,
|
|
178
296
|
timeoutMs: this.opts.timeoutMs,
|
|
179
297
|
maxPages: this.opts.maxPages
|
|
@@ -186,13 +304,23 @@ var RenderService = class {
|
|
|
186
304
|
backend: "chromium"
|
|
187
305
|
};
|
|
188
306
|
}
|
|
189
|
-
|
|
307
|
+
/*
|
|
308
|
+
* A timeout on the request, not on the work.
|
|
309
|
+
*
|
|
310
|
+
* The engine runs on the calling thread and nothing here can interrupt it, so
|
|
311
|
+
* this rejects the caller and leaves the render to finish on its own — and while
|
|
312
|
+
* it runs, a synchronous engine holds the thread and the timer cannot fire until
|
|
313
|
+
* it yields. A host that must be able to stop a runaway document has to run the
|
|
314
|
+
* service somewhere it can terminate: a worker thread or a child process.
|
|
315
|
+
* BroadPaper Cloud does exactly that.
|
|
316
|
+
*/
|
|
317
|
+
async withTimeout(work, backend, timeoutMs = this.opts.timeoutMs) {
|
|
190
318
|
let timer;
|
|
191
319
|
try {
|
|
192
320
|
return await Promise.race([
|
|
193
321
|
work,
|
|
194
322
|
new Promise((_, reject) => {
|
|
195
|
-
timer = setTimeout(() => reject(new RenderError(`Render timed out after ${
|
|
323
|
+
timer = setTimeout(() => reject(new RenderError(`Render timed out after ${timeoutMs} ms on the ${backend} backend.`, "TIMEOUT", 504)), timeoutMs);
|
|
196
324
|
})
|
|
197
325
|
]);
|
|
198
326
|
} catch (e) {
|
|
@@ -316,7 +444,7 @@ function createRenderHandler(options = {}) {
|
|
|
316
444
|
backends: service.backends,
|
|
317
445
|
defaultBackend: service.defaultBackend,
|
|
318
446
|
...service.load,
|
|
319
|
-
version: options.version ?? "0.
|
|
447
|
+
version: options.version ?? "0.3.0",
|
|
320
448
|
protocol: PROTOCOL_VERSION,
|
|
321
449
|
license: { state: license.state, plan: license.plan, validUntil: license.validUntil?.toISOString() ?? null }
|
|
322
450
|
};
|
|
@@ -395,4 +523,4 @@ export {
|
|
|
395
523
|
createRenderHandler,
|
|
396
524
|
createRenderServer
|
|
397
525
|
};
|
|
398
|
-
//# sourceMappingURL=chunk-
|
|
526
|
+
//# sourceMappingURL=chunk-JOLUPQ2N.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 { layoutPaginated, type FormeFont, type LayoutPaginatedOptions } 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 watermark?: unknown;\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\n/** How a document longer than one file should be divided. */\nexport interface RenderPartsOptions {\n /** The most pages one file may hold. A longer document comes back as several, in order. */\n maxPagesPerFile: number;\n /**\n * Told how long the document looks, first as an estimate from a sample — before\n * the expensive work starts — and then exactly, once it is paginated. A host uses\n * it to tell somebody how many files are coming, or to allow a long document\n * longer to finish.\n */\n onEstimate?: (estimate: { pages: number; files: number; exact: boolean }) => void;\n /** Overrides the service's timeout for this document, which a long one needs. */\n timeoutMs?: number;\n}\n\nexport interface RenderPart {\n pdf: Uint8Array;\n pages: number;\n /** The first and last page of the whole document this file holds, counting from 1. */\n fromPage: number;\n toPage: number;\n}\n\nexport interface RenderPartsOutcome {\n parts: RenderPart[];\n /** Pages in the whole document. */\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\n/** Items of the longest list laid out to estimate a document's length. */\nconst ESTIMATE_SAMPLE = 1_000;\n/**\n * How far past the ceiling an estimate must be before it refuses on its own. An\n * estimate is only that, and a document refused on a guess that was wrong would be\n * worse than one refused a minute later on the exact count.\n */\nconst ESTIMATE_MARGIN = 1.25;\n\n/** The longest array anywhere in the data, and the path of keys to it. */\nfunction longestList(data: unknown): { path: string[]; list: unknown[]; length: number } | null {\n let best: { path: string[]; list: unknown[]; length: number } | null = null;\n const visit = (value: unknown, path: string[], depth: number) => {\n if (depth > 6 || value === null || typeof value !== \"object\") return;\n if (Array.isArray(value)) {\n if (!best || value.length > best.length) best = { path, list: value, length: value.length };\n return;\n }\n for (const [key, child] of Object.entries(value as Record<string, unknown>)) visit(child, [...path, key], depth + 1);\n };\n visit(data, [], 0);\n return best;\n}\n\n/** `data` with the array at `path` replaced, copying only the objects on the way to it. */\nfunction withList(data: unknown, path: string[], list: unknown[]): unknown {\n if (!path.length) return list;\n const [head, ...rest] = path;\n const record = data as Record<string, unknown>;\n return { ...record, [head!]: withList(record[head!], rest, list) };\n}\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 document as one file or several, each of at most `maxPagesPerFile` pages.\n *\n * For a document that may come to more than anybody should get as one file. It is\n * laid out once and drawn a range of pages at a time, so every file agrees about\n * where the breaks fall and what the totals are, and a page in the third file\n * still says which page of the whole it is. The length is estimated first, from a\n * sample of the longest list, so a document far past `maxPages` is refused before\n * the expensive work rather than after it.\n *\n * Browserless backend only: Chromium prints a finished document and has no\n * layout to draw a range of pages from.\n */\n async renderParts(request: RenderRequest, split: RenderPartsOptions): Promise<RenderPartsOutcome> {\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 if (backend !== \"forme\") throw new RenderError(\"A document can only be split into several files on the forme backend.\", \"BACKEND_UNAVAILABLE\", 400);\n await this.acquire();\n const started = Date.now();\n try {\n const out = await this.withTimeout(this.formeParts({ ...request, template }, split), backend, split.timeoutMs);\n this.opts.log?.(\"render\", { backend, pages: out.pages, files: out.parts.length, 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 formeOptions(request: RenderRequest): LayoutPaginatedOptions {\n const fonts = this.fonts(request);\n return {\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 ...(request.watermark ? { watermark: request.watermark } : {}),\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 }\n\n private tooManyPages(pages: number, estimated = false): RenderError {\n return new RenderError(\n `Document is ${estimated ? \"about \" : \"\"}${pages} pages; this service allows ${this.opts.maxPages}.`,\n \"TOO_MANY_PAGES\",\n 413\n );\n }\n\n /*\n * Laid out, counted, and only then drawn.\n *\n * The page cap used to be checked on the finished PDF, so a document ten times\n * too long was measured, paginated and drawn in full — twenty-seven minutes for\n * fifty thousand rows — and then refused. Pagination is decided before drawing,\n * and drawing is the larger half of the work, so the count is checked between.\n */\n private async renderForme(request: RenderRequest): Promise<RenderOutcome> {\n const started = Date.now();\n const options = this.formeOptions(request);\n const estimate = await this.estimatePages(options);\n if (estimate !== null && estimate > this.opts.maxPages * ESTIMATE_MARGIN) throw this.tooManyPages(estimate, true);\n const layout = await layoutPaginated(options);\n if (layout.paged.totalPages > this.opts.maxPages) throw this.tooManyPages(layout.paged.totalPages);\n const drawn = await layout.draw();\n return { pdf: drawn.pdf, pages: layout.paged.totalPages, warnings: drawn.warnings, durationMs: Date.now() - started, backend: \"forme\" };\n }\n\n private async formeParts(request: RenderRequest, split: RenderPartsOptions): Promise<RenderPartsOutcome> {\n const started = Date.now();\n const per = Math.max(1, Math.floor(split.maxPagesPerFile));\n const options = this.formeOptions(request);\n\n const estimate = await this.estimatePages(options);\n if (estimate !== null) {\n split.onEstimate?.({ pages: estimate, files: Math.max(1, Math.ceil(estimate / per)), exact: false });\n if (estimate > this.opts.maxPages * ESTIMATE_MARGIN) throw this.tooManyPages(estimate, true);\n }\n\n const layout = await layoutPaginated(options);\n const total = layout.paged.totalPages;\n if (total > this.opts.maxPages) throw this.tooManyPages(total);\n split.onEstimate?.({ pages: total, files: Math.max(1, Math.ceil(total / per)), exact: true });\n\n const parts: RenderPart[] = [];\n const warnings = new Set<string>();\n for (let from = 0; from < total; from += per) {\n const to = Math.min(total, from + per);\n const drawn = await layout.draw({ from, to });\n drawn.warnings.forEach((w) => warnings.add(w));\n parts.push({ pdf: drawn.pdf, pages: to - from, fromPage: from + 1, toPage: to });\n }\n return { parts, pages: total, warnings: [...warnings], durationMs: Date.now() - started, backend: \"forme\" };\n }\n\n /**\n * About how many pages a document will come to, from a sample of its longest list.\n *\n * Null when no list is long enough to be worth sampling, because then laying the\n * whole document out is about as quick as laying a sample out. It is an estimate\n * — rows are not all the same height, and totals drawn from a sample differ —\n * which is why it only ever refuses a document well past the ceiling, and why\n * the exact count is still checked before anything is drawn.\n */\n private async estimatePages(options: LayoutPaginatedOptions): Promise<number | null> {\n const longest = longestList(options.data);\n if (!longest || longest.length <= ESTIMATE_SAMPLE) return null;\n const sampled = withList(options.data, longest.path, (longest.list as unknown[]).slice(0, ESTIMATE_SAMPLE));\n const layout = await layoutPaginated({ ...options, data: sampled as LayoutPaginatedOptions[\"data\"] });\n return Math.ceil((layout.paged.totalPages * longest.length) / ESTIMATE_SAMPLE);\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 // Passed to both backends so that changing backend cannot quietly drop a\n // caller's mark. For a free tier that depends on one, silence would be\n // the expensive kind.\n ...(request.watermark ? { watermark: request.watermark } : {}),\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 /*\n * A timeout on the request, not on the work.\n *\n * The engine runs on the calling thread and nothing here can interrupt it, so\n * this rejects the caller and leaves the render to finish on its own — and while\n * it runs, a synchronous engine holds the thread and the timer cannot fire until\n * it yields. A host that must be able to stop a runaway document has to run the\n * service somewhere it can terminate: a worker thread or a child process.\n * BroadPaper Cloud does exactly that.\n */\n private async withTimeout<T>(work: Promise<T>, backend: RenderBackend, timeoutMs = this.opts.timeoutMs): Promise<T> {\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 ${timeoutMs} ms on the ${backend} backend.`, \"TIMEOUT\", 504)), 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 { LicenseState, Plan, WatermarkSpec } 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 /**\n * An extra mark drawn behind every page.\n *\n * Additive and never a replacement. The licence mark is decided by the\n * service's own licence and a request cannot remove it, so this can only ever\n * put a mark on a document — never take one off. It is here for a host whose\n * own commercial model wants one (BroadPaper Cloud marks what its free plan\n * produces) and for the ordinary \"DRAFT\" a business document sometimes needs.\n *\n * Give it a colour and an opacity. An unstyled mark draws solid black at full\n * opacity and the document underneath cannot be read.\n */\n watermark?: WatermarkSpec;\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 plan: Plan | null;\n /** ISO 8601. Null when there is no licence, or when it has no end date. */\n validUntil: 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.3.0\",\n protocol: PROTOCOL_VERSION,\n license: { state: license.state, plan: license.plan, validUntil: license.validUntil?.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.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReportData, Theme, ReportTemplate, AnyVersionTemplate, DataSource, BlockDefinition } from '@broadpaper/core';
|
|
2
|
-
import { LicenseState, Plan } from '@broadpaper/license';
|
|
3
|
-
export { BroadPaper, BroadPaperConfig, LicenseState, LicenseStatus, Plan, configure, resolveLicenseStatus } from '@broadpaper/license';
|
|
2
|
+
import { WatermarkSpec, LicenseState, Plan } from '@broadpaper/license';
|
|
3
|
+
export { BroadPaper, BroadPaperConfig, LicenseState, LicenseStatus, Plan, WatermarkSpec, configure, resolveLicenseStatus } from '@broadpaper/license';
|
|
4
4
|
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -57,6 +57,19 @@ interface RenderRequest {
|
|
|
57
57
|
pdfUa?: boolean;
|
|
58
58
|
/** PDF/A conformance level. Needs an embeddable font. */
|
|
59
59
|
pdfA?: "2b" | "2u" | "2a" | "3b" | "3u" | "3a";
|
|
60
|
+
/**
|
|
61
|
+
* An extra mark drawn behind every page.
|
|
62
|
+
*
|
|
63
|
+
* Additive and never a replacement. The licence mark is decided by the
|
|
64
|
+
* service's own licence and a request cannot remove it, so this can only ever
|
|
65
|
+
* put a mark on a document — never take one off. It is here for a host whose
|
|
66
|
+
* own commercial model wants one (BroadPaper Cloud marks what its free plan
|
|
67
|
+
* produces) and for the ordinary "DRAFT" a business document sometimes needs.
|
|
68
|
+
*
|
|
69
|
+
* Give it a colour and an opacity. An unstyled mark draws solid black at full
|
|
70
|
+
* opacity and the document underneath cannot be read.
|
|
71
|
+
*/
|
|
72
|
+
watermark?: WatermarkSpec;
|
|
60
73
|
/** Hosts remote assets may be fetched from. May narrow the service's list, never widen it. */
|
|
61
74
|
network?: {
|
|
62
75
|
allowedHosts?: string[];
|
|
@@ -171,6 +184,7 @@ interface ChromiumBackend {
|
|
|
171
184
|
allowedHosts?: string[];
|
|
172
185
|
blockPrivateNetworks?: boolean;
|
|
173
186
|
};
|
|
187
|
+
watermark?: unknown;
|
|
174
188
|
customBlocksScript?: string;
|
|
175
189
|
timeoutMs?: number;
|
|
176
190
|
maxPages?: number;
|
|
@@ -225,6 +239,39 @@ interface RenderOutcome {
|
|
|
225
239
|
durationMs: number;
|
|
226
240
|
backend: RenderBackend;
|
|
227
241
|
}
|
|
242
|
+
/** How a document longer than one file should be divided. */
|
|
243
|
+
interface RenderPartsOptions {
|
|
244
|
+
/** The most pages one file may hold. A longer document comes back as several, in order. */
|
|
245
|
+
maxPagesPerFile: number;
|
|
246
|
+
/**
|
|
247
|
+
* Told how long the document looks, first as an estimate from a sample — before
|
|
248
|
+
* the expensive work starts — and then exactly, once it is paginated. A host uses
|
|
249
|
+
* it to tell somebody how many files are coming, or to allow a long document
|
|
250
|
+
* longer to finish.
|
|
251
|
+
*/
|
|
252
|
+
onEstimate?: (estimate: {
|
|
253
|
+
pages: number;
|
|
254
|
+
files: number;
|
|
255
|
+
exact: boolean;
|
|
256
|
+
}) => void;
|
|
257
|
+
/** Overrides the service's timeout for this document, which a long one needs. */
|
|
258
|
+
timeoutMs?: number;
|
|
259
|
+
}
|
|
260
|
+
interface RenderPart {
|
|
261
|
+
pdf: Uint8Array;
|
|
262
|
+
pages: number;
|
|
263
|
+
/** The first and last page of the whole document this file holds, counting from 1. */
|
|
264
|
+
fromPage: number;
|
|
265
|
+
toPage: number;
|
|
266
|
+
}
|
|
267
|
+
interface RenderPartsOutcome {
|
|
268
|
+
parts: RenderPart[];
|
|
269
|
+
/** Pages in the whole document. */
|
|
270
|
+
pages: number;
|
|
271
|
+
warnings: string[];
|
|
272
|
+
durationMs: number;
|
|
273
|
+
backend: RenderBackend;
|
|
274
|
+
}
|
|
228
275
|
declare class RenderService {
|
|
229
276
|
private readonly registry;
|
|
230
277
|
private readonly opts;
|
|
@@ -246,6 +293,20 @@ declare class RenderService {
|
|
|
246
293
|
* service permits — carrying the code and status the wire protocol names.
|
|
247
294
|
*/
|
|
248
295
|
render(request: RenderRequest): Promise<RenderOutcome>;
|
|
296
|
+
/**
|
|
297
|
+
* Renders one document as one file or several, each of at most `maxPagesPerFile` pages.
|
|
298
|
+
*
|
|
299
|
+
* For a document that may come to more than anybody should get as one file. It is
|
|
300
|
+
* laid out once and drawn a range of pages at a time, so every file agrees about
|
|
301
|
+
* where the breaks fall and what the totals are, and a page in the third file
|
|
302
|
+
* still says which page of the whole it is. The length is estimated first, from a
|
|
303
|
+
* sample of the longest list, so a document far past `maxPages` is refused before
|
|
304
|
+
* the expensive work rather than after it.
|
|
305
|
+
*
|
|
306
|
+
* Browserless backend only: Chromium prints a finished document and has no
|
|
307
|
+
* layout to draw a range of pages from.
|
|
308
|
+
*/
|
|
309
|
+
renderParts(request: RenderRequest, split: RenderPartsOptions): Promise<RenderPartsOutcome>;
|
|
249
310
|
/**
|
|
250
311
|
* Renders one template against many data sets — a monthly run, a mail merge.
|
|
251
312
|
*
|
|
@@ -273,7 +334,20 @@ declare class RenderService {
|
|
|
273
334
|
*/
|
|
274
335
|
private allowedHosts;
|
|
275
336
|
private fonts;
|
|
337
|
+
private formeOptions;
|
|
338
|
+
private tooManyPages;
|
|
276
339
|
private renderForme;
|
|
340
|
+
private formeParts;
|
|
341
|
+
/**
|
|
342
|
+
* About how many pages a document will come to, from a sample of its longest list.
|
|
343
|
+
*
|
|
344
|
+
* Null when no list is long enough to be worth sampling, because then laying the
|
|
345
|
+
* whole document out is about as quick as laying a sample out. It is an estimate
|
|
346
|
+
* — rows are not all the same height, and totals drawn from a sample differ —
|
|
347
|
+
* which is why it only ever refuses a document well past the ceiling, and why
|
|
348
|
+
* the exact count is still checked before anything is drawn.
|
|
349
|
+
*/
|
|
350
|
+
private estimatePages;
|
|
277
351
|
private renderChromium;
|
|
278
352
|
private withTimeout;
|
|
279
353
|
/**
|
|
@@ -340,4 +414,4 @@ declare function createRenderServer(options?: RenderHandlerOptions & {
|
|
|
340
414
|
host?: string;
|
|
341
415
|
}): Promise<RenderServer>;
|
|
342
416
|
|
|
343
|
-
export { type BatchItem, type BatchRequest, type BatchResponse, type ChromiumBackend, type ErrorResponse, HEADERS, type HealthResponse, PROTOCOL_VERSION, type RenderBackend, RenderError, type RenderErrorCode, type RenderFont, type RenderHandler, type RenderHandlerOptions, type RenderJsonResponse, type RenderMetadata, type RenderOutcome, type RenderRequest, type RenderServer, RenderService, type RenderServiceOptions, createRenderHandler, createRenderServer };
|
|
417
|
+
export { type BatchItem, type BatchRequest, type BatchResponse, type ChromiumBackend, type ErrorResponse, HEADERS, type HealthResponse, PROTOCOL_VERSION, type RenderBackend, RenderError, type RenderErrorCode, type RenderFont, type RenderHandler, type RenderHandlerOptions, type RenderJsonResponse, type RenderMetadata, type RenderOutcome, type RenderPart, type RenderPartsOptions, type RenderPartsOutcome, type RenderRequest, type RenderServer, RenderService, type RenderServiceOptions, createRenderHandler, createRenderServer };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @broadpaper/server — render BroadPaper templates to PDF on a server.\n *\n * The browserless engine by default, so this runs in a plain Node image with\n * no browser to install; Chromium is available by injecting it, for the cases\n * that want the browser's own text shaping.\n *\n * import { createRenderServer } from \"@broadpaper/server\";\n * const server = await createRenderServer({ token: process.env.TOKEN });\n *\n * Or mounted inside an app you already have:\n *\n * const render = createRenderHandler({ basePath: \"/api/pdf\" });\n * http.createServer(async (req, res) => { if (!(await render(req, res))) myApp(req, res); });\n */\nexport { RenderService, RenderError } from \"./service.js\";\nexport type { RenderServiceOptions, RenderOutcome, ChromiumBackend } from \"./service.js\";\nexport { createRenderHandler, createRenderServer } from \"./http.js\";\nexport type { RenderHandler, RenderHandlerOptions, RenderServer } from \"./http.js\";\nexport { PROTOCOL_VERSION, HEADERS } from \"./protocol.js\";\nexport type {\n RenderBackend,\n RenderRequest,\n RenderMetadata,\n RenderFont,\n RenderJsonResponse,\n BatchItem,\n BatchRequest,\n BatchResponse,\n HealthResponse,\n ErrorResponse,\n RenderErrorCode\n} from \"./protocol.js\";\n\n/**\n * Licensing.\n *\n * A render service renders under *its own* licence, set once at startup from\n * `BROADPAPER_LICENSE` or here in code. A licence is deliberately not part of\n * the render request: the service decides what its output is, not its callers.\n */\nexport { BroadPaper, configure, resolveLicenseStatus } from \"@broadpaper/license\";\nexport type { BroadPaperConfig, LicenseState, LicenseStatus, Plan } from \"@broadpaper/license\";\n"],"mappings":";;;;;;;;;;AAyCA,SAAS,YAAY,WAAW,4BAA4B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @broadpaper/server — render BroadPaper templates to PDF on a server.\n *\n * The browserless engine by default, so this runs in a plain Node image with\n * no browser to install; Chromium is available by injecting it, for the cases\n * that want the browser's own text shaping.\n *\n * import { createRenderServer } from \"@broadpaper/server\";\n * const server = await createRenderServer({ token: process.env.TOKEN });\n *\n * Or mounted inside an app you already have:\n *\n * const render = createRenderHandler({ basePath: \"/api/pdf\" });\n * http.createServer(async (req, res) => { if (!(await render(req, res))) myApp(req, res); });\n */\nexport { RenderService, RenderError } from \"./service.js\";\nexport type { RenderServiceOptions, RenderOutcome, RenderPart, RenderPartsOptions, RenderPartsOutcome, ChromiumBackend } from \"./service.js\";\nexport { createRenderHandler, createRenderServer } from \"./http.js\";\nexport type { RenderHandler, RenderHandlerOptions, RenderServer } from \"./http.js\";\nexport { PROTOCOL_VERSION, HEADERS } from \"./protocol.js\";\nexport type {\n RenderBackend,\n RenderRequest,\n RenderMetadata,\n RenderFont,\n RenderJsonResponse,\n BatchItem,\n BatchRequest,\n BatchResponse,\n HealthResponse,\n ErrorResponse,\n RenderErrorCode\n} from \"./protocol.js\";\n\n/**\n * Licensing.\n *\n * A render service renders under *its own* licence, set once at startup from\n * `BROADPAPER_LICENSE` or here in code. A licence is deliberately not part of\n * the render request: the service decides what its output is, not its callers.\n */\nexport { BroadPaper, configure, resolveLicenseStatus } from \"@broadpaper/license\";\nexport type { BroadPaperConfig, LicenseState, LicenseStatus, Plan, WatermarkSpec } from \"@broadpaper/license\";\n"],"mappings":";;;;;;;;;;AAyCA,SAAS,YAAY,WAAW,4BAA4B;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broadpaper/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Server-side PDF rendering for BroadPaper: an HTTP render service and a mountable handler, browserless by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
},
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@broadpaper/
|
|
47
|
-
"@broadpaper/
|
|
48
|
-
"@broadpaper/
|
|
49
|
-
"@broadpaper/
|
|
46
|
+
"@broadpaper/blocks": "0.3.0",
|
|
47
|
+
"@broadpaper/forme": "0.3.0",
|
|
48
|
+
"@broadpaper/license": "0.3.0",
|
|
49
|
+
"@broadpaper/core": "0.3.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsup src/index.ts src/cli.ts --format esm --dts --sourcemap --clean --external @broadpaper/core --external @broadpaper/blocks --external @broadpaper/forme --external @broadpaper/license --external @formepdf/core",
|
|
@@ -1 +0,0 @@
|
|
|
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 { LicenseState, Plan } 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 plan: Plan | null;\n /** ISO 8601. Null when there is no licence, or when it has no end date. */\n validUntil: 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.2.1\",\n protocol: PROTOCOL_VERSION,\n license: { state: license.state, plan: license.plan, validUntil: license.validUntil?.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":[]}
|