@xenterprises/fastify-xpdf 1.0.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/SECURITY.md ADDED
@@ -0,0 +1,417 @@
1
+ # Security Policy
2
+
3
+ ## Overview
4
+
5
+ xPDF is a Fastify plugin for PDF generation and manipulation. This document outlines the security considerations, best practices, and guidelines for using xPDF safely in production environments.
6
+
7
+ ## Security Model
8
+
9
+ ### What xPDF Does Safely
10
+ - **PDF Generation**: HTML/Markdown rendered in isolated Puppeteer context - user code never executes
11
+ - **Form Operations**: PDF manipulation using pdf-lib library (pure JavaScript, no native bindings)
12
+ - **Input Validation**: All inputs are validated before processing
13
+ - **Storage**: Optional xStorage integration uses S3-compatible APIs with access control
14
+
15
+ ### Attack Surface
16
+ The main security concerns are:
17
+
18
+ 1. **Puppeteer Sandboxing** - Browser process isolation
19
+ 2. **Untrusted HTML/Markdown** - User-supplied content rendering
20
+ 3. **PDF Resource Consumption** - DoS potential with large operations
21
+ 4. **Storage Access** - Cloud storage credential handling
22
+
23
+ ## Security Guidelines
24
+
25
+ ### 1. Puppeteer Sandboxing
26
+
27
+ **Default Configuration**
28
+ ```javascript
29
+ args = ["--no-sandbox", "--disable-setuid-sandbox"]
30
+ ```
31
+
32
+ **Why**: Linux environments often need these flags for Puppeteer to run. This reduces isolation.
33
+
34
+ **Risk Level**: Medium in containerized environments, Low in Docker with resource limits
35
+
36
+ **Mitigation Strategies**:
37
+
38
+ #### Option A: Use Docker (Recommended)
39
+ ```dockerfile
40
+ FROM node:20-alpine
41
+ RUN apk add --no-cache chromium
42
+ ENV PUPPETEER_SKIP_DOWNLOAD=true
43
+ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
44
+ WORKDIR /app
45
+ COPY . .
46
+ RUN npm install
47
+ CMD ["npm", "start"]
48
+ ```
49
+
50
+ Docker provides OS-level isolation. No additional Puppeteer args needed.
51
+
52
+ #### Option B: Enable Sandboxing on Supported Systems
53
+ ```javascript
54
+ await fastify.register(xPDF, {
55
+ // Remove no-sandbox args for systems that support it
56
+ args: [],
57
+ // or use only essential args
58
+ args: ["--disable-dev-shm-usage"],
59
+ });
60
+ ```
61
+
62
+ #### Option C: Run in Virtual Machine/Separate Process
63
+ - Run Puppeteer in a separate, isolated VM
64
+ - Use IPC for communication
65
+ - Limit resources at OS level
66
+
67
+ **Recommendation**: Use Option A (Docker) for production. It's the simplest and most secure.
68
+
69
+ ---
70
+
71
+ ### 2. HTML/Markdown Input Handling
72
+
73
+ **Current Behavior**: xPDF passes HTML directly to Puppeteer without sanitization.
74
+
75
+ **Why This is Safe**:
76
+ - PDFs are static output - no JavaScript execution in the PDF
77
+ - Puppeteer runs in isolated process
78
+ - User code never runs in your application
79
+
80
+ **When to Sanitize**:
81
+ You should sanitize if:
82
+ - HTML/Markdown comes from untrusted users
83
+ - You generate HTML from user input and pass to xPDF
84
+ - Content includes user-uploaded files or links
85
+
86
+ **Sanitization Example**:
87
+ ```javascript
88
+ import DOMPurify from 'isomorphic-dompurify';
89
+
90
+ // Sanitize user HTML before passing to xPDF
91
+ const userHtml = '<img src="x" onerror="alert(1)">';
92
+ const safeHtml = DOMPurify.sanitize(userHtml);
93
+
94
+ const pdf = await fastify.xPDF.generateFromHtml(safeHtml);
95
+ ```
96
+
97
+ **Markdown Safety**:
98
+ The `marked` library is designed for safe HTML output. However:
99
+ - Unsafe markdown options can enable HTML: `{ breaks: true, gfm: true }`
100
+ - User-provided URLs in markdown could be malicious
101
+ - Sanitize output if markdown comes from untrusted sources
102
+
103
+ ```javascript
104
+ import { marked } from 'marked';
105
+ import DOMPurify from 'isomorphic-dompurify';
106
+
107
+ const userMarkdown = '# [Click here](javascript:alert(1))';
108
+ const html = marked(userMarkdown);
109
+ const safeHtml = DOMPurify.sanitize(html);
110
+ const pdf = await fastify.xPDF.generateFromMarkdown(safeHtml);
111
+ ```
112
+
113
+ ---
114
+
115
+ ### 3. Resource Consumption Protection
116
+
117
+ **Risk**: Unbounded PDF operations could cause DoS or resource exhaustion.
118
+
119
+ **Scenarios**:
120
+ - Generating many large PDFs concurrently
121
+ - Processing extremely large HTML documents
122
+ - Merging thousands of PDFs
123
+ - Exposed via HTTP without rate limiting
124
+
125
+ **Mitigation**:
126
+
127
+ #### A. Implement Concurrency Limits
128
+ ```javascript
129
+ import pQueue from 'p-queue';
130
+
131
+ const pdfQueue = new pQueue({ concurrency: 5, interval: 1000, intervalCap: 50 });
132
+
133
+ // Wrapper
134
+ const generatePdfLimited = (html, options) =>
135
+ pdfQueue.add(() => fastify.xPDF.generateFromHtml(html, options));
136
+
137
+ const pdf = await generatePdfLimited('<h1>Test</h1>');
138
+ ```
139
+
140
+ #### B. Set Timeouts
141
+ ```javascript
142
+ // Add to xPDF configuration
143
+ await fastify.register(xPDF, {
144
+ // Default timeouts are already configured (30s)
145
+ // You can control them via options
146
+ });
147
+
148
+ // Or per-operation
149
+ const timeout = new Promise((_, reject) =>
150
+ setTimeout(() => reject(new Error('PDF timeout')), 60000)
151
+ );
152
+
153
+ Promise.race([
154
+ fastify.xPDF.generateFromHtml(html),
155
+ timeout
156
+ ]);
157
+ ```
158
+
159
+ #### C. Rate Limit at HTTP Level
160
+ ```javascript
161
+ import rateLimit from '@fastify/rate-limit';
162
+
163
+ await fastify.register(rateLimit, {
164
+ max: 100,
165
+ timeWindow: '15 minutes',
166
+ });
167
+
168
+ fastify.post('/pdf/generate', async (request, reply) => {
169
+ const pdf = await fastify.xPDF.generateFromHtml(request.body.html);
170
+ return { url: pdf.url };
171
+ });
172
+ ```
173
+
174
+ #### D. Monitor Resource Usage
175
+ ```javascript
176
+ import os from 'os';
177
+
178
+ fastify.addHook('onRequest', async (request) => {
179
+ const memUsage = process.memoryUsage();
180
+ if (memUsage.heapUsed > 1024 * 1024 * 1024) { // 1GB
181
+ throw new Error('Server overloaded - please try again later');
182
+ }
183
+ });
184
+ ```
185
+
186
+ **Runtime Configuration**:
187
+ ```bash
188
+ # Limit Node.js heap size
189
+ NODE_OPTIONS=--max-old-space-size=2048 npm start
190
+
191
+ # Use cgroup limits in Docker
192
+ docker run --memory="2g" --cpus="2" my-app
193
+ ```
194
+
195
+ ---
196
+
197
+ ### 4. xStorage Credential Security
198
+
199
+ **Risk**: S3 credentials stored insecurely
200
+
201
+ **Safe Practices**:
202
+ ```javascript
203
+ // ✅ Use environment variables
204
+ await fastify.register(xStorage, {
205
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
206
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
207
+ });
208
+
209
+ // ✅ Use IAM roles in AWS/EC2
210
+ // ✅ Use STS temporary credentials
211
+ // ✅ Rotate credentials regularly
212
+
213
+ // ❌ Never hardcode credentials
214
+ // ❌ Never commit credentials to git
215
+ // ❌ Never log credentials
216
+ ```
217
+
218
+ **xStorage Settings**:
219
+ ```javascript
220
+ await fastify.register(xStorage, {
221
+ // Default ACL: 'private' (secure by default)
222
+ // Only make specific files public with signed URLs
223
+ // Never use 'public-read' for sensitive PDFs
224
+ });
225
+ ```
226
+
227
+ ---
228
+
229
+ ### 5. HTTP Endpoint Security
230
+
231
+ **Example Server**:
232
+ ```javascript
233
+ import Fastify from 'fastify';
234
+ import rateLimit from '@fastify/rate-limit';
235
+ import helmet from '@fastify/helmet';
236
+ import xPDF from '@xenterprises/fastify-xpdf';
237
+
238
+ const fastify = Fastify();
239
+
240
+ // Security headers
241
+ await fastify.register(helmet);
242
+
243
+ // Rate limiting
244
+ await fastify.register(rateLimit, {
245
+ max: 100,
246
+ timeWindow: '15 minutes',
247
+ });
248
+
249
+ // Register xPDF
250
+ await fastify.register(xPDF, {
251
+ useStorage: true,
252
+ defaultFolder: 'pdfs',
253
+ });
254
+
255
+ // Input validation
256
+ fastify.post('/pdf/generate', async (request, reply) => {
257
+ // 1. Validate content length
258
+ const MAX_SIZE = 1024 * 1024; // 1MB
259
+ if (request.rawBody.length > MAX_SIZE) {
260
+ throw new Error('Content too large');
261
+ }
262
+
263
+ // 2. Validate format
264
+ const { html } = request.body;
265
+ if (typeof html !== 'string' || !html.trim()) {
266
+ throw new Error('Invalid HTML');
267
+ }
268
+
269
+ // 3. Sanitize if needed
270
+ const safeHtml = DOMPurify.sanitize(html);
271
+
272
+ // 4. Generate PDF with timeout
273
+ const timeout = new Promise((_, r) =>
274
+ setTimeout(() => r(new Error('Timeout')), 30000)
275
+ );
276
+
277
+ const pdf = await Promise.race([
278
+ fastify.xPDF.generateFromHtml(safeHtml),
279
+ timeout
280
+ ]);
281
+
282
+ return { url: pdf.url };
283
+ });
284
+
285
+ await fastify.listen({ port: 3000, host: '127.0.0.1' });
286
+ ```
287
+
288
+ ---
289
+
290
+ ### 6. Error Handling
291
+
292
+ **Risk**: Errors could expose internal information
293
+
294
+ **Best Practice**:
295
+ ```javascript
296
+ // ✅ Log full errors internally
297
+ fastify.setErrorHandler((error, request, reply) => {
298
+ fastify.log.error(error); // Full error logged
299
+
300
+ // Return generic error to client
301
+ reply.status(500).send({
302
+ error: 'PDF generation failed',
303
+ code: 'PDF_ERROR',
304
+ // Don't expose internal details
305
+ });
306
+ });
307
+
308
+ // ❌ Don't expose internal errors
309
+ // reply.send({ error: error.message }); // Bad!
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Dependency Security
315
+
316
+ ### Regular Updates
317
+ ```bash
318
+ # Check for vulnerabilities
319
+ npm audit
320
+
321
+ # Update to latest secure versions
322
+ npm audit fix
323
+
324
+ # Subscribe to security updates
325
+ npm audit --audit-level=moderate
326
+ ```
327
+
328
+ ### Key Dependencies
329
+ - **puppeteer**: Keep updated for browser security patches
330
+ - **marked**: Check for XSS vulnerabilities
331
+ - **pdf-lib**: Pure JavaScript - no native code risks
332
+
333
+ ---
334
+
335
+ ## Compliance & Standards
336
+
337
+ ### PDF Standards
338
+ - xPDF generates **PDF 1.4** compatible files
339
+ - No encryption by default (passwords not supported in v1.0)
340
+ - No digital signatures (planned for v1.2)
341
+
342
+ ### Data Privacy
343
+ If using xStorage:
344
+ - Configure encryption at rest (S3 SSE)
345
+ - Use HTTPS for all transfers
346
+ - Enable bucket logging
347
+ - Set bucket policies to restrict access
348
+
349
+ ```javascript
350
+ // Example secure xStorage config
351
+ await fastify.register(xStorage, {
352
+ endpoint: process.env.S3_ENDPOINT,
353
+ region: process.env.S3_REGION,
354
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
355
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
356
+ bucket: process.env.S3_BUCKET,
357
+ publicUrl: process.env.S3_PUBLIC_URL,
358
+
359
+ // Use only when absolutely necessary
360
+ defaultAcl: 'private', // Secure by default
361
+ });
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Reporting Security Issues
367
+
368
+ **Please do not open public GitHub/GitLab issues for security vulnerabilities.**
369
+
370
+ Email security concerns to: [contact info when available]
371
+
372
+ Include:
373
+ - Description of vulnerability
374
+ - Steps to reproduce
375
+ - Potential impact
376
+ - Suggested fix (if any)
377
+
378
+ **Response Time**: We aim to respond within 48 hours and release patches within 2 weeks.
379
+
380
+ ---
381
+
382
+ ## Security Checklist for Production
383
+
384
+ - [ ] Running in Docker or VM with resource limits
385
+ - [ ] Using strong authentication for xStorage
386
+ - [ ] Rate limiting enabled on HTTP endpoints
387
+ - [ ] Input validation and sanitization in place
388
+ - [ ] Error handling doesn't expose details
389
+ - [ ] Monitoring and logging configured
390
+ - [ ] Security headers enabled (@fastify/helmet)
391
+ - [ ] Dependencies regularly updated
392
+ - [ ] Credentials in environment variables
393
+ - [ ] Access logs enabled
394
+ - [ ] Backup strategy for stored PDFs
395
+ - [ ] HTTPS enforced for all endpoints
396
+
397
+ ---
398
+
399
+ ## Additional Resources
400
+
401
+ - [OWASP Top 10](https://owasp.org/www-project-top-ten/)
402
+ - [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
403
+ - [Fastify Security](https://www.fastify.io/docs/latest/Guides/Security/)
404
+ - [Puppeteer Security](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md)
405
+
406
+ ---
407
+
408
+ ## Version History
409
+
410
+ | Version | Date | Security Updates |
411
+ |---------|------|------------------|
412
+ | 1.0.0 | 2024-12-29 | Initial release |
413
+
414
+ ---
415
+
416
+ **Last Updated**: 2024-12-29
417
+ **Maintained By**: Tim Mushen
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@xenterprises/fastify-xpdf",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Fastify plugin for PDF generation and manipulation. Convert HTML/Markdown to PDF, fill forms, and merge PDFs.",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./helpers": "./src/utils/helpers.js"
10
+ },
11
+ "scripts": {
12
+ "start": "fastify start -l info server/app.js",
13
+ "dev": "fastify start -w -l info -P server/app.js",
14
+ "test": "node --test test/xPDF.test.js"
15
+ },
16
+ "keywords": [
17
+ "fastify",
18
+ "pdf",
19
+ "puppeteer",
20
+ "html-to-pdf",
21
+ "markdown-to-pdf",
22
+ "pdf-forms",
23
+ "pdf-merge",
24
+ "pdf-generation"
25
+ ],
26
+ "author": "Tim Mushen",
27
+ "license": "ISC",
28
+ "engines": {
29
+ "node": ">=20.0.0",
30
+ "npm": ">=10.0.0"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-x-pdf"
35
+ },
36
+ "bugs": {
37
+ "url": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-x-pdf/-/issues"
38
+ },
39
+ "homepage": "https://gitlab.com/x-enterprises/fastify-plugins/fastify-x-pdf#readme",
40
+ "dependencies": {
41
+ "fastify-plugin": "^5.0.0",
42
+ "marked": "^15.0.0",
43
+ "pdf-lib": "^1.17.1",
44
+ "puppeteer": "^23.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "fastify": "^5.1.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@xenterprises/fastify-xstorage": "^1.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "@xenterprises/fastify-xstorage": {
54
+ "optional": true
55
+ }
56
+ }
57
+ }
package/server/app.js ADDED
@@ -0,0 +1,151 @@
1
+ // server/app.js
2
+ import Fastify from "fastify";
3
+ import xPDF from "../src/xPDF.js";
4
+
5
+ const fastify = Fastify({
6
+ logger: true,
7
+ });
8
+
9
+ // xPDF is a library of methods, not a server with routes
10
+ // The routes below are EXAMPLES of how to use the fastify.xPDF methods
11
+ // You can implement your own routes or use xPDF methods directly in your application
12
+
13
+ // Register xPDF
14
+ await fastify.register(xPDF, {
15
+ headless: true,
16
+ useStorage: false,
17
+ defaultFolder: "pdfs",
18
+ format: "A4",
19
+ printBackground: true,
20
+ margin: {
21
+ top: "1cm",
22
+ right: "1cm",
23
+ bottom: "1cm",
24
+ left: "1cm",
25
+ },
26
+ });
27
+
28
+ // Example: HTML to PDF
29
+ fastify.post("/pdf/from-html", async (request, reply) => {
30
+ const { html, filename = "document.pdf" } = request.body;
31
+
32
+ if (!html) {
33
+ return reply.code(400).send({ error: "HTML content required" });
34
+ }
35
+
36
+ const result = await fastify.xPDF.generateFromHtml(html, {
37
+ filename,
38
+ saveToStorage: false,
39
+ });
40
+
41
+ return {
42
+ success: true,
43
+ filename: result.filename,
44
+ size: result.size,
45
+ };
46
+ });
47
+
48
+ // Example: Markdown to PDF
49
+ fastify.post("/pdf/from-markdown", async (request, reply) => {
50
+ const { markdown, filename = "document.pdf" } = request.body;
51
+
52
+ if (!markdown) {
53
+ return reply.code(400).send({ error: "Markdown content required" });
54
+ }
55
+
56
+ const result = await fastify.xPDF.generateFromMarkdown(markdown, {
57
+ filename,
58
+ saveToStorage: false,
59
+ });
60
+
61
+ return {
62
+ success: true,
63
+ filename: result.filename,
64
+ size: result.size,
65
+ };
66
+ });
67
+
68
+ // Example: Fill PDF form
69
+ fastify.post("/pdf/fill-form", async (request, reply) => {
70
+ const { pdfBase64, fieldValues = {}, filename = "filled-form.pdf" } = request.body;
71
+
72
+ if (!pdfBase64) {
73
+ return reply.code(400).send({ error: "PDF (base64) required" });
74
+ }
75
+
76
+ // Convert base64 to buffer
77
+ const pdfBuffer = Buffer.from(pdfBase64, "base64");
78
+
79
+ const result = await fastify.xPDF.fillForm(pdfBuffer, fieldValues, {
80
+ flatten: true,
81
+ filename,
82
+ saveToStorage: false,
83
+ });
84
+
85
+ return {
86
+ success: true,
87
+ filename: result.filename,
88
+ size: result.size,
89
+ };
90
+ });
91
+
92
+ // Example: List PDF form fields
93
+ fastify.post("/pdf/list-fields", async (request, reply) => {
94
+ const { pdfBase64 } = request.body;
95
+
96
+ if (!pdfBase64) {
97
+ return reply.code(400).send({ error: "PDF (base64) required" });
98
+ }
99
+
100
+ // Convert base64 to buffer
101
+ const pdfBuffer = Buffer.from(pdfBase64, "base64");
102
+
103
+ const fields = await fastify.xPDF.listFormFields(pdfBuffer);
104
+
105
+ return {
106
+ success: true,
107
+ fields,
108
+ count: fields.length,
109
+ };
110
+ });
111
+
112
+ // Example: Merge PDFs
113
+ fastify.post("/pdf/merge", async (request, reply) => {
114
+ const { pdfsBase64 = [], filename = "merged.pdf" } = request.body;
115
+
116
+ if (!Array.isArray(pdfsBase64) || pdfsBase64.length === 0) {
117
+ return reply.code(400).send({ error: "Array of PDFs (base64) required" });
118
+ }
119
+
120
+ // Convert base64 strings to buffers
121
+ const buffers = pdfsBase64.map((base64) => Buffer.from(base64, "base64"));
122
+
123
+ const result = await fastify.xPDF.mergePDFs(buffers, {
124
+ filename,
125
+ saveToStorage: false,
126
+ });
127
+
128
+ return {
129
+ success: true,
130
+ filename: result.filename,
131
+ size: result.size,
132
+ pageCount: result.pageCount,
133
+ };
134
+ });
135
+
136
+ // Health check
137
+ fastify.get("/health", async () => {
138
+ return { status: "ok", timestamp: new Date().toISOString() };
139
+ });
140
+
141
+ // Start server
142
+ const start = async () => {
143
+ try {
144
+ await fastify.listen({ port: process.env.PORT || 3000, host: "0.0.0.0" });
145
+ } catch (err) {
146
+ fastify.log.error(err);
147
+ process.exit(1);
148
+ }
149
+ };
150
+
151
+ start();
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Main exports for xPDF plugin
3
+ */
4
+
5
+ export { default } from "./xPDF.js";
6
+ export { default as xPDF } from "./xPDF.js";
7
+ export * as helpers from "./utils/helpers.js";