n8n-nodes-supermachine 0.1.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 (30) hide show
  1. package/credentials/SupermachineApi.credentials.ts +41 -0
  2. package/dist/credentials/SupermachineApi.credentials.js +38 -0
  3. package/dist/credentials/SupermachineApi.credentials.js.map +1 -0
  4. package/dist/nodes/Supermachine/Supermachine.node.js +428 -0
  5. package/dist/nodes/Supermachine/Supermachine.node.js.map +1 -0
  6. package/dist/nodes/Supermachine/SupermachineTrigger.node.js +157 -0
  7. package/dist/nodes/Supermachine/SupermachineTrigger.node.js.map +1 -0
  8. package/dist/nodes/Supermachine/descriptions/AccountDescription.js +73 -0
  9. package/dist/nodes/Supermachine/descriptions/AccountDescription.js.map +1 -0
  10. package/dist/nodes/Supermachine/descriptions/ImageDescription.js +355 -0
  11. package/dist/nodes/Supermachine/descriptions/ImageDescription.js.map +1 -0
  12. package/dist/nodes/Supermachine/descriptions/ToolsDescription.js +241 -0
  13. package/dist/nodes/Supermachine/descriptions/ToolsDescription.js.map +1 -0
  14. package/dist/nodes/Supermachine/descriptions/index.js +21 -0
  15. package/dist/nodes/Supermachine/descriptions/index.js.map +1 -0
  16. package/dist/nodes/Supermachine/index.js +5 -0
  17. package/dist/nodes/Supermachine/index.js.map +1 -0
  18. package/dist/nodes/Supermachine/supermachine.svg +132 -0
  19. package/gulpfile.js +58 -0
  20. package/index.ts +5 -0
  21. package/nodes/Supermachine/Supermachine.node.ts +508 -0
  22. package/nodes/Supermachine/SupermachineTrigger.node.ts +175 -0
  23. package/nodes/Supermachine/descriptions/AccountDescription.ts +72 -0
  24. package/nodes/Supermachine/descriptions/ImageDescription.ts +355 -0
  25. package/nodes/Supermachine/descriptions/ToolsDescription.ts +241 -0
  26. package/nodes/Supermachine/descriptions/index.ts +3 -0
  27. package/nodes/Supermachine/index.ts +3 -0
  28. package/nodes/Supermachine/supermachine.svg +132 -0
  29. package/package.json +47 -0
  30. package/tsconfig.json +15 -0
