@limetech/n8n-nodes-lime 3.7.0 → 3.7.1

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.
@@ -1,6 +1,15 @@
1
- import { createHmac } from 'node:crypto';
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createHmac,
5
+ hkdfSync,
6
+ randomBytes,
7
+ } from 'node:crypto';
2
8
  import { NodeOperationError, INode } from 'n8n-workflow';
3
9
 
10
+ const ENCRYPTION_KEY_ENV = 'N8N_ENCRYPTION_KEY';
11
+ const HKDF_INFO = 'lime-webhook-secret';
12
+
4
13
  /**
5
14
  * Generate an HMAC SHA-256 hash for the given data using the provided key.
6
15
  *
@@ -57,20 +66,7 @@ export const verifyRequest = (
57
66
  webhookSecret: string,
58
67
  data: Buffer
59
68
  ): void => {
60
- if (!webhookSecret && !limeSignature) {
61
- throw new NodeOperationError(
62
- node,
63
- 'Webhook secret and lime signature are missing!'
64
- );
65
- }
66
- if (!webhookSecret && limeSignature) {
67
- throw new NodeOperationError(
68
- node,
69
- 'Webhook authentication failed, secret key is missing while signature is present!'
70
- );
71
- }
72
-
73
- if (!limeSignature && webhookSecret) {
69
+ if (!limeSignature) {
74
70
  throw new NodeOperationError(
75
71
  node,
76
72
  'Webhook authentication failed, signature key is missing while secret is present!'
@@ -85,3 +81,62 @@ export const verifyRequest = (
85
81
  );
86
82
  }
87
83
  };
84
+
85
+ /**
86
+ * Retrieves the master encryption key from the environment variable.
87
+ * Throws an error if the environment variable is not set.
88
+ *
89
+ * @return The master encryption key.
90
+ */
91
+ const getMasterKey = (): string => {
92
+ const key = process.env[ENCRYPTION_KEY_ENV];
93
+ if (!key) {
94
+ throw new Error(
95
+ `${ENCRYPTION_KEY_ENV} must be set to manage Lime CRM webhooks`
96
+ );
97
+ }
98
+ return key;
99
+ };
100
+
101
+ /**
102
+ * Encrypts a plaintext string using AES-256-GCM with a derived key based on HKDF.
103
+ *
104
+ * @param plaintext - The plain text string to be encrypted.
105
+ * @return The encrypted string encoded in base64 format.
106
+ */
107
+ export const encryptSecret = (plaintext: string): string => {
108
+ const salt = randomBytes(16);
109
+ const iv = randomBytes(12);
110
+ const key = Buffer.from(
111
+ hkdfSync('sha256', getMasterKey(), salt, HKDF_INFO, 32)
112
+ );
113
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
114
+ const ct = Buffer.concat([
115
+ cipher.update(plaintext, 'utf8'),
116
+ cipher.final(),
117
+ ]);
118
+ const tag = cipher.getAuthTag();
119
+ return Buffer.concat([salt, iv, tag, ct]).toString('base64');
120
+ };
121
+
122
+ /**
123
+ * Decrypts a Base64-encoded secret using AES-256-GCM with key derivation.
124
+ *
125
+ * @param blob - The Base64-encoded string containing the encrypted data, including salt, IV, authentication tag, and ciphertext.
126
+ * @return The decrypted data as a UTF-8 string.
127
+ */
128
+ export const decryptSecret = (blob: string): string => {
129
+ const buf = Buffer.from(blob, 'base64');
130
+ const salt = buf.subarray(0, 16);
131
+ const iv = buf.subarray(16, 28);
132
+ const tag = buf.subarray(28, 44);
133
+ const ct = buf.subarray(44);
134
+ const key = Buffer.from(
135
+ hkdfSync('sha256', getMasterKey(), salt, HKDF_INFO, 32)
136
+ );
137
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
138
+ decipher.setAuthTag(tag);
139
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString(
140
+ 'utf8'
141
+ );
142
+ };
@@ -19,9 +19,11 @@ import {
19
19
  getSubscription,
20
20
  listSubscriptionsWithExistingData,
21
21
  } from './transport';
