n8n-nodes-pdfkraft 0.2.0 → 0.4.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 ADDED
@@ -0,0 +1,131 @@
1
+ # n8n-nodes-pdfkraft
2
+
3
+ An n8n community node for [PDFKraft](https://pdfkraft.com) — generate PDFs and images from templates using your n8n workflows.
4
+
5
+ ## Installation
6
+
7
+ In your n8n instance, go to **Settings → Community Nodes → Install** and enter:
8
+
9
+ ```
10
+ n8n-nodes-pdfkraft
11
+ ```
12
+
13
+ ## Credentials
14
+
15
+ After installation, create a **PDFKraft API** credential:
16
+
17
+ 1. Go to [pdfkraft.com/dashboard/account](https://pdfkraft.com/dashboard/account) and copy your API key.
18
+ 2. In n8n, go to **Credentials → New → PDFKraft API**.
19
+ 3. Paste your API key. Leave the Base URL as `https://api.pdfkraft.com/api/v1`.
20
+
21
+ ## PDFKraft Node
22
+
23
+ Provides six operations:
24
+
25
+ ### Generate Document
26
+ Queues a document for generation and returns immediately with a document ID. Use this when you want to fire-and-forget and handle completion via a webhook trigger.
27
+
28
+ **Inputs:** Template, Payload (JSON), Filename (optional)
29
+ **Output:** `{ id, status: "pending", ... }`
30
+
31
+ ### Generate Document and Wait
32
+ Generates a document and polls until complete. Returns the final document record including `download_url`.
33
+
34
+ **Inputs:** Template, Payload (JSON), Filename, Timeout (seconds)
35
+ **Output:** `{ id, status: "success", download_url, filename, ... }`
36
+
37
+ ### Generate Document and Download
38
+ The most common operation. Generates a document, waits for completion, and downloads the file — all in one step.
39
+
40
+ **Inputs:** Template, Payload (JSON), Filename, Timeout, Output Field Name
41
+ **Output:** JSON (`id`, `filename`, `download_url`) + Binary file in the configured output field
42
+
43
+ ### Get Document
44
+ Fetches the current status and metadata of any document by ID.
45
+
46
+ **Inputs:** Document ID
47
+ **Output:** Full document record
48
+
49
+ ### Download Document
50
+ Downloads a completed document as binary data.
51
+
52
+ **Inputs:** Document ID, Output Field Name
53
+ **Output:** JSON (`documentId`, `filename`, `download_url`) + Binary file
54
+
55
+ ### List Documents
56
+ Returns generated documents with optional status filtering and pagination.
57
+
58
+ **Inputs:** Status Filter (All / Success / Pending / Failure), Limit or Return All
59
+ **Output:** One item per document
60
+
61
+ ### List Templates
62
+ Returns all templates in your account.
63
+
64
+ **Output:** One item per template
65
+
66
+ ---
67
+
68
+ ### Template Variables helper
69
+
70
+ When you select a template, open the **Template Variables** dropdown to see what variables that template expects, along with their sample values. This is a read-only reference — it does not affect the generated document.
71
+
72
+ To define sample data for your templates, open the template editor in PDFKraft and fill in the **Sample Data** section.
73
+
74
+ ---
75
+
76
+ ## PDFKraft Trigger Node
77
+
78
+ Listens for webhook events from PDFKraft and triggers your workflow when documents are generated.
79
+
80
+ ### Setup
81
+
82
+ 1. In PDFKraft, go to **Settings → Webhooks → Create Endpoint**.
83
+ 2. Set the URL to the webhook URL shown in your n8n trigger node.
84
+ 3. Copy the **Secret** shown after creation.
85
+ 4. In n8n, paste the secret into the **Webhook Secret** field.
86
+
87
+ ### Options
88
+
89
+ | Field | Description |
90
+ |---|---|
91
+ | Webhook Secret | The secret from your PDFKraft webhook endpoint. Used to verify request authenticity. |
92
+ | Verify Signature | Recommended. Rejects requests that don't have a valid HMAC signature. |
93
+ | Event Types | Choose Document Succeeded, Document Failed, or both. |
94
+ | Template Filter | Only fire for documents from a specific template. Select "All Templates" to receive all events. |
95
+
96
+ ### Output
97
+
98
+ The trigger outputs the full webhook payload:
99
+
100
+ ```json
101
+ {
102
+ "event": "document.success",
103
+ "document": {
104
+ "id": "...",
105
+ "status": "success",
106
+ "filename": "invoice-1234.pdf",
107
+ "download_url": "https://...",
108
+ "document_template_identifier": "invoice",
109
+ "meta": {},
110
+ "generated_at": "2026-06-22T10:00:00.000Z"
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## Example Workflow
116
+
117
+ **Generate an invoice PDF and save it to Google Drive:**
118
+
119
+ 1. **HTTP Request** (or any trigger) → provides order data
120
+ 2. **PDFKraft** → Generate Document and Download
121
+ - Template: `Invoice [PDF]`
122
+ - Payload: `{ "order_id": "{{ $json.id }}", "customer_name": "{{ $json.customer }}", "total": "{{ $json.total }}" }`
123
+ - Filename: `invoice-{{order_id}}.pdf`
124
+ 3. **Google Drive** → Upload File
125
+ - File Name: `{{ $json.filename }}`
126
+ - Binary Property: `data`
127
+
128
+ ## Resources
129
+
130
+ - [PDFKraft](https://pdfkraft.com)
131
+ - [n8n Community Nodes documentation](https://docs.n8n.io/integrations/community-nodes/)
@@ -4,6 +4,7 @@ export declare class PDFKraft implements INodeType {
4
4
  methods: {
5
5
  loadOptions: {
6
6
  getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
+ getTemplateSampleData(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
8
  };
8
9
  };
9
10
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
@@ -8,6 +8,32 @@ const MIME_TYPES = {
8
8
  jpg: 'image/jpeg',
9
9
  webp: 'image/webp',
10
10
  };
11
+ async function apiRequest(helpers, method, url, headers, body) {
12
+ return helpers.request({
13
+ method,
14
+ url,
15
+ headers: { ...headers, 'Content-Type': 'application/json' },
16
+ body: body ? JSON.stringify(body) : undefined,
17
+ json: true,
18
+ });
19
+ }
20
+ async function pollUntilDone(helpers, baseUrl, headers, documentId, timeoutSeconds, node, itemIndex) {
21
+ const deadline = Date.now() + timeoutSeconds * 1000;
22
+ let doc = {};
23
+ while (Date.now() < deadline) {
24
+ await new Promise(r => setTimeout(r, 2000));
25
+ doc = await apiRequest(helpers, 'GET', `${baseUrl}/document_cards/${documentId}`, headers);
26
+ if (doc.status === 'success' || doc.status === 'failure')
27
+ break;
28
+ }
29
+ if (doc.status !== 'success' && doc.status !== 'failure') {
30
+ throw new n8n_workflow_1.NodeOperationError(node, `Document ${documentId} did not complete within ${timeoutSeconds}s`, { itemIndex });
31
+ }
32
+ if (doc.status === 'failure') {
33
+ throw new n8n_workflow_1.NodeOperationError(node, `Document generation failed: ${doc.failure_cause ?? 'unknown error'}`, { itemIndex });
34
+ }
35
+ return doc;
36
+ }
11
37
  class PDFKraft {
12
38
  constructor() {
13
39
  this.description = {
@@ -23,6 +49,7 @@ class PDFKraft {
23
49
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
24
50
  credentials: [{ name: 'pdfKraftApi', required: true }],
25
51
  properties: [
52
+ // ── Operation ────────────────────────────────────────────────────────
26
53
  {
27
54
  displayName: 'Operation',
28
55
  name: 'operation',
@@ -32,15 +59,21 @@ class PDFKraft {
32
59
  {
33
60
  name: 'Generate Document',
34
61
  value: 'generateDocument',
35
- description: 'Queue a document for generation and return immediately with a document ID',
62
+ description: 'Queue a document for generation and return immediately with the document ID',
36
63
  action: 'Generate a document',
37
64
  },
38
65
  {
39
66
  name: 'Generate Document and Wait',
40
67
  value: 'generateAndWait',
41
- description: 'Generate a document and poll until it is complete — returns the final result with download URL',
68
+ description: 'Generate a document and poll until it is complete — returns the final result including the download URL',
42
69
  action: 'Generate a document and wait for result',
43
70
  },
71
+ {
72
+ name: 'Generate Document and Download',
73
+ value: 'generateAndDownload',
74
+ description: 'Generate a document, wait for it to complete, and download the file as binary data in a single step',
75
+ action: 'Generate a document and download the file',
76
+ },
44
77
  {
45
78
  name: 'Get Document',
46
79
  value: 'getDocument',
@@ -56,7 +89,7 @@ class PDFKraft {
56
89
  {
57
90
  name: 'List Documents',
58
91
  value: 'listDocuments',
59
- description: 'List recently generated documents for your account',
92
+ description: 'List generated documents for your account',
60
93
  action: 'List documents',
61
94
  },
62
95
  {
@@ -76,18 +109,29 @@ class PDFKraft {
76
109
  typeOptions: { loadOptionsMethod: 'getTemplates' },
77
110
  default: '',
78
111
  required: true,
79
- displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
112
+ displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
80
113
  description: 'The template to generate from. Only published templates appear. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
81
114
  },
82
- // ── Generate fields ──────────────────────────────────────────────────
115
+ {
116
+ displayName: 'Template Variables',
117
+ name: 'templateVarsRef',
118
+ type: 'options',
119
+ typeOptions: {
120
+ loadOptionsMethod: 'getTemplateSampleData',
121
+ loadOptionsDependsOn: ['templateId'],
122
+ },
123
+ default: '',
124
+ displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
125
+ description: 'Reference — open this dropdown to see what variables the selected template expects, along with their sample values. This field does not affect output.',
126
+ },
127
+ // ── Payload / filename ───────────────────────────────────────────────
83
128
  {
84
129
  displayName: 'Payload',
85
130
  name: 'payload',
86
131
  type: 'json',
87
132
  default: '{}',
88
- displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
89
- description: 'JSON object with variables to merge into the template. Use Liquid syntax in your template: {{ variable_name }}.',
90
- hint: 'Tip: use n8n expressions to pass data from previous nodes, e.g. <code>{{ $json.customer_name }}</code>',
133
+ displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
134
+ description: 'JSON data to merge into the template. Use <code>{{ variable_name }}</code> syntax in your template. Open the "Template Variables" dropdown above to see expected fields.',
91
135
  },
92
136
  {
93
137
  displayName: 'Filename',
@@ -95,37 +139,46 @@ class PDFKraft {
95
139
  type: 'string',
96
140
  default: '',
97
141
  placeholder: 'invoice-{{order_id}}.pdf',
98
- displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
99
- description: 'Output filename. Supports {{variable}} placeholders using values from the payload. Leave blank to use the template default.',
142
+ displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
143
+ description: 'Output filename. Supports <code>{{variable}}</code> placeholders from the payload. Leave blank to use the template default.',
144
+ },
145
+ // ── Wait / download options ──────────────────────────────────────────
146
+ {
147
+ displayName: 'Timeout (Seconds)',
148
+ name: 'timeoutSeconds',
149
+ type: 'number',
150
+ default: 60,
151
+ typeOptions: { minValue: 5, maxValue: 300 },
152
+ displayOptions: { show: { operation: ['generateAndWait', 'generateAndDownload'] } },
153
+ description: 'How long to wait for the document to complete before failing',
100
154
  },
155
+ {
156
+ displayName: 'Output Field Name',
157
+ name: 'binaryPropertyName',
158
+ type: 'string',
159
+ default: 'data',
160
+ displayOptions: { show: { operation: ['generateAndDownload', 'downloadDocument'] } },
161
+ description: 'Name of the binary output field that will contain the downloaded file',
162
+ },
163
+ // ── Additional options (collapsible) ─────────────────────────────────
101
164
  {
102
165
  displayName: 'Additional Options',
103
166
  name: 'additionalOptions',
104
167
  type: 'collection',
105
168
  placeholder: 'Add option',
106
169
  default: {},
107
- displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
170
+ displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
108
171
  options: [
109
172
  {
110
173
  displayName: 'Meta',
111
174
  name: 'meta',
112
175
  type: 'json',
113
176
  default: '{}',
114
- description: 'Arbitrary metadata to attach to the document (returned in webhook payloads and API responses)',
177
+ description: 'Arbitrary JSON metadata to attach to the document, returned in webhook payloads and API responses. Useful for tracking order IDs, user IDs, etc.',
115
178
  },
116
179
  ],
117
180
  },
118
- // ── Generate and Wait specific ───────────────────────────────────────
119
- {
120
- displayName: 'Timeout (Seconds)',
121
- name: 'timeoutSeconds',
122
- type: 'number',
123
- default: 60,
124
- typeOptions: { minValue: 5, maxValue: 300 },
125
- displayOptions: { show: { operation: ['generateAndWait'] } },
126
- description: 'How long to wait for the document to complete before failing',
127
- },
128
- // ── Document ID fields ───────────────────────────────────────────────
181
+ // ── Get / Download by ID ─────────────────────────────────────────────
129
182
  {
130
183
  displayName: 'Document ID',
131
184
  name: 'documentId',
@@ -136,16 +189,24 @@ class PDFKraft {
136
189
  displayOptions: { show: { operation: ['getDocument', 'downloadDocument'] } },
137
190
  description: 'The document ID returned by the Generate Document operation',
138
191
  },
139
- // ── Download options ─────────────────────────────────────────────────
192
+ // ── List documents ───────────────────────────────────────────────────
140
193
  {
141
- displayName: 'Output Field Name',
142
- name: 'binaryPropertyName',
143
- type: 'string',
144
- default: 'data',
145
- displayOptions: { show: { operation: ['downloadDocument'] } },
146
- description: 'Name of the output field to put the binary file data into',
194
+ displayName: 'Return All',
195
+ name: 'returnAll',
196
+ type: 'boolean',
197
+ default: false,
198
+ displayOptions: { show: { operation: ['listDocuments'] } },
199
+ description: 'Whether to return all pages of results or just the first page',
200
+ },
201
+ {
202
+ displayName: 'Limit',
203
+ name: 'limit',
204
+ type: 'number',
205
+ default: 20,
206
+ typeOptions: { minValue: 1, maxValue: 100 },
207
+ displayOptions: { show: { operation: ['listDocuments'], returnAll: [false] } },
208
+ description: 'Maximum number of documents to return',
147
209
  },
148
- // ── List documents options ───────────────────────────────────────────
149
210
  {
150
211
  displayName: 'Status Filter',
151
212
  name: 'statusFilter',
@@ -153,22 +214,13 @@ class PDFKraft {
153
214
  options: [
154
215
  { name: 'All', value: '' },
155
216
  { name: 'Success', value: 'success' },
156
- { name: 'Failure', value: 'failure' },
157
217
  { name: 'Pending', value: 'pending' },
218
+ { name: 'Failure', value: 'failure' },
158
219
  ],
159
220
  default: '',
160
221
  displayOptions: { show: { operation: ['listDocuments'] } },
161
222
  description: 'Filter documents by status',
162
223
  },
163
- {
164
- displayName: 'Limit',
165
- name: 'limit',
166
- type: 'number',
167
- default: 20,
168
- typeOptions: { minValue: 1, maxValue: 100 },
169
- displayOptions: { show: { operation: ['listDocuments'] } },
170
- description: 'Maximum number of documents to return',
171
- },
172
224
  ],
173
225
  };
174
226
  this.methods = {
@@ -185,14 +237,38 @@ class PDFKraft {
185
237
  });
186
238
  const templates = (response.data ?? []).filter(t => t.published_at);
187
239
  if (templates.length === 0) {
188
- return [{ name: 'No published templates found — publish one in PDFKraft first', value: '' }];
240
+ return [{ name: '(No published templates — publish one in PDFKraft first)', value: '' }];
189
241
  }
190
242
  return templates.map(t => ({
191
- name: `${t.name} (${t.output_type.toUpperCase()})`,
243
+ name: `${t.name} [${t.output_type.toUpperCase()}]`,
192
244
  value: t.id,
193
245
  description: t.identifier,
194
246
  }));
195
247
  },
248
+ async getTemplateSampleData() {
249
+ const templateId = this.getCurrentNodeParameter('templateId');
250
+ if (!templateId)
251
+ return [{ name: '← Select a template first', value: '' }];
252
+ const credentials = await this.getCredentials('pdfKraftApi');
253
+ const baseUrl = credentials.baseUrl.replace(/\/$/, '');
254
+ const apiKey = credentials.apiKey;
255
+ const response = await this.helpers.request({
256
+ method: 'GET',
257
+ url: `${baseUrl}/document_templates/${templateId}`,
258
+ headers: { Authorization: `Bearer ${apiKey}` },
259
+ json: true,
260
+ });
261
+ const sampleData = response.sample_data ?? response.sample_data_draft ?? {};
262
+ const entries = Object.entries(sampleData);
263
+ if (entries.length === 0) {
264
+ return [{ name: '(No sample data defined — add it in the template editor under Sample Data)', value: '' }];
265
+ }
266
+ return entries.map(([key, value]) => ({
267
+ name: key,
268
+ value: key,
269
+ description: `Sample: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`,
270
+ }));
271
+ },
196
272
  },
197
273
  };
198
274
  }
@@ -202,11 +278,12 @@ class PDFKraft {
202
278
  const credentials = await this.getCredentials('pdfKraftApi');
203
279
  const baseUrl = credentials.baseUrl.replace(/\/$/, '');
204
280
  const apiKey = credentials.apiKey;
205
- const headers = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
281
+ const authHeaders = { Authorization: `Bearer ${apiKey}` };
282
+ const jsonHeaders = { ...authHeaders, 'Content-Type': 'application/json' };
206
283
  for (let i = 0; i < items.length; i++) {
207
284
  const operation = this.getNodeParameter('operation', i);
208
- // ── Generate Document (fire and forget) ────────────────────────────
209
- if (operation === 'generateDocument') {
285
+ // ── Shared: build generate body ────────────────────────────────────
286
+ const buildGenerateBody = () => {
210
287
  const templateId = this.getNodeParameter('templateId', i);
211
288
  const payloadRaw = this.getNodeParameter('payload', i);
212
289
  const filename = this.getNodeParameter('filename', i);
@@ -218,82 +295,61 @@ class PDFKraft {
218
295
  const body = { document_template_id: templateId, payload, meta };
219
296
  if (filename)
220
297
  body.filename = filename;
221
- const response = await this.helpers.request({
222
- method: 'POST',
223
- url: `${baseUrl}/documents`,
224
- headers,
225
- body: JSON.stringify(body),
226
- json: true,
227
- });
298
+ return body;
299
+ };
300
+ // ── Generate Document (fire and forget) ────────────────────────────
301
+ if (operation === 'generateDocument') {
302
+ const response = await apiRequest(this.helpers, 'POST', `${baseUrl}/documents`, jsonHeaders, buildGenerateBody());
228
303
  results.push({ json: response });
229
304
  }
230
- // ── Generate and Wait (poll until complete) ────────────────────────
305
+ // ── Generate and Wait ──────────────────────────────────────────────
231
306
  if (operation === 'generateAndWait') {
232
- const templateId = this.getNodeParameter('templateId', i);
233
- const payloadRaw = this.getNodeParameter('payload', i);
234
- const filename = this.getNodeParameter('filename', i);
235
- const additionalOptions = this.getNodeParameter('additionalOptions', i);
236
307
  const timeoutSeconds = this.getNodeParameter('timeoutSeconds', i);
237
- const payload = typeof payloadRaw === 'string' ? JSON.parse(payloadRaw) : payloadRaw;
238
- const meta = additionalOptions.meta
239
- ? (typeof additionalOptions.meta === 'string' ? JSON.parse(additionalOptions.meta) : additionalOptions.meta)
240
- : {};
241
- const body = { document_template_id: templateId, payload, meta };
242
- if (filename)
243
- body.filename = filename;
244
- const created = await this.helpers.request({
245
- method: 'POST',
246
- url: `${baseUrl}/documents`,
247
- headers,
248
- body: JSON.stringify(body),
249
- json: true,
250
- });
251
- const documentId = created.id;
252
- const deadline = Date.now() + timeoutSeconds * 1000;
253
- let doc = {};
254
- while (Date.now() < deadline) {
255
- await new Promise(r => setTimeout(r, 2000));
256
- doc = await this.helpers.request({
257
- method: 'GET',
258
- url: `${baseUrl}/document_cards/${documentId}`,
259
- headers,
260
- json: true,
261
- });
262
- if (doc.status === 'success' || doc.status === 'failure')
263
- break;
264
- }
265
- if (doc.status !== 'success' && doc.status !== 'failure') {
266
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document ${documentId} did not complete within ${timeoutSeconds}s`, { itemIndex: i });
267
- }
268
- if (doc.status === 'failure') {
269
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document generation failed: ${doc.failure_cause ?? 'unknown error'}`, { itemIndex: i });
270
- }
308
+ const created = await apiRequest(this.helpers, 'POST', `${baseUrl}/documents`, jsonHeaders, buildGenerateBody());
309
+ const doc = await pollUntilDone(this.helpers, baseUrl, jsonHeaders, created.id, timeoutSeconds, this.getNode(), i);
271
310
  results.push({ json: doc });
272
311
  }
312
+ // ── Generate and Download ──────────────────────────────────────────
313
+ if (operation === 'generateAndDownload') {
314
+ const timeoutSeconds = this.getNodeParameter('timeoutSeconds', i);
315
+ const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
316
+ const created = await apiRequest(this.helpers, 'POST', `${baseUrl}/documents`, jsonHeaders, buildGenerateBody());
317
+ const doc = await pollUntilDone(this.helpers, baseUrl, jsonHeaders, created.id, timeoutSeconds, this.getNode(), i);
318
+ const outputType = doc.output_type ?? 'pdf';
319
+ const filename = doc.filename ?? `document.${outputType}`;
320
+ const mimeType = MIME_TYPES[outputType] ?? 'application/octet-stream';
321
+ const buffer = await this.helpers.request({
322
+ method: 'GET',
323
+ url: `${baseUrl}/documents/${doc.id}/download`,
324
+ headers: authHeaders,
325
+ encoding: null,
326
+ resolveWithFullResponse: false,
327
+ });
328
+ const binaryData = await this.helpers.prepareBinaryData(buffer, filename, mimeType);
329
+ results.push({
330
+ json: {
331
+ id: doc.id,
332
+ filename,
333
+ output_type: outputType,
334
+ download_url: doc.download_url,
335
+ generated_at: doc.generated_at,
336
+ },
337
+ binary: { [binaryPropertyName]: binaryData },
338
+ });
339
+ }
273
340
  // ── Get Document ───────────────────────────────────────────────────
274
341
  if (operation === 'getDocument') {
275
342
  const documentId = this.getNodeParameter('documentId', i);
276
- const response = await this.helpers.request({
277
- method: 'GET',
278
- url: `${baseUrl}/document_cards/${documentId}`,
279
- headers,
280
- json: true,
281
- });
282
- results.push({ json: response });
343
+ const doc = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_cards/${documentId}`, jsonHeaders);
344
+ results.push({ json: doc });
283
345
  }
