@ironfang/n8n-nodes-ironfang 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +164 -100
  2. package/dist/credentials/IronfangApi.credentials.js +6 -1
  3. package/dist/credentials/IronfangS3.credentials.d.ts +8 -0
  4. package/dist/credentials/IronfangS3.credentials.js +15 -0
  5. package/dist/credentials/IronfangWebhookApi.credentials.d.ts +8 -0
  6. package/dist/credentials/IronfangWebhookApi.credentials.js +14 -0
  7. package/dist/nodes/Ironfang/Ironfang.node.d.ts +7 -10
  8. package/dist/nodes/Ironfang/Ironfang.node.js +36 -535
  9. package/dist/nodes/Ironfang/IronfangTrigger.node.d.ts +11 -0
  10. package/dist/nodes/Ironfang/IronfangTrigger.node.js +56 -0
  11. package/dist/nodes/Ironfang/catalog.d.ts +2 -0
  12. package/dist/nodes/Ironfang/catalog.js +1806 -0
  13. package/dist/nodes/Ironfang/credentialTest.js +24 -14
  14. package/dist/nodes/Ironfang/destinationCredentialTest.d.ts +2 -0
  15. package/dist/nodes/Ironfang/destinationCredentialTest.js +12 -0
  16. package/dist/nodes/Ironfang/execute.d.ts +2 -0
  17. package/dist/nodes/Ironfang/execute.js +205 -0
  18. package/dist/nodes/Ironfang/files.d.ts +10 -0
  19. package/dist/nodes/Ironfang/files.js +50 -0
  20. package/dist/nodes/Ironfang/parameters.d.ts +2 -0
  21. package/dist/nodes/Ironfang/parameters.js +50 -0
  22. package/dist/nodes/Ironfang/renderOptions.d.ts +2 -0
  23. package/dist/nodes/Ironfang/renderOptions.js +8 -0
  24. package/dist/nodes/Ironfang/renderwolf.d.ts +19 -0
  25. package/dist/nodes/Ironfang/renderwolf.js +556 -0
  26. package/dist/nodes/Ironfang/templates.d.ts +2 -0
  27. package/dist/nodes/Ironfang/templates.js +15 -0
  28. package/dist/nodes/Ironfang/transport.d.ts +3 -1
  29. package/dist/nodes/Ironfang/transport.js +20 -5
  30. package/dist/nodes/Ironfang/types.d.ts +31 -0
  31. package/dist/nodes/Ironfang/types.js +2 -0
  32. package/dist/nodes/Ironfang/webhook.d.ts +4 -0
  33. package/dist/nodes/Ironfang/webhook.js +39 -0
  34. package/docs/operations.md +1046 -0
  35. package/docs/validation.md +58 -0
  36. package/examples/audit-polling.json +223 -0
  37. package/examples/invoice-generation-storage.json +83 -0
  38. package/examples/invoice-validation.json +54 -0
  39. package/package.json +16 -7
