@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/.env.example +11 -0
- package/CHANGELOG.md +106 -0
- package/LICENSE +15 -0
- package/QUICK_START.md +462 -0
- package/README.md +580 -0
- package/SECURITY.md +417 -0
- package/package.json +57 -0
- package/server/app.js +151 -0
- package/src/index.js +7 -0
- package/src/services/forms.js +163 -0
- package/src/services/generator.js +147 -0
- package/src/services/merger.js +115 -0
- package/src/utils/helpers.js +220 -0
- package/src/xPDF.js +126 -0
- package/test/xPDF.test.js +903 -0
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();
|