284
346
  // ── Download Document ──────────────────────────────────────────────
285
347
  if (operation === 'downloadDocument') {
286
348
  const documentId = this.getNodeParameter('documentId', i);
287
349
  const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
288
- // Fetch document metadata to get filename and output type
289
- const doc = await this.helpers.request({
290
- method: 'GET',
291
- url: `${baseUrl}/document_cards/${documentId}`,
292
- headers,
293
- json: true,
294
- });
350
+ const doc = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_cards/${documentId}`, jsonHeaders);
295
351
  if (doc.status !== 'success') {
296
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document is not ready — status is "${doc.status}"`, { itemIndex: i });
352
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document is not ready — current status is "${doc.status}"`, { itemIndex: i });
297
353
  }
298
354
  const outputType = doc.output_type ?? 'pdf';
299
355
  const filename = doc.filename ?? `document.${outputType}`;
@@ -301,38 +357,43 @@ class PDFKraft {
301
357
  const buffer = await this.helpers.request({
302
358
  method: 'GET',
303
359
  url: `${baseUrl}/documents/${documentId}/download`,
304
- headers: { Authorization: `Bearer ${apiKey}` },
360
+ headers: authHeaders,
305
361
  encoding: null,
306
362
  resolveWithFullResponse: false,
307
363
  });
308
364
  const binaryData = await this.helpers.prepareBinaryData(buffer, filename, mimeType);
309
- results.push({ json: { documentId, filename, output_type: outputType }, binary: { [binaryPropertyName]: binaryData } });
365
+ results.push({
366
+ json: { documentId, filename, output_type: outputType, download_url: doc.download_url },
367
+ binary: { [binaryPropertyName]: binaryData },
368
+ });
310
369
  }
311
370
  // ── List Documents ─────────────────────────────────────────────────
312
371
  if (operation === 'listDocuments') {
372
+ const returnAll = this.getNodeParameter('returnAll', i);
313
373
  const statusFilter = this.getNodeParameter('statusFilter', i);
314
- const limit = this.getNodeParameter('limit', i);
315
- const params = new URLSearchParams({ page: '1' });
316
- if (statusFilter)
317
- params.set('status', statusFilter);
318
- const response = await this.helpers.request({
319
- method: 'GET',
320
- url: `${baseUrl}/document_cards?${params}`,
321
- headers,
322
- json: true,
323
- });
324
- const docs = (response.data ?? []).slice(0, limit);
325
- for (const doc of docs)
326
- results.push({ json: doc });
374
+ const limit = returnAll ? Infinity : this.getNodeParameter('limit', i);
375
+ let page = 1;
376
+ let collected = 0;
377
+ let hasMore = true;
378
+ while (hasMore && collected < limit) {
379
+ const params = new URLSearchParams({ page: String(page) });
380
+ if (statusFilter)
381
+ params.set('status', statusFilter);
382
+ const response = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_cards?${params}`, jsonHeaders);
383
+ const docs = response.data ?? [];
384
+ const remaining = limit - collected;
385
+ for (const doc of docs.slice(0, remaining)) {
386
+ results.push({ json: doc });
387
+ collected++;
388
+ }
389
+ const totalPages = response.meta?.total_pages ?? 1;
390
+ hasMore = page < totalPages && docs.length > 0;
391
+ page++;
392
+ }
327
393
  }