22
- import { createHash } from 'node:crypto';
22
+ import { createHash, randomBytes } from 'node:crypto';
23
+
24
+ import { decryptSecret, encryptSecret } from '../crypto';
23
25
  import { getWebhook, handleWorkflowError } from './utils';
24
- import { verifyRequest } from '../common';
26
+ import { verifyRequest } from '../crypto';
25
27
 
26
28
  /**
27
29
  * Trigger node for handling incoming webhooks from **Lime CRM**.
@@ -165,7 +167,7 @@ export class LimeCrmTrigger implements INodeType {
165
167
  }
166
168
 
167
169
  try {
168
- await getSubscription(this, webhook);
170
+ await getSubscription(this, webhook.data.webhookId);
169
171
  } catch (error) {
170
172
  if (error.cause?.status === 404) {
171
173
  delete webhook.data.webhookId;
@@ -202,46 +204,44 @@ export class LimeCrmTrigger implements INodeType {
202
204
  });
203
205
  }
204
206
 
205
- if (existingSubscriptionResponse.data.length === 0) {
206
- const credentials = await this.getCredentials(
207
- LIME_CRM_API_CREDENTIAL_KEY
208
- );
209
- const webhookCreateData = {
210
- ...webhook,
211
- secret: credentials.webhookSecret as string,
212
- };
207
+ const rawSecret = randomBytes(32).toString('hex');
208
+ webhook.data.webhookSecret = encryptSecret(rawSecret);
209
+ const webhookCreateData = {
210
+ ...webhook,
211
+ secret: rawSecret,
212
+ };
213
213
 
214
- const createSubscriptionResponse = await createSubscription(
215
- this,
216
- webhookCreateData
217
- );
214
+ const createSubscriptionResponse = await createSubscription(
215
+ this,
216
+ webhookCreateData
217
+ );
218
218
 
219
- if (!createSubscriptionResponse.success) {
220
- throw new NodeApiError(this.getNode(), {
221
- message:
222
- createSubscriptionResponse.data.error.message,
223
- });
219
+ if (!createSubscriptionResponse.success) {
220
+ throw new NodeApiError(this.getNode(), {
221
+ message: createSubscriptionResponse.data.error.message,
222
+ });
223
+ }
224
+
225
+ // Delete existing duplicated webhooks if a new one was successfully created
226
+ if (existingSubscriptionResponse.data.length > 0) {
227
+ for (const subscription of existingSubscriptionResponse.data) {
228
+ Logger.info(
229
+ 'Deleting existing Lime CRM webhook with ID: ' +
230
+ subscription.id
231
+ );
232
+ await deleteSubscription(this, subscription.id);
224
233
  }
234
+ }
225
235
 
226
- const subscriptionId = createSubscriptionResponse.data.id;
227
- const events = createSubscriptionResponse.data.events;
236
+ const subscriptionId = createSubscriptionResponse.data.id;
237
+ const events = createSubscriptionResponse.data.events;
228
238
 
229
- webhook.data.webhookId = subscriptionId;
230
- webhook.data.webhookEvents = events;
231
- Logger.info(
232
- `Webhook with URL ${webhook.url}, ID ${subscriptionId} and events ${events} created!`
233
- );
234
- return true;
235
- } else {
236
- const limeWebhook = existingSubscriptionResponse.data[0];
237
- webhook.data.webhookId = limeWebhook.id;
238
- webhook.data.webhookEvents = limeWebhook.events;
239
- Logger.info(
240
- `Webhook with URL ${webhook.url}, and events ${webhook.events} exists in Lime.
241
- ID ${limeWebhook.id} set up to the N8N data.`
242
- );
243
- return true;
244
- }
239
+ webhook.data.webhookId = subscriptionId;
240
+ webhook.data.webhookEvents = events;
241
+ Logger.info(
242
+ `Webhook with URL ${webhook.url}, ID ${subscriptionId} and events ${events} created!`
243
+ );
244
+ return true;
245
245
  },
246
246
 
247
247
  async delete(this: IHookFunctions): Promise<boolean> {
@@ -252,7 +252,7 @@ export class LimeCrmTrigger implements INodeType {
252
252
  );
253
253
  if (webhook.data.webhookId !== undefined) {
254
254
  try {
255
- await deleteSubscription(this, webhook);
255
+ await deleteSubscription(this, webhook.data.webhookId);
256
256
  } catch {
257
257
  Logger.error(
258
258
  `Failed to delete webhook with ID: ${webhook.data.webhookId}`,
@@ -296,10 +296,14 @@ export class LimeCrmTrigger implements INodeType {
296
296
  Logger.info('Webhook received. Starting webhook processing...', {
297
297
  ...webhook.context,
298
298
  });
299
- const credentials = await this.getCredentials(
300
- LIME_CRM_API_CREDENTIAL_KEY
301
- );
302
- const webhookSecret = credentials.webhookSecret as string;
299
+ const encryptedSecret = webhook.data.webhookSecret;
300
+ if (!encryptedSecret) {
301
+ throw new NodeOperationError(
302
+ this.getNode(),
303
+ 'Webhook is not registered: missing secret in static data'
304
+ );
305
+ }
306
+ const webhookSecret = decryptSecret(encryptedSecret);
303
307
  const requestObject = this.getRequestObject();
304
308
  const headerData = this.getHeaderData();
305
309
  const bodyData = this.getBodyData();
@@ -42,7 +42,7 @@ export interface WebhookContext {
42
42
  * @group Models
43
43
  */