@@ -0,0 +1,508 @@
1
+ import type {
2
+ IExecuteFunctions,
3
+ INodeExecutionData,
4
+ INodeType,
5
+ INodeTypeDescription,
6
+ IHttpRequestMethods,
7
+ IDataObject,
8
+ ILoadOptionsFunctions,
9
+ INodePropertyOptions,
10
+ } from 'n8n-workflow';
11
+
12
+ import {
13
+ getAccountFields,
14
+ getAccountOperations,
15
+ getImageFields,
16
+ getImageOperations,
17
+ getToolsFields,
18
+ getToolsOperations,
19
+ } from './descriptions';
20
+
21
+ export class Supermachine implements INodeType {
22
+ description: INodeTypeDescription = {
23
+ displayName: 'Supermachine',
24
+ name: 'supermachine',
25
+ icon: 'file:supermachine.svg',
26
+ group: ['transform'],
27
+ version: 1,
28
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
29
+ description: 'Interact with Supermachine AI Image API',
30
+ defaults: { name: 'Supermachine' },
31
+ inputs: ['main'],
32
+ outputs: ['main'],
33
+ credentials: [{ name: 'supermachineApi', required: true }],
34
+ properties: [
35
+ {
36
+ displayName: 'Resource',
37
+ name: 'resource',
38
+ type: 'options',
39
+ noDataExpression: true,
40
+ options: [
41
+ { name: 'Account', value: 'account' },
42
+ { name: 'Image', value: 'image' },
43
+ { name: 'Tools', value: 'tools' },
44
+ ],
45
+ default: 'image',
46
+ },
47
+ ...getAccountOperations,
48
+ ...getAccountFields,
49
+ ...getImageOperations,
50
+ ...getImageFields,
51
+ ...getToolsOperations,
52
+ ...getToolsFields,
53
+ ],
54
+ };
55
+
56
+ methods = {
57
+ loadOptions: {
58
+ // ═══════════════════════════════════════════════════════════
59
+ // Load danh sách Models
60
+ // ═══════════════════════════════════════════════════════════
61
+ async getModels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
62
+ const credentials = await this.getCredentials('supermachineApi');
63
+ const apiKey = credentials.apiKey as string;
64
+
65
+ try {
66
+ const response = await this.helpers.httpRequest({
67
+ method: 'GET',
68
+ url: 'https://dev.supermachine.art/v1/models',
69
+ headers: {
70
+ 'Authorization': `Bearer ${apiKey}`,
71
+ 'Content-Type': 'application/json',
72
+ },
73
+ json: true,
74
+ });
75
+
76
+ const items = (response as IDataObject).items as IDataObject[];
77
+
78
+ if (!Array.isArray(items) || items.length === 0) {
79
+ return [{ name: 'No models available', value: '' }];
80
+ }
81
+
82
+ return items.map((model: IDataObject) => ({
83
+ name: model.title as string,
84
+ value: model.title as string,
85
+ description: `${model.slug} (ID: ${model.id})`,
86
+ }));
87
+
88
+ } catch (error) {
89
+ console.error('Supermachine getModels error:', error);
90
+ return [{ name: `⚠️ Error: ${(error as Error).message}`, value: '' }];
91
+ }
92
+ },
93
+
94
+ // ═══════════════════════════════════════════════════════════
95
+ // Load danh sách LoRAs theo modelId được chọn
96
+ // ═══════════════════════════════════════════════════════════
97
+ async getLoras(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
98
+ const credentials = await this.getCredentials('supermachineApi');
99
+ const apiKey = credentials.apiKey as string;
100
+
101
+ const modelTitle = this.getNodeParameter('model') as string;
102
+
103
+ if (!modelTitle) {
104
+ return [{
105
+ name: '⚠️ Please select a model first',
106
+ value: '',
107
+ description: 'LoRAs depend on the selected model'
108
+ }];
109
+ }
110
+
111
+ try {
112
+ const modelsResponse = await this.helpers.httpRequest({
113
+ method: 'GET',
114
+ url: 'https://dev.supermachine.art/v1/models',
115
+ headers: {
116
+ 'Authorization': `Bearer ${apiKey}`,
117
+ 'Content-Type': 'application/json',
118
+ },
119
+ json: true,
120
+ });
121
+
122
+ const models = (modelsResponse as IDataObject).items as IDataObject[];
123
+ const selectedModel = models.find((m: IDataObject) => m.title === modelTitle);
124
+
125
+ if (!selectedModel) {
126
+ return [{
127
+ name: `⚠️ Model "${modelTitle}" not found`,
128
+ value: '',
129
+ description: 'Try refreshing the page'
130
+ }];
131
+ }
132
+
133
+ const modelId = selectedModel.id;
134
+
135
+ const lorasResponse = await this.helpers.httpRequest({
136
+ method: 'GET',
137
+ url: `https://dev.supermachine.art/v1/loras?modelId=${modelId}`,
138
+ headers: {
139
+ 'Authorization': `Bearer ${apiKey}`,
140
+ 'Content-Type': 'application/json',
141
+ },
142
+ json: true,
143
+ });
144
+
145
+ const items = (lorasResponse as IDataObject).items as IDataObject[];
146
+
147
+ if (!Array.isArray(items) || items.length === 0) {
148
+ return [{
149
+ name: 'No LoRAs available for this model',
150
+ value: '',
151
+ description: `Model "${modelTitle}" doesn't support LoRAs`
152
+ }];
153
+ }
154
+
155
+ return items.map((lora: IDataObject) => {
156
+ const triggerWords = lora.triggerWords as string[] || [];
157
+ const triggerHint = triggerWords.length > 0
158
+ ? ` | Trigger: ${triggerWords.join(', ')}`
159
+ : '';
160
+
161
+ return {
162
+ name: lora.name as string,
163
+ value: String(lora.id),
164
+ description: `${lora.slug}${triggerHint}`,
165
+ };
166
+ });
167
+
168
+ } catch (error) {
169
+ console.error('Supermachine getLoras error:', error);
170
+ return [{
171
+ name: `⚠️ Error loading LoRAs: ${(error as Error).message}`,
172
+ value: '',
173
+ description: 'Check your API key and network connection'
174
+ }];
175
+ }
176
+ },
177
+
178
+ // ═══════════════════════════════════════════════════════════
179
+ // Load danh sách Character Categories
180
+ // ═══════════════════════════════════════════════════════════
181
+ async getCharacterCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
182
+ const credentials = await this.getCredentials('supermachineApi');
183
+ const apiKey = credentials.apiKey as string;
184
+
185
+ try {
186
+ const response = await this.helpers.httpRequest({
187
+ method: 'GET',
188
+ url: 'https://dev.supermachine.art/v1/characters/categories',
189
+ headers: {
190
+ 'Authorization': `Bearer ${apiKey}`,
191
+ 'Content-Type': 'application/json',
192
+ },
193
+ json: true,
194
+ });
195
+
196
+ const items = (response as IDataObject).items as IDataObject[];
197
+
198
+ if (!Array.isArray(items) || items.length === 0) {
199
+ return [{
200
+ name: 'No categories available',
201
+ value: '',
202
+ description: 'Character categories may not be enabled'
203
+ }];
204
+ }
205
+
206
+ return items.map((category: IDataObject) => ({
207
+ name: category.name as string,
208
+ value: String(category.id),
209
+ description: `Category ID: ${category.id}`,
210
+ }));
211
+
212
+ } catch (error) {
213
+ console.error('Supermachine getCharacterCategories error:', error);
214
+ return [{
215
+ name: `⚠️ Error: ${(error as Error).message}`,
216
+ value: '',
217
+ description: 'Check your API key and permissions'
218
+ }];
219
+ }
220
+ },
221
+
222
+ // ═══════════════════════════════════════════════════════════
223
+ // Load danh sách Characters (phụ thuộc category + model)
224
+ // ═══════════════════════════════════════════════════════════
225
+ async getCharacters(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
226
+ const credentials = await this.getCredentials('supermachineApi');
227
+ const apiKey = credentials.apiKey as string;
228
+
229
+ const additionalFields = this.getCurrentNodeParameter('additionalFields') as IDataObject;
230
+ const categoryId = additionalFields?.characterCategory as string;
231
+ const modelTitle = this.getNodeParameter('model') as string;
232
+
233
+ if (!categoryId) {
234
+ return [{
235
+ name: '⚠️ Please select a Character Category first',
236
+ value: '',
237
+ description: 'Characters depend on the selected category'
238
+ }];
239
+ }
240
+
241
+ if (!modelTitle) {
242
+ return [{
243
+ name: '⚠️ Please select a Model first',
244
+ value: '',
245
+ description: 'Characters depend on the selected model'
246
+ }];
247
+ }
248
+
249
+ try {
250
+ const modelsResponse = await this.helpers.httpRequest({
251
+ method: 'GET',
252
+ url: 'https://dev.supermachine.art/v1/models',
253
+ headers: {
254
+ 'Authorization': `Bearer ${apiKey}`,
255
+ 'Content-Type': 'application/json',
256
+ },
257
+ json: true,
258
+ });
259
+
260
+ const models = (modelsResponse as IDataObject).items as IDataObject[];
261
+ const selectedModel = models.find((m: IDataObject) => m.title === modelTitle);
262
+
263
+ if (!selectedModel) {
264
+ return [{
265
+ name: `⚠️ Model "${modelTitle}" not found`,
266
+ value: '',
267
+ description: 'Try refreshing the page'
268
+ }];
269
+ }
270
+
271
+ const modelId = selectedModel.id;
272
+
273
+ const charactersResponse = await this.helpers.httpRequest({
274
+ method: 'GET',
275
+ url: `https://dev.supermachine.art/v1/characters?categoryId=${categoryId}&modelId=${modelId}`,
276
+ headers: {
277
+ 'Authorization': `Bearer ${apiKey}`,
278
+ 'Content-Type': 'application/json',
279
+ },
280
+ json: true,
281
+ });
282
+
283
+ const items = (charactersResponse as IDataObject).items as IDataObject[];
284
+
285
+ if (!Array.isArray(items) || items.length === 0) {
286
+ return [{
287
+ name: 'No characters available',
288
+ value: '',
289
+ description: `No characters found for this category and model combination`
290
+ }];
291
+ }
292
+
293
+ return items.map((character: IDataObject) => {
294
+ const embedCode = character.embedCode as string;
295
+ const embedHint = embedCode ? ` | Use: "${embedCode}"` : '';
296
+
297
+ return {
298
+ name: character.name as string,
299
+ value: String(character.id),
300
+ description: `${character.slug}${embedHint}`,
301
+ };
302
+ });
303
+
304
+ } catch (error) {
305
+ console.error('Supermachine getCharacters error:', error);
306
+ return [{
307
+ name: `⚠️ Error loading characters: ${(error as Error).message}`,
308
+ value: '',
309
+ description: 'Check your API key and network connection'
310
+ }];
311
+ }
312
+ },
313
+ },
314
+ };
315
+
316
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
317
+ const items = this.getInputData();
318
+ const returnData: INodeExecutionData[] = [];
319
+
320
+ for (let i = 0; i < items.length; i++) {
321
+ try {
322
+ const resource = this.getNodeParameter('resource', i) as string;
323
+ const operation = this.getNodeParameter('operation', i) as string;
324
+
325
+ let requestOptions: IDataObject = {
326
+ method: 'GET' as IHttpRequestMethods,
327
+ url: '',
328
+ json: true,
329
+ };
330
+
331
+ // ══════════════════════════════════════════════════════
332
+ // ACCOUNT OPERATIONS
333
+ // ══════════════════════════════════════════════════════
334
+ if (resource === 'account') {
335
+ if (operation === 'getProfile') {
336
+ requestOptions.url = 'https://dev.supermachine.art/v1/user';
337
+ }
338
+ else if (operation === 'listModels') {
339
+ requestOptions.url = 'https://dev.supermachine.art/v1/models';
340
+ if (!this.getNodeParameter('returnAll', i, false)) {
341
+ requestOptions.qs = {
342
+ perPage: this.getNodeParameter('limit', i, 20),
343
+ page: 1,
344
+ };
345
+ }
346
+ }
347
+ else if (operation === 'getModelDetail') {
348
+ const modelTitle = this.getNodeParameter('modelId', i) as string;
349
+ const modelsResponse = await this.helpers.requestWithAuthentication.call(
350
+ this,
351
+ 'supermachineApi',
352
+ { method: 'GET', url: 'https://dev.supermachine.art/v1/models', json: true },
353
+ );
354
+ const models = (modelsResponse as IDataObject).items as IDataObject[];
355
+ const model = models.find((m: IDataObject) => m.title === modelTitle);
356
+
357
+ if (!model) {
358
+ throw new Error(`Model "${modelTitle}" not found`);
359
+ }
360
+
361
+ requestOptions.url = `https://dev.supermachine.art/v1/models/gems/id/${model.id}`;
362
+ }
363
+ else if (operation === 'listModelCategories') {
364
+ requestOptions.url = 'https://dev.supermachine.art/v1/models/categories';
365
+ }
366
+ else if (operation === 'listLoras') {
367
+ const modelTitle = this.getNodeParameter('modelId', i) as string;
368
+ const modelsResponse = await this.helpers.requestWithAuthentication.call(
369
+ this,
370
+ 'supermachineApi',
371
+ { method: 'GET', url: 'https://dev.supermachine.art/v1/models', json: true },
372
+ );
373
+ const models = (modelsResponse as IDataObject).items as IDataObject[];
374
+ const model = models.find((m: IDataObject) => m.title === modelTitle);
375
+
376
+ if (!model) {
377
+ throw new Error(`Model "${modelTitle}" not found`);
378
+ }
379
+
380
+ requestOptions.url = `https://dev.supermachine.art/v1/loras?modelId=${model.id}`;
381
+ }
382
+ else if (operation === 'listLoraCategories') {
383
+ requestOptions.url = 'https://dev.supermachine.art/v1/loras/categories';
384
+ }
385
+ else if (operation === 'listCharacterCategories') {
386
+ requestOptions.url = 'https://dev.supermachine.art/v1/characters/categories';
387
+ }
388
+ }
389
+
390
+ // ══════════════════════════════════════════════════════
391
+ // IMAGE OPERATIONS
392
+ // ══════════════════════════════════════════════════════
393
+ else if (resource === 'image') {
394
+ if (operation === 'generate') {
395
+ requestOptions.method = 'POST';
396
+ requestOptions.url = 'https://dev.supermachine.art/v1/generate';
397
+
398
+ const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject;
399
+ const body: IDataObject = {
400
+ prompt: this.getNodeParameter('prompt', i) as string,
401
+ model: this.getNodeParameter('model', i) as string,
402
+ width: this.getNodeParameter('width', i, 1024) as number,
403
+ height: this.getNodeParameter('height', i, 768) as number,
404
+ };
405
+
406
+ if (additionalFields.negativePrompt) {
407
+ body.negativePrompt = additionalFields.negativePrompt;
408
+ }
409
+ if (additionalFields.numberResults) {
410
+ body.numberResults = additionalFields.numberResults;
411
+ }
412
+ if (additionalFields.outputFormat) {
413
+ body.outputFormat = additionalFields.outputFormat;
414
+ }
415
+ if (additionalFields.seed) {
416
+ body.seed = additionalFields.seed;
417
+ }
418
+ if (additionalFields.steps) {
419
+ body.steps = additionalFields.steps;
420
+ }
421
+ if (additionalFields.cfgScale) {
422
+ body.cfgScale = additionalFields.cfgScale;
423
+ }
424
+ if (additionalFields.callbackUrl) {
425
+ body.callbackUrl = additionalFields.callbackUrl;
426
+ }
427
+
428
+ if (additionalFields.loras) {
429
+ const lorasData = additionalFields.loras as IDataObject;
430
+ const loraValues = lorasData.loraValues as IDataObject[];
431
+ if (Array.isArray(loraValues) && loraValues.length > 0) {
432
+ body.loras = loraValues.map((lora: IDataObject) => ({
433
+ id: lora.loraId,
434
+ strength: lora.strength || 1,
435
+ }));
436
+ }
437
+ }
438
+
439
+ if (additionalFields.characterId) {
440
+ body.characterId = additionalFields.characterId;
441
+ }
442
+
443
+ if (additionalFields.img2img) {
444
+ const img2imgData = additionalFields.img2img as IDataObject;
445
+ const img2imgValues = img2imgData.img2imgValues as IDataObject[];
446
+ if (Array.isArray(img2imgValues) && img2imgValues.length > 0) {
447
+ const img2img = img2imgValues[0];
448
+ if (img2img.sourceImageUrl) {
449
+ body.sourceImageUrl = img2img.sourceImageUrl;
450
+ body.denoisingStrength = img2img.denoisingStrength || 0.75;
451
+ }
452
+ }
453
+ }
454
+
455
+ requestOptions.body = body;
456
+ }
457
+ else if (operation === 'getStatus' || operation === 'listImages') {
458
+ const batchId = this.getNodeParameter('batchId', i) as string;
459
+ requestOptions.url = `https://dev.supermachine.art/v1/images?batchId=${batchId}`;
460
+
461
+ if (operation === 'listImages' && !this.getNodeParameter('returnAll', i, false)) {
462
+ requestOptions.qs = {
463
+ limit: this.getNodeParameter('limit', i, 20),
464
+ page: this.getNodeParameter('page', i, 1),
465
+ };
466
+ }
467
+ }
468
+ }
469
+
470
+ // ══════════════════════════════════════════════════════
471
+ // TOOLS OPERATIONS
472
+ // ══════════════════════════════════════════════════════
473
+ else if (resource === 'tools') {
474
+ throw new Error('Tools operations chưa có endpoints chính thức từ Supermachine API docs. Vui lòng check documentation.');
475
+ }
476
+
477
+ const responseData = await this.helpers.requestWithAuthentication.call(
478
+ this,
479
+ 'supermachineApi',
480
+ requestOptions,
481
+ );
482
+
483
+ let jsonData: any;
484
+ if (operation === 'listModels' || operation === 'listImages' || operation === 'listLoras') {
485
+ jsonData = responseData;
486
+ } else {
487
+ jsonData = (responseData as any).items || responseData;
488
+ }
489
+
490
+ returnData.push({ json: jsonData });
491
+
492
+ } catch (error) {
493
+ if (this.continueOnFail()) {
494
+ returnData.push({
495
+ json: {
496
+ error: (error as Error).message,
497
+ stack: (error as Error).stack,
498
+ }
499
+ });
500
+ continue;
501
+ }
502
+ throw error;
503
+ }
504
+ }
505
+
506
+ return [returnData];
507
+ }
508
+ }
@@ -0,0 +1,175 @@
1
+ import type {
2
+ IHookFunctions,
3
+ IWebhookFunctions,
4
+ IWebhookResponseData,
5
+ INodeType,
6
+ INodeTypeDescription,
7
+ IDataObject,
8
+ ILoadOptionsFunctions,
9
+ INodePropertyOptions,
10
+ } from 'n8n-workflow';
11
+
12
+ import { createHmac, timingSafeEqual } from 'crypto';
13
+
14
+ export class SupermachineTrigger implements INodeType {
15
+ description: INodeTypeDescription = {
16
+ displayName: 'Supermachine Trigger',
17
+ name: 'supermachineTrigger',
18
+ // eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
19
+ icon: 'file:supermachine.svg',
20
+ group: ['trigger'],
21
+ version: 1,
22
+ subtitle: '=Job Completed',
23
+ description: 'Starts the workflow when Supermachine job is completed',
24
+ defaults: {
25
+ name: 'Supermachine Trigger',
26
+ },
27
+ inputs: [],
28
+ outputs: ['main'],
29
+ credentials: [
30
+ {
31
+ name: 'supermachineApi',
32
+ required: true,
33
+ },
34
+ ],
35
+ webhooks: [
36
+ {
37
+ name: 'default',
38
+ httpMethod: 'POST',
39
+ responseMode: 'onReceived',
40
+ path: 'webhook',
41
+ },
42
+ ],
43
+ properties: [
44
+ {
45
+ displayName: 'Event',
46
+ name: 'event',
47
+ type: 'options',
48
+ options: [
49
+ {
50
+ name: 'Job Completed',
51
+ value: 'job.completed',
52
+ },
53
+ {
54
+ name: 'Job Failed',
55
+ value: 'job.failed',
56
+ },
57
+ ],
58
+ default: 'job.completed',
59
+ required: true,
60
+ },
61
+ ],
62
+ };
63
+
64
+ methods = {
65
+ loadOptions: {
66
+ async getEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
67
+ return [
68
+ {
69
+ name: 'Job Completed',
70
+ value: 'job.completed',
71
+ },
72
+ {
73
+ name: 'Job Failed',
74
+ value: 'job.failed',
75
+ },
76
+ ];
77
+ },
78
+ },
79
+ };
80
+
81
+ webhookMethods = {
82
+ default: {
83
+ async checkExists(this: IHookFunctions): Promise<boolean> {
84
+ // Webhook must be configured manually in Supermachine dashboard
85
+ // This returns true to indicate webhook is ready to receive
86
+ return true;
87
+ },
88
+ async create(this: IHookFunctions): Promise<boolean> {
89
+ const webhookUrl = this.getNodeWebhookUrl('default') as string;
90
+ // Store webhook URL for user to configure manually
91
+ const staticData = this.getWorkflowStaticData('node');
92
+ staticData.webhookUrl = webhookUrl;
93
+
94
+ // Note: If Supermachine API supports webhook registration, implement here
95
+ // Example:
96
+ // const credentials = await this.getCredentials('supermachineApi');
97
+ // await this.helpers.requestWithAuthentication.call(this, 'supermachineApi', {
98
+ // method: 'POST',
99
+ // url: 'https://dev.supermachine.art/v1/webhooks',
100
+ // body: { url: webhookUrl, events: ['job.completed'] }
101
+ // });
102
+
103
+ return true;
104
+ },
105
+ async delete(this: IHookFunctions): Promise<boolean> {
106
+ // Implement webhook deletion if API supports
107
+ return true;
108
+ },
109
+ },
110
+ };
111
+
112
+ async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
113
+ const bodyData = this.getBodyData() as IDataObject;
114
+ const headers = this.getHeaderData() as IDataObject;
115
+
116
+ // Verify webhook signature if Supermachine sends signature
117
+ const signature = headers['x-supermachine-signature'];
118
+ if (signature && typeof signature === 'string') {
119
+ try {
120
+ const credentials = await this.getCredentials('supermachineApi');
121
+ const apiKey = credentials.apiKey as string;
122
+
123
+ // Get raw body for signature verification
124
+ const req = this.getRequestObject();
125
+ const rawBody = (req as any).rawBody || JSON.stringify(bodyData);
126
+
127
+ const hmac = createHmac('sha256', apiKey);
128
+ hmac.update(rawBody);
129
+ const calculatedSig = 'sha256=' + hmac.digest('hex');
130
+
131
+ // Safe comparison to prevent timing attacks
132
+ if (
133
+ signature.length !== calculatedSig.length ||
134
+ !timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSig))
135
+ ) {
136
+ throw new Error('Invalid webhook signature');
137
+ }
138
+ } catch (error) {
139
+ const err = error as Error;
140
+ throw new Error(`Signature verification failed: ${err.message}`);
141
+ }
142
+ }
143
+
144
+ // Parse response from Supermachine
145
+ let imageUrl = '';
146
+ if (bodyData.imageUrl && typeof bodyData.imageUrl === 'string') {
147
+ imageUrl = bodyData.imageUrl;
148
+ } else if (bodyData.output && typeof bodyData.output === 'object') {
149
+ const output = bodyData.output as IDataObject;
150
+ if (output.imageUrl && typeof output.imageUrl === 'string') {
151
+ imageUrl = output.imageUrl;
152
+ }
153
+ }
154
+
155
+ const jobData: IDataObject = {
156
+ batchId: bodyData.batchId || bodyData.id,
157
+ status: bodyData.status,
158
+ imageUrl,
159
+ model: bodyData.model,
160
+ prompt: bodyData.prompt,
161
+ creditsUsed: bodyData.creditsUsed,
162
+ timestamp: new Date().toISOString(),
163
+ };
164
+
165
+ return {
166
+ workflowData: [
167
+ [
168
+ {
169
+ json: jobData,
170
+ },
171
+ ],
172
+ ],
173
+ };
174
+ }
175
+ }