328
394
  // ── List Templates ─────────────────────────────────────────────────
329
395
  if (operation === 'listTemplates') {
330
- const response = await this.helpers.request({
331
- method: 'GET',
332
- url: `${baseUrl}/document_template_cards`,
333
- headers,
334
- json: true,
335
- });
396
+ const response = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_template_cards`, jsonHeaders);
336
397
  for (const t of response.data ?? [])
337
398
  results.push({ json: t });
338
399
  }
@@ -1,6 +1,11 @@
1
- import type { IHookFunctions, IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
1
+ import type { IHookFunctions, IWebhookFunctions, ILoadOptionsFunctions, INodePropertyOptions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
2
2
  export declare class PDFKraftTrigger implements INodeType {
3
3
  description: INodeTypeDescription;
4
+ methods: {
5
+ loadOptions: {
6
+ getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
+ };
8
+ };
4
9
  webhookMethods: {
5
10
  default: {
6
11
  checkExists(this: IHookFunctions): Promise<boolean>;
@@ -27,16 +27,69 @@ class PDFKraftTrigger {
27
27
  type: 'string',
28
28
  typeOptions: { password: true },
29
29
  default: '',
30
- description: 'The secret shown when you created the webhook endpoint in PDFKraft',
30
+ required: true,
31
+ description: 'The secret shown when you created the webhook endpoint in PDFKraft (Settings → Webhooks → Create Endpoint)',
31
32
  },
32
33
  {
33
34
  displayName: 'Verify Signature',
34
35
  name: 'verifySignature',
35
36
  type: 'boolean',
36
37
  default: true,
38
+ description: 'Whether to verify the HMAC signature on incoming requests. Recommended to leave enabled.',
39
+ },
40
+ {
41
+ displayName: 'Event Types',
42
+ name: 'eventTypes',
43
+ type: 'multiOptions',
44
+ options: [
45
+ {
46
+ name: 'Document Succeeded',
47
+ value: 'document.success',
48
+ description: 'Trigger when a document is generated successfully',
49
+ },
50
+ {
51
+ name: 'Document Failed',
52
+ value: 'document.failure',
53
+ description: 'Trigger when a document generation fails',
54
+ },
55
+ ],
56
+ default: ['document.success'],
57
+ description: 'Which event types should trigger this node. Leave both selected to receive all events.',
58
+ },
59
+ {
60
+ displayName: 'Template Filter',
61
+ name: 'templateFilter',
62
+ type: 'options',
63
+ typeOptions: { loadOptionsMethod: 'getTemplates' },
64
+ default: '*',
65
+ description: 'Only trigger for documents generated from this template. Select "All Templates" to receive events from every template.',
37
66
  },
38
67
  ],
39
68
  };
69
+ this.methods = {
70
+ loadOptions: {
71
+ async getTemplates() {
72
+ const credentials = await this.getCredentials('pdfKraftApi');
73
+ const baseUrl = credentials.baseUrl.replace(/\/$/, '');
74
+ const apiKey = credentials.apiKey;
75
+ const response = await this.helpers.request({
76
+ method: 'GET',
77
+ url: `${baseUrl}/document_template_cards`,
78
+ headers: { Authorization: `Bearer ${apiKey}` },
79
+ json: true,
80
+ });
81
+ const templates = (response.data ?? []).filter(t => t.published_at);
82
+ return [
83
+ { name: 'All Templates', value: '*' },
84
+ ...templates.map(t => ({
85
+ name: `${t.name} [${t.output_type.toUpperCase()}]`,
86
+ value: t.identifier,
87
+ description: t.identifier,
88
+ })),
89
+ ];
90
+ },
91
+ },
92
+ };
40
93
  this.webhookMethods = {
41
94
  default: {
42
95
  async checkExists() { return false; },
@@ -49,20 +102,37 @@ class PDFKraftTrigger {
49
102
  const req = this.getRequestObject();
50
103
  const verifySignature = this.getNodeParameter('verifySignature');
51
104
  const secret = this.getNodeParameter('webhookSecret');
105
+ const eventTypes = this.getNodeParameter('eventTypes');
106
+ const templateFilter = this.getNodeParameter('templateFilter');
107
+ // ── Signature verification ─────────────────────────────────────────
52
108
  if (verifySignature && secret) {
53
109
  const signature = req.headers['x-pdfkraft-signature'];
54
110
  const timestamp = req.headers['x-pdfkraft-timestamp'];
55
111
  const rawBody = JSON.stringify(req.body);
56
112
  const expected = crypto_1.default
57
113
  .createHmac('sha256', secret)
58
- .update(`${timestamp}.${rawBody}`)
114
+ .update(`${timestamp ?? ''}.${rawBody}`)
59
115
  .digest('hex');
60
- if (!crypto_1.default.timingSafeEqual(Buffer.from(signature ?? '', 'hex'), Buffer.from(expected, 'hex'))) {
61
- return { webhookResponse: { statusCode: 401 } };
116
+ const sigBuf = Buffer.from(signature ?? '', 'hex');
117
+ const expBuf = Buffer.from(expected, 'hex');
118
+ if (sigBuf.length !== expBuf.length || !crypto_1.default.timingSafeEqual(sigBuf, expBuf)) {
119
+ return { webhookResponse: { statusCode: 401, body: { error: 'invalid signature' } } };
120
+ }
121
+ }
122
+ const body = req.body;
123
+ // ── Event type filter ──────────────────────────────────────────────
124
+ if (eventTypes.length > 0 && body.event && !eventTypes.includes(body.event)) {
125
+ return { webhookResponse: { statusCode: 200, body: { ignored: true, reason: 'event_type_filtered' } } };
126
+ }
127
+ // ── Template filter ────────────────────────────────────────────────
128
+ if (templateFilter && templateFilter !== '*') {
129
+ const docIdentifier = body.document?.document_template_identifier;
130
+ if (docIdentifier !== templateFilter) {
131
+ return { webhookResponse: { statusCode: 200, body: { ignored: true, reason: 'template_filtered' } } };
62
132
  }
63
133
  }
64
134
  return {
65
- workflowData: [[{ json: req.body }]],
135
+ workflowData: [[{ json: body }]],
66
136
  };
67
137
  }
68
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-pdfkraft",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "n8n community node for PDFKraft — generate PDFs and images via the PDFKraft API",
5
5
  "keywords": ["n8n-community-node-package"],
6
6
  "main": "dist/index.js",