n8n-nodes-wassenger 1.2.4 → 1.2.6

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.
@@ -164,8 +164,8 @@ exports.chatContactProperties = [
164
164
  displayName: 'Country',
165
165
  name: 'country',
166
166
  type: 'multiOptions',
167
- options: [{ name: 'Any country', value: '' }].concat(countries_1.countries.map(country => ({ name: country.name + ' ' + country.flag, value: country.code }))),
168
- default: '',
167
+ options: countries_1.countries.map(country => ({ name: country.name + ' ' + country.flag, value: country.code })),
168
+ default: [],
169
169
  placeholder: 'Select countries',
170
170
  description: 'Filter contacts by country codes',
171
171
  required: false
@@ -287,10 +287,8 @@ exports.chatContactProperties = [
287
287
  options: [
288
288
  { name: 'Name A-Z', value: 'name:asc' },
289
289
  { name: 'Name Z-A', value: 'name:desc' },
290
- { name: 'Recently Created', value: 'createdAt:desc' },
291
- { name: 'Oldest Created', value: 'createdAt:asc' },
292
- { name: 'Recently Updated', value: 'updatedAt:desc' },
293
- { name: 'Oldest Updated', value: 'updatedAt:asc' },
290
+ { name: 'Recently Updated', value: 'date:desc' },
291
+ { name: 'Oldest Updated', value: 'date:asc' },
294
292
  ],
295
293
  default: 'name:asc',
296
294
  description: 'Sort order for results',
@@ -820,13 +818,20 @@ async function executeChatContactOperations(index) {
820
818
  if (operation === 'getDeviceContacts') {
821
819
  const filters = this.getNodeParameter('filters', index, {});
822
820
  const queryParameters = {};
823
- // Add all filters to query parameters
821
+ // Add all filters to query parameters. "Any country" was an option worth
822
+ // '', which the API rejects, and saved workflows can still carry it.
824
823
  for (const [key, value] of Object.entries(filters)) {
825
- if (value !== undefined && value !== '' &&
826
- (typeof value !== 'object' || (Array.isArray(value) && value.length > 0))) {
827
- queryParameters[key] = value;
824
+ const kept = Array.isArray(value) ? value.filter((item) => item !== '') : value;
825
+ if (kept !== undefined && kept !== '' &&
826
+ (typeof kept !== 'object' || (Array.isArray(kept) && kept.length > 0))) {
827
+ queryParameters[key] = kept;
828
828
  }
829
829
  }
830
+ // The API's date sort is by update date, which these values used to call
831
+ // updatedAt, and saved workflows keep the value they were saved with.
832
+ if (typeof queryParameters.sort === 'string') {
833
+ queryParameters.sort = queryParameters.sort.replace(/^updatedAt:/, 'date:');
834
+ }
830
835
  return (0, request_1.request)(this, 'GET', `/chat/${device}/contacts`, undefined, queryParameters);
831
836
  }
832
837
  // GET CONTACT DETAILS
@@ -89,9 +89,7 @@ exports.chatFileProperties = [
89
89
  { name: 'Image', value: 'image' },
90
90
  { name: 'Video', value: 'video' },
91
91
  { name: 'Audio', value: 'audio' },
92
- { name: 'Voice', value: 'ptt' },
93
92
  { name: 'Document', value: 'document' },
94
- { name: 'Sticker', value: 'sticker' },
95
93
  ],
96
94
  default: [],
97
95
  description: 'Filter files by media type',
@@ -180,8 +178,6 @@ exports.chatFileProperties = [
180
178
  options: [
181
179
  { name: 'Recent First', value: 'date:desc' },
182
180
  { name: 'Oldest First', value: 'date:asc' },
183
- { name: 'Largest First', value: 'size:desc' },
184
- { name: 'Smallest First', value: 'size:asc' },
185
181
  ],
186
182
  default: 'date:desc',
187
183
  description: 'Files sorting criteria',
@@ -269,9 +269,8 @@ exports.chatProperties = [
269
269
  name: 'include',
270
270
  type: 'multiOptions',
271
271
  options: [
272
- { name: 'Messages', value: 'messages' },
273
- { name: 'Notes', value: 'notes' },
274
- { name: 'Participants', value: 'participants' },
272
+ { name: 'Last Message', value: 'lastMessage' },
273
+ { name: 'Group Participants', value: 'group.participants' },
275
274
  ],
276
275
  default: [],
277
276
  description: 'Include related data in the response',
@@ -295,13 +294,10 @@ exports.chatProperties = [
295
294
  name: 'sort',
296
295
  type: 'options',
297
296
  options: [
298
- { name: 'Most Recent First', value: 'lastMessageAt:desc' },
299
- { name: 'Oldest First', value: 'lastMessageAt:asc' },
300
- { name: 'Unread First', value: 'unreadCount:desc' },
301
- { name: 'Name A-Z', value: 'name:asc' },
302
- { name: 'Name Z-A', value: 'name:desc' },
297
+ { name: 'Recent Activity First', value: 'date:desc' },
298
+ { name: 'Oldest Activity First', value: 'date:asc' },
303
299
  ],
304
- default: 'lastMessageAt:desc',
300
+ default: 'date:desc',
305
301
  description: 'Sort order for results',
306
302
  },
307
303
  ],
@@ -318,12 +314,10 @@ exports.chatProperties = [
318
314
  },
319
315
  },
320
316
  options: [
321
- { name: 'Messages', value: 'messages' },
322
- { name: 'Notes', value: 'notes' },
323
- { name: 'Participants', value: 'participants' },
317
+ { name: 'Recent Files', value: 'files' },
324
318
  ],
325
319
  default: [],
326
- description: 'Include related data in the response',
320
+ description: 'Include the 30 most recent files shared in the chat',
327
321
  },
328
322
  // SEND TYPING STATUS OPTIONS
329
323
  {
@@ -947,6 +941,13 @@ exports.chatProperties = [
947
941
  description: 'Optional department ID to assign the chats to',
948
942
  },
949
943
  ];
944
+ // The sorts Get Chats used to offer, which the API rejects, as the console
945
+ // asks the API for the same orders. Saved workflows keep the value they were
946
+ // saved with, and the first one was the default.
947
+ const OLD_CHAT_SORTS = new Map([
948
+ ['lastMessageAt:desc', 'date:desc'],
949
+ ['lastMessageAt:asc', 'date:asc'],
950
+ ]);
950
951
  async function executeChatOperations(index) {
951
952
  const operation = this.getNodeParameter('operation', index);
952
953
  const device = this.getNodeParameter('device', index);
@@ -961,6 +962,10 @@ async function executeChatOperations(index) {
961
962
  queryParameters[key] = value;
962
963
  }
963
964
  }
965
+ const sort = OLD_CHAT_SORTS.get(String(queryParameters.sort));
966
+ if (sort) {
967
+ queryParameters.sort = sort;
968
+ }
964
969
  return (0, request_1.request)(this, 'GET', `/chat/${device}/chats`, undefined, queryParameters);
965
970
  }
966
971
  // GET DEVICE CHAT DETAILS
@@ -66,7 +66,7 @@ exports.otherProperties = [
66
66
  options: [{ name: 'Any country', value: '' }].concat(countries_1.countries.map(country => ({ name: country.name + ' ' + country.flag, value: country.code }))),
67
67
  default: '',
68
68
  placeholder: 'Select the country code...',
69
- description: 'The country code for the phone numbers.',
69
+ description: 'Country of the numbers written without their country code: a number with no + is read as a national number of this country.',
70
70
  required: false,
71
71
  displayOptions: {
72
72
  show: {
@@ -249,6 +249,17 @@ exports.otherProperties = [
249
249
  }
250
250
  },
251
251
  ];
252
+ // The calling code the API puts in front of a national number, if the country
253
+ // has one. The country list keeps the territories of the North American
254
+ // numbering plan as 1 followed by their area code, which their national
255
+ // numbers already carry.
256
+ function callingCode(code) {
257
+ const country = countries_1.countries.find((entry) => entry.code === code);
258
+ if (!country) {
259
+ return undefined;
260
+ }
261
+ return country.phonePrefix >= 1000 ? 1 : country.phonePrefix;
262
+ }
252
263
  async function executeOtherOperations(itemIndex) {
253
264
  const operation = this.getNodeParameter('operation', itemIndex);
254
265
  if (operation === 'verifyNumberExists') {
@@ -259,13 +270,27 @@ async function executeOtherOperations(itemIndex) {
259
270
  const requestBody = {};
260
271
  const country = this.getNodeParameter('country', itemIndex);
261
272
  const numbers = this.getNodeParameter('phoneNumbers.numbers', itemIndex, []);
262
- if (country && country !== 'any') {
263
- requestBody.country = country.slice(0, 2).toUpperCase();
273
+ // The prefix comes from the whole value: its first two letters would turn
274
+ // "Mexico" into Montenegro and "Chile" into Switzerland.
275
+ const code = (country || '').trim().toUpperCase();
276
+ let prefix;
277
+ if (code && code !== 'ANY') {
278
+ requestBody.country = code.slice(0, 2);
279
+ prefix = callingCode(code);
264
280
  }
265
281
  if (numbers === null || numbers === void 0 ? void 0 : numbers.length) {
266
- requestBody.numbers = numbers
282
+ // The API reads the country only together with a dialling prefix, and
283
+ // puts that prefix in front of a number unless its first character is a
284
+ // +: a number whose + comes after a bracket or an invisible mark would
285
+ // get it twice, so only the numbers with no + carry it.
286
+ const phones = numbers
267
287
  .filter(({ phone }) => phone && typeof phone === 'string')
268
- .map(({ phone }) => ({ phone: phone.trim() }));
288
+ .map(({ phone }) => phone.trim());
289
+ const national = (phone) => !/^[^\d+]*\+/.test(phone);
290
+ if (requestBody.country && !prefix && phones.some(national)) {
291
+ throw new Error(`No calling code is known for the country "${code}": use a two-letter code such as GB or ES, or write every number with its + and country code`);
292
+ }
293
+ requestBody.numbers = phones.map((phone) => (prefix && national(phone) ? { phone, prefix } : { phone }));
269
294
  }
270
295
  return await (0, request_1.request)(this, 'POST', '/numbers/validate', requestBody);
271
296
  }
@@ -66,25 +66,29 @@ exports.outboundMessageProperties = [
66
66
  options: [
67
67
  { name: 'Queued', value: 'queued' },
68
68
  { name: 'Processing', value: 'processing' },
69
- { name: 'Sent', value: 'sent' },
70
- { name: 'Delivered', value: 'delivered' },
71
- { name: 'Read', value: 'read' },
69
+ { name: 'Processed', value: 'processed' },
72
70
  { name: 'Failed', value: 'failed' },
73
- { name: 'Pending', value: 'pending' },
71
+ { name: 'Expired', value: 'expired' },
72
+ { name: 'Removed', value: 'removed' },
73
+ { name: 'Invalid', value: 'invalid' },
74
74
  ],
75
75
  default: [],
76
- description: 'Filter messages by status',
76
+ description: 'Filter messages by status. How far each one got with the recipient is the Delivery Status.',
77
77
  },
78
78
  {
79
79
  displayName: 'Delivery Status',
80
80
  name: 'deliveryStatus',
81
81
  type: 'multiOptions',
82
82
  options: [
83
- { name: 'Pending', value: 'pending' },
83
+ { name: 'Queued', value: 'queued' },
84
+ { name: 'Processing', value: 'processing' },
84
85
  { name: 'Sent', value: 'sent' },
85
86
  { name: 'Delivered', value: 'delivered' },
86
87
  { name: 'Read', value: 'read' },
88
+ { name: 'Played', value: 'played' },
87
89
  { name: 'Failed', value: 'failed' },
90
+ { name: 'Cancelled', value: 'cancelled' },
91
+ { name: 'Revoked', value: 'revoked' },
88
92
  ],
89
93
  default: [],
90
94
  description: 'Filter messages by delivery status',
@@ -95,8 +99,11 @@ exports.outboundMessageProperties = [
95
99
  type: 'multiOptions',
96
100
  options: [
97
101
  { name: 'Pending', value: 'pending' },
98
- { name: 'Success', value: 'success' },
102
+ { name: 'Notified', value: 'notified' },
99
103
  { name: 'Failed', value: 'failed' },
104
+ { name: 'Ignored', value: 'ignored' },
105
+ { name: 'Expired', value: 'expired' },
106
+ { name: 'Cancelled', value: 'cancelled' },
100
107
  ],
101
108
  default: [],
102
109
  description: 'Filter messages by webhook status',
@@ -107,8 +114,9 @@ exports.outboundMessageProperties = [
107
114
  type: 'multiOptions',
108
115
  options: [
109
116
  { name: 'Device', value: 'device' },
110
- { name: 'Agent', value: 'agent' },
111
- { name: 'Events', value: 'events' },
117
+ { name: 'Webhooks', value: 'webhooks' },
118
+ { name: 'Logs', value: 'logs' },
119
+ { name: 'Features', value: 'features' },
112
120
  ],
113
121
  default: [],
114
122
  description: 'Expand subentity documents per message',
@@ -166,13 +174,9 @@ exports.outboundMessageProperties = [
166
174
  { name: 'Image', value: 'image' },
167
175
  { name: 'Video', value: 'video' },
168
176
  { name: 'Audio', value: 'audio' },
169
- { name: 'Voice', value: 'voice' },
170
177
  { name: 'Document', value: 'document' },
171
178
  { name: 'Location', value: 'location' },
172
179
  { name: 'Contact', value: 'contact' },
173
- { name: 'Buttons', value: 'buttons' },
174
- { name: 'List', value: 'list' },
175
- { name: 'Template', value: 'template' },
176
180
  ],
177
181
  default: [],
178
182
  description: 'Filter messages by type',
@@ -289,10 +293,12 @@ exports.outboundMessageProperties = [
289
293
  name: 'sort',
290
294
  type: 'options',
291
295
  options: [
292
- { name: 'Recent First', value: 'date:desc' },
293
- { name: 'Oldest First', value: 'date:asc' },
296
+ { name: 'Recently Created First', value: 'createdAt:desc' },
297
+ { name: 'Oldest Created First', value: 'createdAt:asc' },
298
+ { name: 'Latest Delivery First', value: 'deliverAt:desc' },
299
+ { name: 'Earliest Delivery First', value: 'deliverAt:asc' },
294
300
  ],
295
- default: 'date:desc',
301
+ default: 'createdAt:desc',
296
302
  description: 'Sort order for results',
297
303
  },
298
304
  {
@@ -338,8 +344,9 @@ exports.outboundMessageProperties = [
338
344
  },
339
345
  options: [
340
346
  { name: 'Device', value: 'device' },
341
- { name: 'Agent', value: 'agent' },
342
- { name: 'Events', value: 'events' },
347
+ { name: 'Webhooks', value: 'webhooks' },
348
+ { name: 'Logs', value: 'logs' },
349
+ { name: 'Preview', value: 'preview' },
343
350
  ],
344
351
  default: [],
345
352
  description: 'Include additional information in the response',
@@ -506,6 +513,11 @@ async function executeOutboundMessageOperations(index) {
506
513
  queryParameters[key] = value;
507
514
  }
508
515
  }
516
+ // The sorts used to be date:desc, the default, and date:asc, which the API
517
+ // rejects; saved workflows keep the value they were saved with.
518
+ if (typeof queryParameters.sort === 'string') {
519
+ queryParameters.sort = queryParameters.sort.replace(/^date:/, 'createdAt:');
520
+ }
509
521
  return (0, request_1.request)(this, 'GET', '/messages', undefined, queryParameters);
510
522
  }
511
523
  // GET MESSAGE
package/dist/config.js CHANGED
@@ -3,10 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const name = 'Wassenger';
4
4
  const code = 'wassenger';
5
5
  const waba = '$waba'; // eslint-disable-line
6
+ const client = '$client'; // eslint-disable-line
7
+ // Replaced at build time for OEM/white-label builds. Verified community nodes
8
+ // can't read environment variables, so the value has to be baked in here.
9
+ const runtimeClient = client === '$client' ? '' : client;
6
10
  const config = {
7
11
  name,
8
12
  code,
9
13
  description: 'Wassenger integration for n8n',
10
14
  waba: waba === 'true',
15
+ client: runtimeClient,
16
+ // Prefix applied to the API key for OEM builds. Empty on the public package.
17
+ keyPrefix: runtimeClient ? `${runtimeClient}_` : '',
18
+ // Label prefix used when registering webhooks with the Wassenger API.
19
+ webhookPrefix: runtimeClient.replace('_', '') || 'n8n',
11
20
  };
12
21
  exports.default = config;
@@ -252,7 +252,7 @@ exports.countries = [
252
252
  name: 'Cayman Islands',
253
253
  flag: '🇰🇾',
254
254
  code: 'KY',
255
- phonePrefix: 345
255
+ phonePrefix: 1345
256
256
  },
257
257
  {
258
258
  name: 'Central African Republic',
@@ -593,7 +593,7 @@ exports.countries = [
593
593
  name: 'Vatican City State',
594
594
  flag: '🇻🇦',
595
595
  code: 'VA',
596
- phonePrefix: 379
596
+ phonePrefix: 39
597
597
  },
598
598
  {
599
599
  name: 'Honduras',
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ApiKey = void 0;
7
+ const config_1 = __importDefault(require("../config"));
4
8
  class ApiKey {
5
9
  constructor() {
6
10
  this.name = 'wassengerApiKey';
@@ -9,12 +13,30 @@ class ApiKey {
9
13
  this.properties = [
10
14
  {
11
15
  displayName: 'API Key',
16
+ // Renaming this would invalidate every saved credential, so it stays as is.
12
17
  name: 'wassengerApiKey',
13
18
  type: 'string',
19
+ typeOptions: { password: true },
14
20
  default: '',
15
21
  required: true,
22
+ description: 'Create one at https://app.wassenger.com/developers/apikeys',
16
23
  },
17
24
  ];
25
+ this.authenticate = {
26
+ type: 'generic',
27
+ properties: {
28
+ headers: {
29
+ Authorization: `=Bearer ${config_1.default.keyPrefix}{{$credentials.wassengerApiKey}}`,
30
+ },
31
+ },
32
+ };
33
+ this.test = {
34
+ request: {
35
+ baseURL: 'https://api.wassenger.com/v1',
36
+ url: '/devices',
37
+ method: 'GET',
38
+ },
39
+ };
18
40
  }
19
41
  }
20
42
  exports.ApiKey = ApiKey;
@@ -4,9 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WassengerTrigger = void 0;
7
- const axios_1 = __importDefault(require("axios"));
8
7
  const request_1 = require("../request");
9
- const clientRuntime = process.env.N8N_RUNTIME_CLIENT || 'n8n';
8
+ const config_1 = __importDefault(require("../config"));
10
9
  /**
11
10
  * The API stores the webhook URL exactly as it was registered, including the
12
11
  * `?workflow=<id>` suffix that `create()` appends. Comparing the raw strings
@@ -388,6 +387,52 @@ const sampleWebhookEvents = [
388
387
  name: 'WhatsApp number session status update',
389
388
  event: 'number:session'
390
389
  },
390
+ // The ids have to match the sample payloads the API serves from
391
+ // common/docs.js: `create()` sends the selected id as `?sample=`, and an id
392
+ // the API cannot resolve falls back to `inbound-text`, which would hand a
393
+ // funnel workflow a message-shaped payload to map its fields against.
394
+ {
395
+ id: 'deal-stage-changed',
396
+ type: 'deal',
397
+ name: 'Sales funnel deal moved to another stage',
398
+ event: 'deal:stage:changed'
399
+ },
400
+ {
401
+ id: 'deal-won',
402
+ type: 'deal',
403
+ name: 'Sales funnel deal won',
404
+ event: 'deal:won'
405
+ },
406
+ {
407
+ id: 'deal-lost',
408
+ type: 'deal',
409
+ name: 'Sales funnel deal lost',
410
+ event: 'deal:lost'
411
+ },
412
+ {
413
+ id: 'deal-assigned',
414
+ type: 'deal',
415
+ name: 'Sales funnel deal assigned to an agent',
416
+ event: 'deal:assigned'
417
+ },
418
+ {
419
+ id: 'deal-updated',
420
+ type: 'deal',
421
+ name: 'Sales funnel deal title or amount updated',
422
+ event: 'deal:updated'
423
+ },
424
+ {
425
+ id: 'deal-archived',
426
+ type: 'deal',
427
+ name: 'Sales funnel deal archived',
428
+ event: 'deal:archived'
429
+ },
430
+ {
431
+ id: 'deal-reopened',
432
+ type: 'deal',
433
+ name: 'Sales funnel deal reopened',
434
+ event: 'deal:reopened'
435
+ },
391
436
  ];
392
437
  class WassengerTrigger {
393
438
  constructor() {
@@ -461,6 +506,18 @@ class WassengerTrigger {
461
506
  { name: 'Chat Updated (Assigned, Resolved, etc.)', value: 'chat:update' },
462
507
  { name: 'WhatsApp Session Changes', value: 'number:session' },
463
508
  { name: 'Contact Information Updated', value: 'contact:update' },
509
+ // Sales funnel events. Subscribable since the funnel shipped, but
510
+ // missing from this list, which forced anyone automating the funnel
511
+ // onto a generic Webhook node. Gated to Professional and above by
512
+ // the API (see EventConfig in models/webhook.js): the registration
513
+ // call is what rejects a lower plan, not this list.
514
+ { name: 'Sales Funnel Deal Moved to Another Stage', value: 'deal:stage:changed' },
515
+ { name: 'Sales Funnel Deal Won', value: 'deal:won' },
516
+ { name: 'Sales Funnel Deal Lost', value: 'deal:lost' },
517
+ { name: 'Sales Funnel Deal Assigned or Unassigned', value: 'deal:assigned' },
518
+ { name: 'Sales Funnel Deal Title or Amount Updated', value: 'deal:updated' },
519
+ { name: 'Sales Funnel Deal Archived', value: 'deal:archived' },
520
+ { name: 'Sales Funnel Deal Reopened', value: 'deal:reopened' },
464
521
  ],
465
522
  },
466
523
  {
@@ -483,32 +540,20 @@ class WassengerTrigger {
483
540
  const credentials = await this.getCredentials('wassengerApiKey');
484
541
  const apiKey = credentials.wassengerApiKey;
485
542
  try {
486
- const response = await (0, request_1.rawRequest)({ url: '/devices?size=100' }, apiKey);
543
+ const response = await (0, request_1.rawRequest)(this, { url: '/devices?size=100' }, apiKey);
487
544
  if (Array.isArray(response.data) && response.data.length > 0) {
488
545
  const devices = response.data.map((device) => ({
489
546
  name: `${device.alias || 'Unnamed'} (${device.phone || 'No phone yet'})`,
490
547
  value: device.id,
491
548
  }));
492
- if (clientRuntime) {
493
- return devices;
494
- }
495
- return [
496
- {
497
- name: 'All WhatsApp numbers in your account',
498
- value: '',
499
- },
500
- ...devices,
501
- ];
502
- }
503
- if (clientRuntime) {
504
- return [
505
- {
506
- name: 'No WhatsApp numbers available: connect one or upgrade plan ',
507
- value: 'invalid',
508
- }
509
- ];
549
+ return devices;
510
550
  }
511
- return [{ name: 'All WhatsApp numbers in your account', value: '' }];
551
+ return [
552
+ {
553
+ name: 'No WhatsApp numbers available: connect one or upgrade plan ',
554
+ value: 'invalid',
555
+ }
556
+ ];
512
557
  }
513
558
  catch (error) {
514
559
  return [{ name: 'All WhatsApp numbers in your account', value: '' }];
@@ -545,7 +590,7 @@ class WassengerTrigger {
545
590
  // No `workflow` query param here on purpose: the API treats it as a bulk
546
591
  // cleanup trigger and would soft-delete the very webhook being checked,
547
592
  // leaving the workflow with no registration at all.
548
- const response = await (0, request_1.rawRequest)({ url: `/webhooks/${webhookId}` }, apiKey);
593
+ const response = await (0, request_1.rawRequest)(this, { url: `/webhooks/${webhookId}` }, apiKey);
549
594
  return matchesRegistration(response.data, { url: webhookUrl, device, events });
550
595
  }
551
596
  catch {
@@ -557,7 +602,7 @@ class WassengerTrigger {
557
602
  }
558
603
  },
559
604
  async create() {
560
- var _a;
605
+ var _a, _b, _c;
561
606
  const webhookUrl = this.getNodeWebhookUrl('default');
562
607
  const webhookName = this.getNodeParameter('webhookName');
563
608
  const device = this.getNodeParameter('device', '');
@@ -575,7 +620,7 @@ class WassengerTrigger {
575
620
  const apiKey = credentials.wassengerApiKey;
576
621
  const sample = this.getNodeParameter('sampleEvent', '') || 'inbound-text';
577
622
  const url = `${webhookUrl}?workflow=${workflowId}`;
578
- const prefix = clientRuntime.replace('_', '');
623
+ const prefix = config_1.default.webhookPrefix;
579
624
  const requestBody = {
580
625
  name: `${prefix}: ${webhookName}`,
581
626
  url,
@@ -584,7 +629,7 @@ class WassengerTrigger {
584
629
  if (device) {
585
630
  requestBody.device = device;
586
631
  }
587
- const response = await (0, request_1.rawRequest)({
632
+ const response = await (0, request_1.rawRequest)(this, {
588
633
  url: '/webhooks',
589
634
  method: 'POST',
590
635
  data: requestBody,
@@ -601,17 +646,19 @@ class WassengerTrigger {
601
646
  return false;
602
647
  }
603
648
  catch (error) {
604
- if (axios_1.default.isAxiosError(error) && error.response) {
605
- if (error.response.status === 409) {
606
- return true;
607
- }
608
- throw new Error(`Wassenger webhook registration failed: ${device || "(no device)"} ${((_a = error.response.data) === null || _a === void 0 ? void 0 : _a.message) || error.message}`);
649
+ const statusCode = (0, request_1.errorStatusCode)(error);
650
+ if (statusCode === 409) {
651
+ return true;
652
+ }
653
+ if (statusCode) {
654
+ const body = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.body) !== null && _b !== void 0 ? _b : (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.data;
655
+ throw new Error(`Wassenger webhook registration failed: ${device || "(no device)"} ${(body === null || body === void 0 ? void 0 : body.message) || error.message}`);
609
656
  }
610
657
  throw error;
611
658
  }
612
659
  },
613
660
  async delete() {
614
- var _a;
661
+ var _a, _b, _c;
615
662
  const nodeData = this.getWorkflowStaticData('node');
616
663
  const webhookId = nodeData.webhookId;
617
664
  if (!webhookId) {
@@ -621,21 +668,27 @@ class WassengerTrigger {
621
668
  const credentials = await this.getCredentials('wassengerApiKey');
622
669
  const apiKey = credentials.wassengerApiKey;
623
670
  const workflowId = this.getWorkflow().id;
624
- await (0, request_1.rawRequest)({
671
+ await (0, request_1.rawRequest)(this, {
625
672
  url: `/webhooks/${webhookId}?workflow=${workflowId}`,
626
673
  method: 'DELETE',
627
674
  headers: {
628
675
  'Content-Type': 'application/json'
629
676
  },
630
677
  }, apiKey);
678
+ // The registration is gone: forget its id, or the next activation would
679
+ // check (and later delete) a row that no longer belongs to this workflow.
680
+ delete nodeData.webhookId;
631
681
  return true;
632
682
  }
633
683
  catch (error) {
634
- if (axios_1.default.isAxiosError(error) && error.response && error.response.status === 404) {
684
+ const statusCode = (0, request_1.errorStatusCode)(error);
685
+ if (statusCode === 404) {
686
+ delete nodeData.webhookId;
635
687
  return true;
636
688
  }
637
- if (axios_1.default.isAxiosError(error) && error.response) {
638
- console.error(`Wassenger webhook deletion failed: ${webhookId} ${error.response.status || '<no status>'} ${((_a = error.response.data) === null || _a === void 0 ? void 0 : _a.message) || error.message}`);
689
+ if (statusCode) {
690
+ const body = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.body) !== null && _b !== void 0 ? _b : (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.data;
691
+ console.error(`Wassenger webhook deletion failed: ${webhookId} ${statusCode} ${(body === null || body === void 0 ? void 0 : body.message) || error.message}`);
639
692
  }
640
693
  else {
641
694
  console.error('Error deleting Wassenger webhook:', webhookId, error);
@@ -648,6 +701,21 @@ class WassengerTrigger {
648
701
  }
649
702
  // This method is called when a webhook request is received
650
703
  async webhook() {
704
+ // The platform calls the URL this node registered, `?workflow=<id>` included.
705
+ // When n8n moved that path to another workflow, deliveries registered for the
706
+ // first one can land here: acknowledge them (200, so the platform neither
707
+ // retries nor flags the row) without starting an execution.
708
+ // Platform senders used to post to the registered URL plus '/', which put the
709
+ // slash after the query (`?workflow=<id>/`). Workers keep that sender until they
710
+ // restart, so that one slash is still accepted. It can go once every worker
711
+ // runs a sender that requests the registered URL as is.
712
+ const { workflow: rawWorkflow } = this.getQueryData();
713
+ const addressedTo = typeof rawWorkflow === 'string' && rawWorkflow.endsWith('/')
714
+ ? rawWorkflow.slice(0, -1)
715
+ : rawWorkflow;
716
+ if (addressedTo !== undefined && addressedTo !== this.getWorkflow().id) {
717
+ return { webhookResponse: { ignored: true } };
718
+ }
651
719
  const req = this.getRequestObject();
652
720
  const body = req.body;
653
721
  // Return the data
@@ -9,12 +9,8 @@ exports.getLabels = getLabels;
9
9
  exports.getWabaDevices = getWabaDevices;
10
10
  exports.getTemplates = getTemplates;
11
11
  const request_1 = require("../request");
12
- async function query(url, apiKey) {
13
- const opts = {
14
- url,
15
- method: 'GET'
16
- };
17
- const { data } = await (0, request_1.rawRequest)(opts, apiKey);
12
+ async function query(context, url, apiKey) {
13
+ const { data } = await (0, request_1.rawRequest)(context, { url, method: 'GET' }, apiKey);
18
14
  return data;
19
15
  }
20
16
  async function getDevices() {
@@ -24,7 +20,7 @@ async function getDevices() {
24
20
  throw new Error('No Wassenger API Key credentials found!');
25
21
  }
26
22
  const apiKey = credentials.wassengerApiKey;
27
- const data = await query(`/devices?size=100`, apiKey);
23
+ const data = await query(this, `/devices?size=100`, apiKey);
28
24
  return data.map((device) => ({
29
25
  name: `${device.alias} (${device.phone})`,
30
26
  value: device.id,
@@ -43,7 +39,7 @@ async function getGroups() {
43
39
  const target = this.getNodeParameter('target');
44
40
  const apiKey = credentials.wassengerApiKey;
45
41
  const kind = target === 'channel' ? 'channel' : 'group';
46
- const data = await query(`/devices/${device}/groups?size=500&kind=${kind}`, apiKey);
42
+ const data = await query(this, `/devices/${device}/groups?size=500&kind=${kind}`, apiKey);
47
43
  return data.filter((group) => {
48
44
  return group.isReadOnly !== true;
49
45
  }).map((group) => ({
@@ -63,7 +59,7 @@ async function getFiles() {
63
59
  if (search) {
64
60
  url += `&search=${search}`;
65
61
  }
66
- const data = await query(url, apiKey);
62
+ const data = await query(this, url, apiKey);
67
63
  return data.map((file) => ({
68
64
  name: file.filename,
69
65
  value: file.id,
@@ -80,7 +76,7 @@ async function getTeamAgents() {
80
76
  throw new Error('Please select WhatsApp device selected');
81
77
  }
82
78
  const apiKey = credentials.wassengerApiKey;
83
- const data = await query(`/devices/${device}/team?size=100`, apiKey);
79
+ const data = await query(this, `/devices/${device}/team?size=100`, apiKey);
84
80
  return data.map((agent) => ({
85
81
  name: agent.displayName + ' - ' + agent.email,
86
82
  value: agent.id,
@@ -96,7 +92,7 @@ async function getDepartments() {
96
92
  throw new Error('Please select WhatsApp device selected');
97
93
  }
98
94
  const apiKey = credentials.wassengerApiKey;
99
- const data = await query(`/devices/${device}/departments`, apiKey);
95
+ const data = await query(this, `/devices/${device}/departments`, apiKey);
100
96
  return data.map((department) => ({
101
97
  name: department.name,
102
98
  value: department.id
@@ -114,7 +110,7 @@ async function getLabels() {
114
110
  throw new Error('Please select WhatsApp device selected');
115
111
  }
116
112
  const apiKey = credentials.wassengerApiKey;
117
- const data = await query(`/devices/${device}/labels?size=100`, apiKey);
113
+ const data = await query(this, `/devices/${device}/labels?size=100`, apiKey);
118
114
  return data.filter((label) => label.scope === 'chat').map((label) => ({
119
115
  name: label.name,
120
116
  value: label.name
@@ -126,7 +122,7 @@ async function getWabaDevices() {
126
122
  throw new Error('No Wassenger API Key credentials found!');
127
123
  }
128
124
  const apiKey = credentials.wassengerApiKey;
129
- const data = await query('/devices?size=100&connector=cloud', apiKey);
125
+ const data = await query(this, '/devices?size=100&connector=cloud', apiKey);
130
126
  return data
131
127
  .filter((device) => device.waba && device.waba.id)
132
128
  .map((device) => ({
@@ -148,13 +144,13 @@ async function getTemplates() {
148
144
  // Actually, standard templates endpoint allows filtering by WABA ID, but we might just want templates for this device context.
149
145
  // The user request says: "device.waba.id must be avaialble in order to pull templates filtered by the device waba ID field"
150
146
  // So we fetch device first.
151
- const [deviceData] = await query(`/devices/${device}`, apiKey);
147
+ const [deviceData] = await query(this, `/devices/${device}`, apiKey);
152
148
  if (!deviceData || !deviceData.waba || !deviceData.waba.id) {
153
149
  return [];
154
150
  }
155
151
  const wabaId = deviceData.waba.id;
156
152
  // 2. Fetch templates
157
- const templates = await query(`/waba/templates?waba=${wabaId}&status=APPROVED&size=500`, apiKey);
153
+ const templates = await query(this, `/waba/templates?waba=${wabaId}&status=APPROVED&size=500`, apiKey);
158
154
  return templates.map((template) => ({
159
155
  name: `${template.name} (${template.language})`,
160
156
  value: template.name,
package/dist/request.js CHANGED
@@ -3,25 +3,56 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.credentialsName = void 0;
7
+ exports.errorStatusCode = errorStatusCode;
6
8
  exports.rawRequest = rawRequest;
7
9
  exports.request = request;
8
- const axios_1 = __importDefault(require("axios"));
10
+ const config_1 = __importDefault(require("./config"));
9
11
  const baseUrl = 'https://api.wassenger.com/v1';
10
- const clientRuntime = process.env.N8N_RUNTIME_CLIENT;
11
- async function rawRequest(opts, apiKey) {
12
+ exports.credentialsName = 'wassengerApiKey';
13
+ function authHeaders(apiKey, extra) {
14
+ return {
15
+ Authorization: `Bearer ${config_1.default.keyPrefix}${apiKey}`,
16
+ 'x-n8n-client': 'n8n',
17
+ ...(extra || {}),
18
+ };
19
+ }
20
+ /**
21
+ * Pulls an HTTP status code out of whatever shape n8n's request helper threw.
22
+ * The helper doesn't guarantee a single error class, so check the known fields
23
+ * rather than assuming one.
24
+ */
25
+ function errorStatusCode(error) {
26
+ var _a, _b, _c, _d, _e;
27
+ return (error === null || error === void 0 ? void 0 : error.httpCode) ? Number(error.httpCode)
28
+ : (_c = (_a = error === null || error === void 0 ? void 0 : error.statusCode) !== null && _a !== void 0 ? _a : (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status) !== null && _c !== void 0 ? _c : (_e = (_d = error === null || error === void 0 ? void 0 : error.cause) === null || _d === void 0 ? void 0 : _d.response) === null || _e === void 0 ? void 0 : _e.status;
29
+ }
30
+ /**
31
+ * Makes an unauthenticated-context request against the Wassenger API.
32
+ *
33
+ * Returns an `{ data }` envelope, matching the shape callers already expect.
34
+ */
35
+ async function rawRequest(context, opts, apiKey) {
12
36
  if (!apiKey) {
13
37
  throw new Error('API key is required');
14
38
  }
15
39
  if (!opts.url) {
16
40
  throw new Error('URL path is required');
17
41
  }
18
- opts.baseURL = baseUrl;
19
- opts.headers = opts.headers || {};
20
- Object.assign(opts.headers, {
21
- 'x-n8n-client': 'n8n',
22
- 'Authorization': `Bearer ${clientRuntime ? clientRuntime + '_' : ''}${apiKey}`,
23
- });
24
- return await (0, axios_1.default)(opts);
42
+ const options = {
43
+ url: `${baseUrl}${opts.url}`,
44
+ method: opts.method || 'GET',
45
+ headers: authHeaders(apiKey, opts.headers),
46
+ json: true,
47
+ };
48
+ if (opts.params) {
49
+ options.qs = opts.params;
50
+ }
51
+ if (opts.data !== undefined) {
52
+ options.body = opts.data;
53
+ }
54
+ const data = await context.helpers.httpRequest(options);
55
+ return { data };
25
56
  }
26
57
  /**
27
58
  * Makes an authenticated request to the Wassenger API
@@ -35,69 +66,63 @@ async function rawRequest(opts, apiKey) {
35
66
  * @returns The API response data
36
67
  */
37
68
  async function request(executeFunctions, method, endpoint, body, query, customHeaders) {
38
- var _a;
39
- const credentials = await executeFunctions.getCredentials('wassengerApiKey');
40
- const apiKey = credentials.wassengerApiKey;
69
+ var _a, _b, _c, _d, _e, _f, _g;
70
+ const credentials = await executeFunctions.getCredentials(exports.credentialsName);
71
+ const apiKey = credentials[exports.credentialsName];
41
72
  if (!apiKey) {
42
73
  throw new Error('API key is required');
43
74
  }
44
- const url = `${baseUrl}${endpoint}`;
45
- const headers = {
46
- 'Content-Type': 'application/json',
47
- 'Authorization': `Bearer ${clientRuntime ? clientRuntime + '_' : ''}${apiKey}`,
48
- 'x-n8n-client': 'n8n',
49
- ...(customHeaders || {}),
50
- };
51
- const config = {
52
- url,
75
+ const options = {
76
+ url: `${baseUrl}${endpoint}`,
53
77
  method,
54
- headers,
55
- params: query,
78
+ headers: authHeaders(apiKey, { 'Content-Type': 'application/json', ...(customHeaders || {}) }),
79
+ json: true,
56
80
  };
81
+ if (query) {
82
+ options.qs = query;
83
+ }
57
84
  if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && body) {
58
- config.data = body;
85
+ options.body = body;
59
86
  }
60
87
  try {
61
- const response = await (0, axios_1.default)(config);
62
- return response.data;
88
+ return await executeFunctions.helpers.httpRequest(options);
63
89
  }
64
90
  catch (error) {
65
- if (axios_1.default.isAxiosError(error)) {
66
- // Create a detailed error object with additional information
67
- const errorResponse = {
68
- error: error.message,
69
- statusCode: (_a = error.response) === null || _a === void 0 ? void 0 : _a.status,
70
- };
71
- if (error.response) {
72
- errorResponse.statusText = error.response.statusText;
73
- // Add response data if available
74
- if (error.response.data) {
75
- if (typeof error.response.data === 'object') {
76
- errorResponse.responseBody = error.response.data;
77
- }
78
- else {
79
- try {
80
- errorResponse.responseBody = JSON.parse(error.response.data);
81
- }
82
- catch (e) {
83
- errorResponse.responseBody = error.response.data;
84
- }
85
- }
86
- // Add any error message or code from the response
87
- if (error.response.data.error) {
88
- errorResponse.apiError = error.response.data.error;
89
- }
90
- if (error.response.data.code) {
91
- errorResponse.apiCode = error.response.data.code;
92
- }
93
- if (error.response.data.message) {
94
- errorResponse.apiMessage = error.response.data.message;
95
- }
91
+ const statusCode = errorStatusCode(error);
92
+ const responseBody = (_d = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.body) !== null && _b !== void 0 ? _b : (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.data) !== null && _d !== void 0 ? _d : (_f = (_e = error === null || error === void 0 ? void 0 : error.cause) === null || _e === void 0 ? void 0 : _e.response) === null || _f === void 0 ? void 0 : _f.body;
93
+ // Keep the detailed shape the action modules and tests already expect.
94
+ const errorResponse = {
95
+ error: error === null || error === void 0 ? void 0 : error.message,
96
+ statusCode,
97
+ };
98
+ if ((_g = error === null || error === void 0 ? void 0 : error.response) === null || _g === void 0 ? void 0 : _g.statusText) {
99
+ errorResponse.statusText = error.response.statusText;
100
+ }
101
+ if (responseBody !== undefined) {
102
+ if (typeof responseBody === 'object') {
103
+ errorResponse.responseBody = responseBody;
104
+ }
105
+ else {
106
+ try {
107
+ errorResponse.responseBody = JSON.parse(responseBody);
108
+ }
109
+ catch (e) {
110
+ errorResponse.responseBody = responseBody;
111
+ }
112
+ }
113
+ const parsed = errorResponse.responseBody;
114
+ if (parsed && typeof parsed === 'object') {
115
+ if (parsed.error) {
116
+ errorResponse.apiError = parsed.error;
117
+ }
118
+ if (parsed.code) {
119
+ errorResponse.apiCode = parsed.code;
120
+ }
121
+ if (parsed.message) {
122
+ errorResponse.apiMessage = parsed.message;
96
123
  }
97
124
  }
98
- throw new Error(`Wassenger API Error: ${JSON.stringify(errorResponse)}`);
99
125
  }
100
- // Re-throw non-axios errors
101
- throw error;
126
+ throw new Error(`Wassenger API Error: ${JSON.stringify(errorResponse)}`);
102
127
  }
103
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-wassenger",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "n8n plugin for Wassenger",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -37,16 +37,11 @@
37
37
  "build": "npm run clean && tsc && cp icon.png dist/nodes/icon.png",
38
38
  "watch": "tsc --watch",
39
39
  "lint": "eslint . --ext .ts",
40
- "prepublishOnly": "npm run build",
41
40
  "dev": "npm run build && n8n start",
42
41
  "dev:copy": "npm run build && cp -r dist/ $HOME/.n8n/nodes/node_modules/n8n-nodes-wassenger/dist && cp package.json $HOME/.n8n/nodes/node_modules/n8n-nodes-wassenger/",
43
42
  "test": "jest",
44
- "test:watch": "jest --watch"
45
- },
46
- "dependencies": {
47
- "axios": "^1.8.4",
48
- "n8n-core": "=1.89.0",
49
- "n8n-workflow": "=1.89.0"
43
+ "test:watch": "jest --watch",
44
+ "typecheck": "tsc --noEmit"
50
45
  },
51
46
  "devDependencies": {
52
47
  "@types/jest": "^29.5.1",
@@ -55,8 +50,25 @@
55
50
  "eslint-config-airbnb-base": "^14.2.1",
56
51
  "eslint-plugin-import": "^2.22.1",
57
52
  "jest": "^29.5.0",
53
+ "n8n-workflow": "=1.89.0",
58
54
  "nock": "^13.3.1",
59
55
  "ts-jest": "^29.1.0",
60
56
  "typescript": "^5.6.3"
61
- }
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/wassengerhq/n8n-wassenger.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/wassengerhq/n8n-wassenger/issues"
64
+ },
65
+ "engines": {
66
+ "node": ">=20.15"
67
+ },
68
+ "files": [
69
+ "dist",
70
+ "index.js",
71
+ "README.md",
72
+ "LICENSE"
73
+ ]
62
74
  }