@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,343 @@
1
+ import { ReportData, Theme, ReportTemplate, AnyVersionTemplate, DataSource, BlockDefinition } from '@broadpaper/core';
2
+ import { LicenseState, Entitlement } from '@broadpaper/license';
3
+ export { BroadPaper, BroadPaperConfig, Entitlement, LicenseState, LicenseStatus, configure, resolveLicenseStatus } from '@broadpaper/license';
4
+ import { IncomingMessage, ServerResponse, Server } from 'node:http';
5
+
6
+ /**
7
+ * The wire protocol of the render service.
8
+ *
9
+ * This file is the contract between the service, the browser client and the
10
+ * .NET client under `dotnet/`. Every field here is JSON — a template, a theme
11
+ * and a data set are all host-defined documents — so the same shapes are
12
+ * expressible in any language, and the .NET types mirror these names exactly.
13
+ *
14
+ * Anything added here must be optional, because a client is always older than
15
+ * the service it is talking to.
16
+ */
17
+
18
+ /** Which engine draws the pages. BroadPaper decides the pages either way. */
19
+ type RenderBackend = "forme" | "chromium";
20
+ interface RenderMetadata {
21
+ title?: string;
22
+ author?: string;
23
+ subject?: string;
24
+ keywords?: string[];
25
+ /** Document language, e.g. "en-GB". Required for PDF/UA. */
26
+ lang?: string;
27
+ }
28
+ /** A font to embed. Without one the engine falls back to its standard faces. */
29
+ interface RenderFont {
30
+ family: string;
31
+ /** A path the service can read, a `data:` URI, or base64 in `bytes`. */
32
+ src?: string;
33
+ /** Base64-encoded TTF/OTF. Use this from a client that cannot share a filesystem. */
34
+ bytes?: string;
35
+ weight?: number;
36
+ style?: "normal" | "italic";
37
+ }
38
+ interface RenderRequest {
39
+ /** The saved template. Older schema versions are migrated on the way in. */
40
+ template: ReportTemplate | AnyVersionTemplate;
41
+ /** Runtime data, keyed by data source id. */
42
+ data?: ReportData;
43
+ theme?: Partial<Theme>;
44
+ dataSources?: DataSource[];
45
+ /** ISO-8601. Fixes the clock, which is one of the conditions for byte-identical output. */
46
+ now?: string;
47
+ locale?: string;
48
+ currency?: string;
49
+ timeZone?: string;
50
+ metadata?: RenderMetadata;
51
+ /** Overrides the service default, when the service permits it. */
52
+ backend?: RenderBackend;
53
+ fonts?: RenderFont[];
54
+ /** Tagged (accessible) PDF. */
55
+ tagged?: boolean;
56
+ /** PDF/UA-1 conformance. Needs an embeddable font. */
57
+ pdfUa?: boolean;
58
+ /** PDF/A conformance level. Needs an embeddable font. */
59
+ pdfA?: "2b" | "2u" | "2a" | "3b" | "3u" | "3a";
60
+ /** Hosts remote assets may be fetched from. May narrow the service's list, never widen it. */
61
+ network?: {
62
+ allowedHosts?: string[];
63
+ };
64
+ /** "pdf" returns the bytes; "json" returns {@link RenderJsonResponse}. Default "pdf". */
65
+ format?: "pdf" | "json";
66
+ }
67
+ /** One entry of a batch: the same template rendered against different data. */
68
+ interface BatchItem {
69
+ /** Echoed back on the result, so a caller can match them up. */
70
+ id: string;
71
+ data?: ReportData;
72
+ theme?: Partial<Theme>;
73
+ metadata?: RenderMetadata;
74
+ now?: string;
75
+ locale?: string;
76
+ currency?: string;
77
+ timeZone?: string;
78
+ }
79
+ interface BatchRequest extends Omit<RenderRequest, "data" | "format"> {
80
+ items: BatchItem[];
81
+ }
82
+ interface RenderJsonResponse {
83
+ pdfBase64: string;
84
+ pages: number;
85
+ warnings: string[];
86
+ durationMs: number;
87
+ backend: RenderBackend;
88
+ }
89
+ interface BatchResponse {
90
+ results: Array<{
91
+ id: string;
92
+ ok: true;
93
+ pdfBase64: string;
94
+ pages: number;
95
+ warnings: string[];
96
+ durationMs: number;
97
+ } | {
98
+ id: string;
99
+ ok: false;
100
+ error: string;
101
+ code: RenderErrorCode;
102
+ }>;
103
+ backend: RenderBackend;
104
+ durationMs: number;
105
+ }
106
+ interface HealthResponse {
107
+ ok: boolean;
108
+ /** Backends this service can actually use right now. */
109
+ backends: RenderBackend[];
110
+ defaultBackend: RenderBackend;
111
+ active: number;
112
+ queued: number;
113
+ version: string;
114
+ /** The protocol revision, so a client can refuse a service it does not understand. */
115
+ protocol: number;
116
+ /**
117
+ * What this service's PDFs will come out as.
118
+ *
119
+ * Reported because the alternative is an operator discovering the answer in a
120
+ * finance report a customer has already been sent. `state: "unlicensed"` is a
121
+ * perfectly good answer in a development environment and a serious one in
122
+ * production, and only the operator can tell which they are looking at.
123
+ */
124
+ license?: {
125
+ state: LicenseState;
126
+ /** Null unless the licence is being honoured. */
127
+ entitlement: Entitlement | null;
128
+ /** ISO 8601, when a licence was readable. */
129
+ expiresAt: string | null;
130
+ };
131
+ }
132
+ type RenderErrorCode = "BAD_REQUEST" | "UNAUTHORISED" | "PAYLOAD_TOO_LARGE" | "BACKEND_UNAVAILABLE" | "TIMEOUT" | "TOO_MANY_PAGES" | "BUSY" | "RENDER_FAILED";
133
+ interface ErrorResponse {
134
+ error: string;
135
+ code: RenderErrorCode;
136
+ }
137
+ /** Bumped when a change would break an older client. */
138
+ declare const PROTOCOL_VERSION = 1;
139
+ /** Response headers a client can read without parsing the body. */
140
+ declare const HEADERS: {
141
+ readonly pages: "x-broadpaper-pages";
142
+ readonly warnings: "x-broadpaper-warnings";
143
+ readonly backend: "x-broadpaper-backend";
144
+ readonly durationMs: "x-broadpaper-duration-ms";
145
+ };
146
+
147
+ declare class RenderError extends Error {
148
+ readonly code: RenderErrorCode;
149
+ readonly status: number;
150
+ constructor(message: string, code: RenderErrorCode, status: number);
151
+ }
152
+ /**
153
+ * What a Chromium backend has to provide.
154
+ *
155
+ * Injected rather than imported, so this package never depends on Playwright
156
+ * and a service that only wants the browserless engine does not drag a browser
157
+ * into its image.
158
+ */
159
+ interface ChromiumBackend {
160
+ render(opts: {
161
+ template: unknown;
162
+ data?: unknown;
163
+ theme?: unknown;
164
+ dataSources?: unknown;
165
+ now?: Date;
166
+ locale?: string;
167
+ currency?: string;
168
+ timeZone?: string;
169
+ metadata?: unknown;
170
+ network?: {
171
+ allowedHosts?: string[];
172
+ blockPrivateNetworks?: boolean;
173
+ };
174
+ customBlocksScript?: string;
175
+ timeoutMs?: number;
176
+ maxPages?: number;
177
+ }): Promise<{
178
+ pdf: Uint8Array;
179
+ pages: number;
180
+ warnings: Array<string | {
181
+ message: string;
182
+ }>;
183
+ durationMs: number;
184
+ }>;
185
+ close?(): Promise<void>;
186
+ }
187
+ interface RenderServiceOptions {
188
+ /** Which backend to use when a request does not name one. Default "forme". */
189
+ defaultBackend?: RenderBackend;
190
+ /**
191
+ * Backends a request may ask for. Defaults to whatever is available. Narrow
192
+ * it when you do not want callers choosing the expensive one.
193
+ */
194
+ allowedBackends?: RenderBackend[];
195
+ /** Custom blocks for the browserless backend: the host's own code, in process. */
196
+ blocks?: Array<BlockDefinition<never>>;
197
+ /** Custom blocks bundle for Chromium (an IIFE calling `BroadPaper.registerBlocks`). */
198
+ customBlocksScript?: string;
199
+ /** Fonts embedded in every render. A request may add to these, never replace them. */
200
+ fonts?: RenderFont[];
201
+ /** Chromium backend. Without one, only the browserless backend is available. */
202
+ chromium?: ChromiumBackend;
203
+ /** Concurrent renders. Default 4. */
204
+ concurrency?: number;
205
+ /** How long a request may wait for a slot before being refused. Default 30s. */
206
+ queueTimeoutMs?: number;
207
+ /** Per-render timeout. Default 60s. */
208
+ timeoutMs?: number;
209
+ /** Refuse documents longer than this. Default 500 pages. */
210
+ maxPages?: number;
211
+ /** Hosts remote assets may be fetched from. A request may narrow this, never widen it. */
212
+ allowedHosts?: string[];
213
+ /** Injected Forme entry point, so a host can pick the node, browser or worker build. */
214
+ renderer?: (doc: Record<string, unknown>) => Promise<{
215
+ pdf: Uint8Array;
216
+ layout?: unknown;
217
+ warnings?: string[];
218
+ }>;
219
+ log?: (message: string, detail?: Record<string, unknown>) => void;
220
+ }
221
+ interface RenderOutcome {
222
+ pdf: Uint8Array;
223
+ pages: number;
224
+ warnings: string[];
225
+ durationMs: number;
226
+ backend: RenderBackend;
227
+ }
228
+ declare class RenderService {
229
+ private readonly registry;
230
+ private readonly opts;
231
+ private active;
232
+ private readonly waiting;
233
+ constructor(options?: RenderServiceOptions);
234
+ /** Backends this service can actually use, in preference order. */
235
+ get backends(): RenderBackend[];
236
+ get defaultBackend(): RenderBackend;
237
+ get load(): {
238
+ active: number;
239
+ queued: number;
240
+ };
241
+ /**
242
+ * Renders one document.
243
+ *
244
+ * Throws {@link RenderError} for anything a caller can act on — a backend
245
+ * they may not use, a queue that never cleared, a document longer than the
246
+ * service permits — carrying the code and status the wire protocol names.
247
+ */
248
+ render(request: RenderRequest): Promise<RenderOutcome>;
249
+ /**
250
+ * Renders one template against many data sets — a monthly run, a mail merge.
251
+ *
252
+ * One failure does not fail the batch: each result carries its own outcome,
253
+ * because a caller sending five hundred clients wants the four hundred and
254
+ * ninety-nine that worked and a list of the one that did not.
255
+ */
256
+ renderBatch(request: BatchRequest): Promise<BatchResponse>;
257
+ close(): Promise<void>;
258
+ /**
259
+ * Migrates and validates the incoming template.
260
+ *
261
+ * A service takes JSON from over the wire — hand-written, generated, saved by
262
+ * a version of the designer nobody here has seen. Handing that straight to
263
+ * the renderer turns a missing `styles` block into "Cannot read properties of
264
+ * undefined (reading 'height')", which tells the caller nothing at all.
265
+ * Parsing first fills in every default the schema declares and names the
266
+ * field when it cannot.
267
+ */
268
+ private parse;
269
+ private pickBackend;
270
+ /**
271
+ * Narrows the network policy. A request may restrict what a render fetches;
272
+ * it may not reach anywhere the service was not already willing to go.
273
+ */
274
+ private allowedHosts;
275
+ private fonts;
276
+ private renderForme;
277
+ private renderChromium;
278
+ private withTimeout;
279
+ /**
280
+ * Waits for a slot, but not forever. An unbounded queue turns one busy minute
281
+ * into a pile of work whose callers have all given up, and then does it
282
+ * anyway.
283
+ */
284
+ private acquire;
285
+ private release;
286
+ }
287
+
288
+ /**
289
+ * HTTP in front of the render service.
290
+ *
291
+ * Two layers, on purpose. `createRenderHandler` is a plain Node
292
+ * `(req, res) => Promise<boolean>` that returns whether it handled the request,
293
+ * so it drops into an existing Express, Fastify, Connect or raw `http` app —
294
+ * most teams already have a service and do not want a second one to deploy.
295
+ * `createRenderServer` is that handler with a listener around it, for the teams
296
+ * that do.
297
+ *
298
+ * The routes are the whole API surface:
299
+ *
300
+ * POST /render → PDF bytes, or JSON with `format: "json"`
301
+ * POST /render/batch → one template, many data sets, JSON
302
+ * GET /health → readiness, backends, load, protocol version
303
+ */
304
+
305
+ interface RenderHandlerOptions extends RenderServiceOptions {
306
+ /** Shared-secret bearer token. Strongly recommended anywhere but a laptop. */
307
+ token?: string;
308
+ /** Maximum request body size in bytes. Default 25 MB. */
309
+ maxBodyBytes?: number;
310
+ /**
311
+ * Origins allowed to call this from a browser. `"*"` is the default because
312
+ * the token is what actually guards the endpoint; set it when you want the
313
+ * browser to enforce an origin too.
314
+ */
315
+ cors?: string | false;
316
+ /** Mount point, e.g. "/api/pdf". Default "". */
317
+ basePath?: string;
318
+ /** Reported by `/health`, so an operator can see what is deployed. */
319
+ version?: string;
320
+ }
321
+ interface RenderHandler {
322
+ (req: IncomingMessage, res: ServerResponse): Promise<boolean>;
323
+ service: RenderService;
324
+ close(): Promise<void>;
325
+ }
326
+ /**
327
+ * Builds the request handler. Returns `false` when the request was not one of
328
+ * ours, so a host can fall through to its own routes.
329
+ */
330
+ declare function createRenderHandler(options?: RenderHandlerOptions): RenderHandler;
331
+ interface RenderServer {
332
+ server: Server;
333
+ port: number;
334
+ service: RenderService;
335
+ close(): Promise<void>;
336
+ }
337
+ /** The handler, with a listener around it. */
338
+ declare function createRenderServer(options?: RenderHandlerOptions & {
339
+ port?: number;
340
+ host?: string;
341
+ }): Promise<RenderServer>;
342
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ HEADERS,
3
+ PROTOCOL_VERSION,
4
+ RenderError,
5
+ RenderService,
6
+ createRenderHandler,
7
+ createRenderServer
8
+ } from "./chunk-5GQOM7MG.js";
9
+
10
+ // src/index.ts
11
+ import { BroadPaper, configure, resolveLicenseStatus } from "@broadpaper/license";
12
+ export {
13
+ BroadPaper,
14
+ HEADERS,
15
+ PROTOCOL_VERSION,
16
+ RenderError,
17
+ RenderService,
18
+ configure,
19
+ createRenderHandler,
20
+ createRenderServer,
21
+ resolveLicenseStatus
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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, Entitlement, LicenseState, LicenseStatus } from \"@broadpaper/license\";\n"],"mappings":";;;;;;;;;;AAyCA,SAAS,YAAY,WAAW,4BAA4B;","names":[]}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@broadpaper/server",
3
+ "version": "0.1.1",
4
+ "description": "Server-side PDF rendering for BroadPaper: an HTTP render service and a mountable handler, browserless by default.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "Dockerfile",
18
+ "LICENSE",
19
+ "THIRD-PARTY-NOTICES.md"
20
+ ],
21
+ "bin": {
22
+ "broadpaper-server": "./dist/cli.js"
23
+ },
24
+ "sideEffects": false,
25
+ "dependencies": {
26
+ "@broadpaper/blocks": "0.1.1",
27
+ "@broadpaper/core": "0.1.1",
28
+ "@broadpaper/license": "0.1.1",
29
+ "@broadpaper/forme": "0.1.1"
30
+ },
31
+ "scripts": {
32
+ "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",
33
+ "typecheck": "tsc --noEmit -p tsconfig.json",
34
+ "serve": "node dist/cli.js"
35
+ }
36
+ }