44
44
  export interface Webhook {
45
- data: IDataObject;
45
+ data: IDataObject & { webhookId?: string; webhookSecret?: string };
46
46
  events: string[];
47
47
  url?: string;
48
48
  context: WebhookContext;
@@ -35,7 +35,7 @@ export interface ApiResponseWebhook {
35
35
  * Retrieve details of a specific subscription from Lime CRM.
36
36
  *
37
37
  * @param nodeContext - The n8n node execution context
38
- * @param webhook - The webhook object containing subscription details
38
+ * @param webhookId - Id of a webhook containing subscription details
39
39
  *
40
40
  * @returns The subscription information from Lime CRM.
41
41
  *
@@ -44,11 +44,11 @@ export interface ApiResponseWebhook {
44
44
  */
45
45
  export async function getSubscription(
46
46
  nodeContext: IAllExecuteFunctions,
47
- webhook: Webhook
47
+ webhookId: string
48
48
  ): Promise<APIResponse<ApiResponseWebhook>> {
49
49
  return await callLimeApi(nodeContext, {
50
50
  method: 'GET',
51
- url: `${SUBSCRIPTION_URL}${webhook.data.webhookId}`,
51
+ url: `${SUBSCRIPTION_URL}${webhookId}`,
52
52
  });
53
53
  }
54
54
 
@@ -113,7 +113,7 @@ export async function createSubscription(
113
113
  * Delete a webhook subscription from Lime CRM.
114
114
  *
115
115
  * @param nodeContext - The n8n node execution context
116
- * @param webhook - The webhook object identifying the subscription to delete
116
+ * @param webhookId - ID of a webhook that should be deleted
117
117
  *
118
118
  * @returns Response indicating success or failure of the deletion.
119
119
  *
@@ -122,13 +122,13 @@ export async function createSubscription(
122
122
  */
123
123
  export async function deleteSubscription(
124
124
  nodeContext: IAllExecuteFunctions,
125
- webhook: Webhook
125
+ webhookId: string
126
126
  ): Promise<APIResponse<void>> {
127
127
  return await callLimeApi(nodeContext, {
128
128
  method: 'DELETE',
129
- url: `${SUBSCRIPTION_URL}${webhook.data.webhookId}/`,
129
+ url: `${SUBSCRIPTION_URL}${webhookId}/`,
130
130
  errorMetadata: {
131
- id: webhook.data.webhookId,
131
+ id: webhookId,
132
132
  },
133
133
  });
134
134
  }
@@ -22,7 +22,7 @@ import { ObservableActionType } from './types/enums/ObservableAction';
22
22
  import { ObservableType } from './types/enums/ObservableType';
23
23
  import { FORMS_API_CREDENTIALS_NAME } from '../../credentials';
24
24
  import { getWorkflowUrl } from './utils/workflow';
25
- import { verifyHmac } from '../common';
25
+ import { verifyHmac } from '../crypto';
26
26
  import { handleWorkflowError } from '../errorHandling';
27
27
 
28
28
  const FORMS_OBSERVABLE_WEBHOOK_NAME_PREFIX = 'N8N';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limetech/n8n-nodes-lime",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "description": "n8n node to connect to Lime CRM",
5
5
  "license": "Apache-2.0",
6
6
  "main": "nodes/index.ts",
@@ -1,29 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifyRequest = void 0;
4
- exports.verifyHmac = verifyHmac;
5
- const node_crypto_1 = require("node:crypto");
6
- const n8n_workflow_1 = require("n8n-workflow");
7
- function generateHmac(key, data) {
8
- return 'sha256=' + (0, node_crypto_1.createHmac)('sha256', key).update(data).digest('hex');
9
- }
10
- function verifyHmac(key, data, comparedHmac) {
11
- return generateHmac(key, data) === comparedHmac;
12
- }
13
- const verifyRequest = (node, limeSignature, webhookSecret, data) => {
14
- if (!webhookSecret && !limeSignature) {
15
- throw new n8n_workflow_1.NodeOperationError(node, 'Webhook secret and lime signature are missing!');
16
- }
17
- if (!webhookSecret && limeSignature) {
18
- throw new n8n_workflow_1.NodeOperationError(node, 'Webhook authentication failed, secret key is missing while signature is present!');
19
- }
20
- if (!limeSignature && webhookSecret) {
21
- throw new n8n_workflow_1.NodeOperationError(node, 'Webhook authentication failed, signature key is missing while secret is present!');
22
- }
23
- const expectedHmac = generateHmac(webhookSecret, data);
24
- if (expectedHmac !== limeSignature) {
25
- throw new n8n_workflow_1.NodeOperationError(node, 'Webhook authentication failed, signatures do not match');
26
- }
27
- };
28
- exports.verifyRequest = verifyRequest;
29
- //# sourceMappingURL=common.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"common.js","sourceRoot":"","sources":["../../nodes/common.ts"],"names":[],"mappings":";;;AA6BA,gCAMC;AAnCD,6CAAyC;AACzC,+CAAyD;AAYzD,SAAS,YAAY,CAAC,GAAW,EAAE,IAAY;IAC3C,OAAO,SAAS,GAAG,IAAA,wBAAU,EAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC;AAcD,SAAgB,UAAU,CACtB,GAAW,EACX,IAAY,EACZ,YAAoB;IAEpB,OAAO,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,YAAY,CAAC;AACpD,CAAC;AAkBM,MAAM,aAAa,GAAG,CACzB,IAAW,EACX,aAAqB,EACrB,aAAqB,EACrB,IAAY,EACR,EAAE;IACN,IAAI,CAAC,aAAa,IAAI,CAAC,aAAa,EAAE,CAAC;QACnC,MAAM,IAAI,iCAAkB,CACxB,IAAI,EACJ,gDAAgD,CACnD,CAAC;IACN,CAAC;IACD,IAAI,CAAC,aAAa,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,iCAAkB,CACxB,IAAI,EACJ,kFAAkF,CACrF,CAAC;IACN,CAAC;IAED,IAAI,CAAC,aAAa,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,iCAAkB,CACxB,IAAI,EACJ,kFAAkF,CACrF,CAAC;IACN,CAAC;IAED,MAAM,YAAY,GAAG,YAAY,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;IACvD,IAAI,YAAY,KAAK,aAAa,EAAE,CAAC;QACjC,MAAM,IAAI,iCAAkB,CACxB,IAAI,EACJ,wDAAwD,CAC3D,CAAC;IACN,CAAC;AACL,CAAC,CAAC;AAjCW,QAAA,aAAa,iBAiCxB"}