@@ -1,549 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Ironfang = void 0;
4
- const n8n_workflow_1 = require("n8n-workflow");
5
- const transport_1 = require("./transport");
6
- const credentialTest_1 = require("./credentialTest");
7
- /**
8
- * Ironfang node. One node per vendor: pick a product with Resource, then an
9
- * operation within it. Renderwolf (https://ironfang.uk/renderwolf/docs) is the
10
- * first product; later ones are added as further resources rather than as
11
- * separate nodes.
12
- *
13
- * Render operations return binary data; sign and usage return JSON.
14
- */
4
+ const renderwolf_1 = require("./renderwolf");
5
+ const catalog_1 = require("./catalog");
6
+ const parameters_1 = require("./parameters");
7
+ const execute_1 = require("./execute");
8
+ const renderOptions_1 = require("./renderOptions");
9
+ const templates_1 = require("./templates");
10
+ const destinationCredentialTest_1 = require("./destinationCredentialTest");
11
+ const legacy = renderwolf_1.renderwolf;
12
+ const legacyOperations = (legacy.description.properties[1].options ?? []);
13
+ const products = ['auditwolf', 'financewolf', 'renderwolf', 'tools'];
14
+ // Resource-specific defaults are selected from the checked-in catalogue.
15
+ // eslint-disable-next-line n8n-nodes-base/node-param-default-missing
16
+ const operationParameters = products.map(product => ({
17
+ displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true,
18
+ default: product === 'renderwolf' ? 'screenshot' : catalog_1.operations.find(op => op.product === product).id,
19
+ displayOptions: { show: { resource: [product] } },
20
+ options: [...(product === 'renderwolf' ? legacyOperations : []), ...catalog_1.operations.filter(op => op.product === product).map(op => ({ name: op.name, value: op.id, description: op.description, action: op.description }))].sort((a, b) => a.name.localeCompare(b.name)),
21
+ }));
15
22
  class Ironfang {
16
23
  description = {
17
- displayName: 'Ironfang',
18
- name: 'ironfang',
19
- icon: { light: 'file:ironfang.svg', dark: 'file:ironfang.dark.svg' },
24
+ ...legacy.description,
20
25
  usableAsTool: true,
21
- group: ['transform'],
22
- version: [1, 1.1],
26
+ icon: { light: 'file:ironfang.svg', dark: 'file:ironfang.dark.svg' },
23
27
  subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
24
- description: 'Rendering and developer APIs from Ironfang',
25
- defaults: { name: 'Ironfang' },
26
- inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
27
- outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
28
- credentials: [{ name: 'ironfangApi', required: true, testedBy: 'ironfangApiTest' }],
28
+ description: 'Renderwolf, Financewolf, Auditwolf and public developer tools',
29
+ credentials: [
30
+ { name: 'ironfangS3', required: true, testedBy: 'ironfangS3Test', displayOptions: { show: { operation: ['createDestination', 'createExportDestinations'] }, hide: { destinationType: ['webhook'] } } },
31
+ { name: 'ironfangApi', required: true, testedBy: 'ironfangApiTest', displayOptions: { show: { resource: ['renderwolf', 'auditwolf', 'financewolf'] }, hide: { authentication: ['public'] } } },
32
+ ],
29
33
  properties: [
30
- {
31
- displayName: 'Resource',
32
- name: 'resource',
33
- type: 'options',
34
- noDataExpression: true,
35
- default: 'renderwolf',
36
- options: [
37
- {
38
- name: 'Renderwolf',
39
- value: 'renderwolf',
40
- description: 'Screenshots, PDFs and templated images',
41
- },
42
- ],
43
- },
44
- {
45
- displayName: 'Operation',
46
- name: 'operation',
47
- type: 'options',
48
- noDataExpression: true,
49
- default: 'screenshot',
50
- displayOptions: { show: { resource: ['renderwolf'] } },
51
- options: [
52
- {
53
- name: 'PDF',
54
- value: 'pdf',
55
- description: 'Print a URL or raw HTML to PDF',
56
- action: 'Render a PDF',
57
- },
58
- {
59
- name: 'Screenshot',
60
- value: 'screenshot',
61
- description: 'Capture a URL or raw HTML as an image',
62
- action: 'Take a screenshot',
63
- },
64
- {
65
- name: 'Signed URL',
66
- value: 'sign',
67
- description: 'Mint a stable render URL for <img> tags',
68
- action: 'Create a signed URL',
69
- },
70
- {
71
- name: 'Template Image',
72
- value: 'image',
73
- description: 'Render a stored template with variables (OG images)',
74
- action: 'Render a template image',
75
- },
76
- {
77
- name: 'Usage',
78
- value: 'usage',
79
- description: 'Current period usage against your plan cap',
80
- action: 'Get usage',
81
- }, {
82
- name: 'Video Clip',
83
- value: 'video',
84
- description: 'Render a short captioned MP4 from a background and caption cards',
85
- action: 'Create a video clip',
86
- },
87
- ],
88
- },
89
- /* ------------------------- source (screenshot/pdf) ------------------ */
90
- {
91
- displayName: 'Source',
92
- name: 'source',
93
- type: 'options',
94
- default: 'url',
95
- displayOptions: { show: { operation: ['screenshot', 'pdf'] } },
96
- options: [
97
- { name: 'URL', value: 'url' },
98
- { name: 'HTML', value: 'html' },
99
- ],
100
- },
101
- {
102
- displayName: 'URL',
103
- name: 'url',
104
- type: 'string',
105
- default: '',
106
- required: true,
107
- placeholder: 'https://example.com',
108
- displayOptions: { show: { operation: ['screenshot', 'pdf'], source: ['url'] } },
109
- },
110
- {
111
- displayName: 'HTML',
112
- name: 'html',
113
- type: 'string',
114
- typeOptions: { rows: 6 },
115
- default: '',
116
- required: true,
117
- displayOptions: { show: { operation: ['screenshot', 'pdf'], source: ['html'] } },
118
- },
119
- /* ------------------------- screenshot options ----------------------- */
120
- {
121
- displayName: 'Options',
122
- name: 'screenshotOptions',
123
- type: 'collection',
124
- placeholder: 'Add option',
125
- default: {},
126
- displayOptions: { show: { operation: ['screenshot'] } },
127
- options: [
128
- { displayName: 'Dark Mode', name: 'dark_mode', type: 'boolean', default: false },
129
- {
130
- displayName: 'Delay (Ms)',
131
- name: 'delay_ms',
132
- type: 'number',
133
- default: 0,
134
- description: 'Extra settle time after load for late-painting pages',
135
- },
136
- {
137
- displayName: 'Device',
138
- name: 'device',
139
- type: 'options',
140
- default: 'desktop',
141
- description: 'Applies a viewport, pixel density, mobile flag and user agent together. Width and height still win if you set them.',
142
- options: [
143
- { name: 'Desktop', value: 'desktop' },
144
- { name: 'Mobile', value: 'mobile' },
145
- { name: 'Tablet', value: 'tablet' },
146
- ],
147
- },
148
- {
149
- displayName: 'Format',
150
- name: 'format',
151
- type: 'options',
152
- default: 'png',
153
- options: [
154
- { name: 'JPEG', value: 'jpeg' },
155
- { name: 'PNG', value: 'png' },
156
- { name: 'WebP', value: 'webp' },
157
- ],
158
- },
159
- { displayName: 'Full Page', name: 'full_page', type: 'boolean', default: false },
160
- { displayName: 'Height', name: 'height', type: 'number', default: 800 },
161
- {
162
- displayName: 'No Cache',
163
- name: 'no_cache',
164
- type: 'boolean',
165
- default: false,
166
- description: 'Whether to force a live capture instead of reusing an identical recent render. Use when the page must be captured exactly as it is right now, such as evidence or change detection. Counts against your quota.',
167
- },
168
- {
169
- displayName: 'Quality',
170
- name: 'quality',
171
- type: 'number',
172
- default: 85,
173
- description: 'For JPEG and WebP. Ignored for PNG, which is lossless.',
174
- typeOptions: { minValue: 1, maxValue: 100 },
175
- },
176
- {
177
- displayName: 'Selector',
178
- name: 'selector',
179
- type: 'string',
180
- default: '',
181
- description: 'CSS selector - capture just that element',
182
- },
183
- { displayName: 'Width', name: 'width', type: 'number', default: 1280 },
184
- ],
185
- },
186
- /* ------------------------- pdf options ------------------------------ */
187
- {
188
- displayName: 'Options',
189
- name: 'pdfOptions',
190
- type: 'collection',
191
- placeholder: 'Add option',
192
- default: {},
193
- displayOptions: { show: { operation: ['pdf'] } },
194
- options: [
195
- { displayName: 'Footer HTML', name: 'footer_html', type: 'string', default: '' },
196
- { displayName: 'Header HTML', name: 'header_html', type: 'string', default: '' },
197
- { displayName: 'Landscape', name: 'landscape', type: 'boolean', default: false },
198
- {
199
- displayName: 'No Cache',
200
- name: 'no_cache',
201
- type: 'boolean',
202
- default: false,
203
- description: 'Whether to force a live capture instead of reusing an identical recent render. Use when the page must be captured exactly as it is right now, such as evidence or change detection. Counts against your quota.',
204
- },
205
- {
206
- displayName: 'Print Background',
207
- name: 'print_background',
208
- type: 'boolean',
209
- default: true,
210
- },
211
- {
212
- displayName: 'Scale',
213
- name: 'scale',
214
- type: 'number',
215
- default: 1,
216
- typeOptions: { minValue: 0.1, maxValue: 2, numberPrecision: 2 },
217
- },
218
- ],
219
- },
220
- /* ------------------------- template image --------------------------- */
221
- {
222
- displayName: 'Template ID',
223
- name: 'templateId',
224
- type: 'string',
225
- default: '',
226
- required: true,
227
- displayOptions: { show: { operation: ['image'] } },
228
- },
229
- {
230
- displayName: 'Variables',
231
- name: 'vars',
232
- type: 'fixedCollection',
233
- typeOptions: { multipleValues: true },
234
- default: {},
235
- placeholder: 'Add variable',
236
- displayOptions: { show: { operation: ['image'] } },
237
- options: [
238
- {
239
- name: 'values',
240
- displayName: 'Variable',
241
- values: [
242
- { displayName: 'Name', name: 'name', type: 'string', default: '' },
243
- { displayName: 'Value', name: 'value', type: 'string', default: '' },
244
- ],
245
- },
246
- ],
247
- },
248
- {
249
- displayName: 'Format',
250
- name: 'imageFormat',
251
- type: 'options',
252
- default: 'png',
253
- displayOptions: { show: { operation: ['image'] } },
254
- options: [
255
- { name: 'JPEG', value: 'jpeg' },
256
- { name: 'PNG', value: 'png' },
257
- { name: 'WebP', value: 'webp' },
258
- ],
259
- },
260
- /* ------------------------- signed url ------------------------------- */
261
- {
262
- displayName: 'Kind',
263
- name: 'signKind',
264
- type: 'options',
265
- default: 'screenshot',
266
- displayOptions: { show: { operation: ['sign'] } },
267
- options: [
268
- { name: 'Screenshot', value: 'screenshot' },
269
- { name: 'Template Image', value: 'image' },
270
- ],
271
- },
272
- {
273
- displayName: 'URL',
274
- name: 'signUrl',
275
- type: 'string',
276
- default: '',
277
- required: true,
278
- displayOptions: { show: { operation: ['sign'], signKind: ['screenshot'] } },
279
- },
280
- {
281
- displayName: 'Full Page',
282
- name: 'signFullPage',
283
- type: 'boolean',
284
- default: false,
285
- displayOptions: { show: { operation: ['sign'], signKind: ['screenshot'] } },
286
- },
287
- {
288
- displayName: 'Template ID',
289
- name: 'signTemplateId',
290
- type: 'string',
291
- default: '',
292
- required: true,
293
- displayOptions: { show: { operation: ['sign'], signKind: ['image'] } },
294
- },
295
- {
296
- displayName: 'Variables',
297
- name: 'signVars',
298
- type: 'fixedCollection',
299
- typeOptions: { multipleValues: true },
300
- default: {},
301
- placeholder: 'Add variable',
302
- displayOptions: { show: { operation: ['sign'], signKind: ['image'] } },
303
- options: [
304
- {
305
- name: 'values',
306
- displayName: 'Variable',
307
- values: [
308
- { displayName: 'Name', name: 'name', type: 'string', default: '' },
309
- { displayName: 'Value', name: 'value', type: 'string', default: '' },
310
- ],
311
- },
312
- ],
313
- },
314
- {
315
- displayName: 'TTL (Hours)',
316
- name: 'ttlHours',
317
- type: 'number',
318
- default: 0,
319
- description: '0 means the URL never expires',
320
- displayOptions: { show: { operation: ['sign'] } },
321
- },
322
- /* ------------------------- video clip ------------------------------- */
323
- {
324
- displayName: 'Size',
325
- name: 'clipSize',
326
- type: 'options',
327
- default: 'vertical',
328
- displayOptions: { show: { operation: ['video'] } },
329
- description: 'Credits scale with pixels and seconds, so the smaller sizes cost less per second',
330
- options: [
331
- { name: '720p 1280x720', value: '720p' },
332
- { name: 'Landscape 1920x1080', value: 'landscape' },
333
- { name: 'Square 1080x1080', value: 'square' },
334
- { name: 'Vertical 1080x1920', value: 'vertical' },
335
- ],
336
- },
337
- {
338
- displayName: 'Duration (Seconds)',
339
- name: 'clipDuration',
340
- type: 'number',
341
- default: 15,
342
- typeOptions: { minValue: 1, maxValue: 60 },
343
- displayOptions: { show: { operation: ['video'] } },
344
- },
345
- {
346
- displayName: 'Captions',
347
- name: 'captions',
348
- type: 'fixedCollection',
349
- typeOptions: { multipleValues: true },
350
- default: {},
351
- placeholder: 'Add caption',
352
- description: 'Cards that appear and disappear on a schedule',
353
- displayOptions: { show: { operation: ['video'] } },
354
- options: [
355
- {
356
- name: 'values',
357
- displayName: 'Caption',
358
- values: [
359
- { displayName: 'Text', name: 'text', type: 'string', default: '' },
360
- {
361
- displayName: 'From (Seconds)',
362
- name: 'from',
363
- type: 'number',
364
- default: 0,
365
- },
366
- { displayName: 'To (Seconds)', name: 'to', type: 'number', default: 3 },
367
- ],
368
- },
369
- ],
370
- },
371
- {
372
- displayName: 'Options',
373
- name: 'clipOptions',
374
- type: 'collection',
375
- placeholder: 'Add option',
376
- default: {},
377
- displayOptions: { show: { operation: ['video'] } },
378
- options: [
379
- {
380
- displayName: 'Audio URL',
381
- name: 'audio',
382
- type: 'string',
383
- default: '',
384
- description: 'An audio track, trimmed to the clip length',
385
- },
386
- {
387
- displayName: 'Background URL',
388
- name: 'background',
389
- type: 'string',
390
- default: '',
391
- description: 'An image or video to fill the frame. Leave blank for a solid colour.',
392
- },
393
- {
394
- displayName: 'Colour',
395
- name: 'colour',
396
- type: 'color',
397
- default: '',
398
- description: 'Used when there is no background URL',
399
- },
400
- {
401
- displayName: 'Font Size',
402
- name: 'font_size',
403
- type: 'number',
404
- default: 0,
405
- description: 'Caption size. 0 scales it with the canvas.',
406
- },
407
- {
408
- displayName: 'Watermark URL',
409
- name: 'watermark',
410
- type: 'string',
411
- default: '',
412
- description: 'A PNG of your own, placed bottom right',
413
- },
414
- ],
415
- },
416
- /* ------------------------- binary output ---------------------------- */
417
- {
418
- displayName: 'Put Output in Field',
419
- name: 'binaryProperty',
420
- type: 'string',
421
- default: 'data',
422
- description: 'Name of the binary field to write the rendered file to',
423
- displayOptions: { show: { operation: ['screenshot', 'pdf', 'image', 'video'] } },
424
- },
34
+ { displayName: 'Resource', name: 'resource', type: 'options', noDataExpression: true, default: 'renderwolf', options: [{ name: 'Auditwolf', value: 'auditwolf' }, { name: 'Financewolf', value: 'financewolf' }, { name: 'Renderwolf', value: 'renderwolf' }, { name: 'Public Tool', value: 'tools' }] },
35
+ ...operationParameters,
36
+ ...legacy.description.properties.slice(2).map(field => ({ ...field, displayOptions: { ...field.displayOptions, show: { ...field.displayOptions?.show, resource: ['renderwolf'] } } })),
37
+ ...renderOptions_1.renderOptions,
38
+ ...parameters_1.productParameters,
425
39
  ],
426
40
  };
427
- methods = { credentialTest: { ironfangApiTest: credentialTest_1.credentialTest } };
41
+ methods = { ...legacy.methods, credentialTest: { ...legacy.methods.credentialTest, ironfangS3Test: destinationCredentialTest_1.destinationCredentialTest }, listSearch: { searchTemplates: templates_1.searchTemplates } };
428
42
  async execute() {
429
- const items = this.getInputData();
430
- const out = [];
431
- const collectVars = (raw) => {
432
- const vars = {};
433
- for (const v of raw.values ?? []) {
434
- vars[String(v.name)] = String(v.value);
435
- }
436
- return vars;
437
- };
438
- for (let i = 0; i < items.length; i++) {
439
- const operation = this.getNodeParameter('operation', i);
440
- const credentials = await this.getCredentials('ironfangApi');
441
- const baseUrl = (0, transport_1.apiBase)(credentials.baseUrl);
442
- try {
443
- if (operation === 'screenshot' || operation === 'pdf') {
444
- const source = this.getNodeParameter('source', i);
445
- const body = operation === 'screenshot'
446
- ? { ...this.getNodeParameter('screenshotOptions', i) }
447
- : { ...this.getNodeParameter('pdfOptions', i) };
448
- if (source === 'url') {
449
- body.url = this.getNodeParameter('url', i);
450
- }
451
- else {
452
- body.html = this.getNodeParameter('html', i);
453
- }
454
- const data = await transport_1.request.call(this, {
455
- method: 'POST',
456
- url: `${baseUrl}/v1/${operation}`,
457
- body,
458
- encoding: 'arraybuffer',
459
- });
460
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
461
- const isPdf = operation === 'pdf';
462
- const format = isPdf ? 'pdf' : (body.format || 'png');
463
- const binary = await this.helpers.prepareBinaryData(data.body, `render.${format}`, isPdf ? 'application/pdf' : `image/${format}`);
464
- out.push((0, transport_1.binaryOutput)(this, i, binaryProperty, binary, data, { operation }));
465
- }
466
- else if (operation === 'image') {
467
- const templateId = (0, transport_1.identifier)(this, this.getNodeParameter('templateId', i), i);
468
- const format = this.getNodeParameter('imageFormat', i);
469
- const vars = collectVars(this.getNodeParameter('vars', i));
470
- const data = await transport_1.request.call(this, {
471
- method: 'POST',
472
- url: `${baseUrl}/v1/image/${templateId}`,
473
- body: { vars, format },
474
- encoding: 'arraybuffer',
475
- });
476
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
477
- const binary = await this.helpers.prepareBinaryData(data.body, `render.${format}`, `image/${format}`);
478
- out.push((0, transport_1.binaryOutput)(this, i, binaryProperty, binary, data, { operation, template: templateId }));
479
- }
480
- else if (operation === 'video') {
481
- const captions = this.getNodeParameter('captions', i)
482
- .values?.map((c) => ({
483
- text: String(c.text),
484
- from: Number(c.from),
485
- to: Number(c.to),
486
- }));
487
- const body = {
488
- ...this.getNodeParameter('clipOptions', i),
489
- size: this.getNodeParameter('clipSize', i),
490
- duration: this.getNodeParameter('clipDuration', i),
491
- };
492
- if (captions?.length) {
493
- body.captions = captions;
494
- }
495
- // A blank font size means "scale it with the canvas", which
496
- // is what the API does when the field is absent - sending 0
497
- // would be a request for a zero-point font.
498
- if (!body.font_size) {
499
- delete body.font_size;
500
- }
501
- const data = await transport_1.request.call(this, {
502
- method: 'POST',
503
- url: `${baseUrl}/v1/video`,
504
- body,
505
- encoding: 'arraybuffer',
506
- });
507
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
508
- const binary = await this.helpers.prepareBinaryData(data.body, 'clip.mp4', 'video/mp4');
509
- out.push((0, transport_1.binaryOutput)(this, i, binaryProperty, binary, data, { operation }));
510
- }
511
- else if (operation === 'sign') {
512
- const kind = this.getNodeParameter('signKind', i);
513
- const body = {
514
- kind,
515
- ttl_hours: this.getNodeParameter('ttlHours', i),
516
- };
517
- if (kind === 'screenshot') {
518
- body.url = this.getNodeParameter('signUrl', i);
519
- body.full_page = this.getNodeParameter('signFullPage', i);
520
- }
521
- else {
522
- body.template = (0, transport_1.identifier)(this, this.getNodeParameter('signTemplateId', i), i);
523
- body.vars = collectVars(this.getNodeParameter('signVars', i));
524
- }
525
- const resp = await transport_1.request.call(this, { method: 'POST', url: `${baseUrl}/v1/sign`, body, json: true });
526
- out.push({ json: { ...resp.body, _ironfang: resp.metadata }, pairedItem: { item: i } });
527
- }
528
- else if (operation === 'usage') {
529
- const resp = await transport_1.request.call(this, { method: 'GET', url: `${baseUrl}/v1/usage`, json: true });
530
- out.push({ json: { ...resp.body, _ironfang: resp.metadata }, pairedItem: { item: i } });
531
- }
532
- else {
533
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation ${operation}`, {
534
- itemIndex: i,
535
- });
536
- }
537
- }
538
- catch (error) {
539
- if (this.continueOnFail()) {
540
- out.push((0, transport_1.errorOutput)(this, error, i));
541
- continue;
542
- }
543
- throw (0, transport_1.apiError)(this, error, i);
544
- }
545
- }
546
- return [out];
43
+ const resource = this.getNodeParameter('resource', 0);
44
+ const operation = this.getNodeParameter('operation', 0);
45
+ if (resource === 'renderwolf' && legacyOperations.some(op => op.value === operation))
46
+ return legacy.execute.call(this);
47
+ return execute_1.executeProducts.call(this);
547
48
  }
548
49
  }
549
50
  exports.Ironfang = Ironfang;
@@ -0,0 +1,11 @@
1
+ import type { INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
2
+ import { webhookCredentialTest } from './webhook';
3
+ export declare class IronfangTrigger implements INodeType {
4
+ description: INodeTypeDescription;
5
+ methods: {
6
+ credentialTest: {
7
+ ironfangWebhookTest: typeof webhookCredentialTest;
8
+ };
9
+ };
10
+ webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
11
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IronfangTrigger = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const webhook_1 = require("./webhook");
6
+ // Endpoints are registered explicitly and remain customer-owned; n8n owns its local route.
7
+ // eslint-disable-next-line @n8n/community-nodes/webhook-lifecycle-complete
8
+ class IronfangTrigger {
9
+ description = {
10
+ displayName: 'Ironfang Trigger', name: 'ironfangTrigger', group: ['trigger'], version: 1,
11
+ icon: { light: 'file:ironfang.svg', dark: 'file:ironfang.dark.svg' },
12
+ subtitle: 'Signed product events', description: 'Receive signed Auditwolf and Renderwolf events', defaults: { name: 'Ironfang Trigger' },
13
+ inputs: [], outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
14
+ credentials: [{ name: 'ironfangWebhookApi', required: true, testedBy: 'ironfangWebhookTest' }],
15
+ webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'events' }],
16
+ properties: [
17
+ { displayName: 'Register this node’s production webhook URL using Create Webhooks (Auditwolf) or Create Destination (Renderwolf), save the returned signing secret in its credential, then activate the workflow and test the endpoint. Signatures and timestamps are checked before execution.', name: 'setupNotice', type: 'notice', default: '' },
18
+ ],
19
+ };
20
+ methods = { credentialTest: { ironfangWebhookTest: webhook_1.webhookCredentialTest } };
21
+ async webhook() {
22
+ const req = this.getRequestObject();
23
+ const res = this.getResponseObject();
24
+ const credentials = await this.getCredentials('ironfangWebhookApi');
25
+ const now = Date.now();
26
+ let event;
27
+ try {
28
+ if (req.headers['content-encoding'] && req.headers['content-encoding'] !== 'identity')
29
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Compressed webhooks are not accepted');
30
+ if (!req.rawBody && req.readRawBody)
31
+ await req.readRawBody();
32
+ if (!Buffer.isBuffer(req.rawBody))
33
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Raw webhook body is unavailable');
34
+ event = (0, webhook_1.verifyEvent)(req.rawBody, req.headers, String(credentials.product), String(credentials.signingSecret), now);
35
+ }
36
+ catch {
37
+ res.status(401).json({ error: 'Invalid signed webhook' });
38
+ return { noWebhookResponse: true };
39
+ }
40
+ // Only the signed body ID is trusted. Header IDs are not part of the MAC.
41
+ const id = String(event.id);
42
+ const data = this.getWorkflowStaticData('node');
43
+ const seen = Object.assign(Object.create(null), data.events ?? {});
44
+ for (const [key, received] of Object.entries(seen))
45
+ if (received < now - 7 * 86_400_000)
46
+ delete seen[key];
47
+ if (Object.hasOwn(seen, id))
48
+ return { webhookResponse: { accepted: true, duplicate: true } };
49
+ while (Object.keys(seen).length >= 10_000)
50
+ delete seen[Object.keys(seen).sort((a, b) => seen[a] - seen[b])[0]];
51
+ seen[id] = now;
52
+ data.events = seen;
53
+ return { webhookResponse: { accepted: true }, workflowData: [[{ json: { ...event, _ironfang: { product: String(credentials.product), verified: true, eventId: id } } }]] };
54
+ }
55
+ }
56
+ exports.IronfangTrigger = IronfangTrigger;
@@ -0,0 +1,2 @@
1
+ import type { Operation } from './types';
2
+ export declare const operations: Operation[];