n8n-nodes-linq 4.0.2 → 4.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # n8n-nodes-linq
2
2
 
3
- Community node for the Linq Partner API v3 in n8n. Use this node to manage chats, messages, attachments, phone numbers, webhooks, and contacts from your workflows.
3
+ Community node for the Linq Partner API v3 in n8n. The current implementation covers chats, messages, attachments, phone numbers, webhook subscriptions, and Linq webhooks from your workflows.
4
4
 
5
5
  **Ownership:** The Linq Partner API is owned and operated by Linq (https://linqapp.com/). This repository is community-maintained by Everyday Workflows.
6
6
 
@@ -12,14 +12,15 @@ Community node for the Linq Partner API v3 in n8n. Use this node to manage chats
12
12
 
13
13
  - Usage guide: [docs/usage.md](docs/usage.md)
14
14
  - API reference (v3): [docs/api-reference.md](docs/api-reference.md)
15
- - OpenAPI spec: [V3.yaml](V3.yaml)
15
+ - Official Linq docs: https://apidocs.linqapp.com/documentation/api-reference
16
+ - Cached OpenAPI snapshot: [V3.yaml](V3.yaml)
16
17
  - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
17
18
  - License: [LICENSE.md](LICENSE.md)
18
19
 
19
20
  ## Requirements
20
21
 
21
22
  - Node.js >= 20.15
22
- - n8n compatible with `n8n-workflow` ^1.120.7
23
+ - n8n compatible with `n8n-workflow` ^1.120.10
23
24
 
24
25
  ## Installation (in n8n)
25
26
 
@@ -54,66 +55,72 @@ Add the **Linq** node to your workflow. Operations map to Linq Partner API v3 en
54
55
 
55
56
  **Chat**
56
57
 
57
- - Get Many → `GET /v3/chats` (requires `from`, supports cursor/limit)
58
+ - Get Many → `GET /v3/chats` (supports optional `from`, `to`, cursor, and limit)
58
59
  - Get One → `GET /v3/chats/{chatId}`
59
- - Find → `GET /v3/chats` (with `from` + `phone_numbers[]`)
60
+ - Find (legacy helper) → `GET /v3/chats` using `to` plus the older `phone_numbers[]` fallback when multiple handles are supplied
60
61
  - Create → `POST /v3/chats`
62
+ - Leave → `POST /v3/chats/{chatId}/leave`
61
63
  - Share Contact → `POST /v3/chats/{chatId}/share_contact_card`
64
+ - Start Typing → `POST /v3/chats/{chatId}/typing`
65
+ - Stop Typing → `DELETE /v3/chats/{chatId}/typing`
66
+ - Mark as Read → `POST /v3/chats/{chatId}/read`
67
+ - Send Voice Memo → `POST /v3/chats/{chatId}/voicememo`
68
+
69
+ Create and send operations support:
70
+
71
+ - text parts
72
+ - direct media URLs
73
+ - pre-uploaded `attachment_id` values
74
+ - rich link URLs
75
+ - threaded replies (`reply_to`)
76
+ - iMessage effects
77
+ - preferred service selection
78
+ - idempotency keys
62
79
 
63
80
  **Chat Message**
64
81
 
65
82
  - Get Many → `GET /v3/chats/{chatId}/messages`
66
83
  - Get One → `GET /v3/messages/{messageId}`
67
84
  - Create → `POST /v3/chats/{chatId}/messages`
85
+ - Edit → `PATCH /v3/messages/{messageId}`
68
86
  - Delete → `DELETE /v3/messages/{messageId}`
69
87
  - React → `POST /v3/messages/{messageId}/reactions`
88
+ - Get Thread → `GET /v3/messages/{messageId}/thread`
70
89
 
71
90
  **Phone Number**
72
91
 
73
- - Get Many → `GET /v3/phonenumbers`
74
- - Update → `PUT /v3/phonenumbers/{phoneNumberId}`
92
+ - Get Many → `GET /v3/phone_numbers`
75
93
 
76
94
  **Webhook Subscription**
77
95
 
96
+ - Get Events → `GET /v3/webhook-events`
78
97
  - Get Many → `GET /v3/webhook-subscriptions`
79
98
  - Get One → `GET /v3/webhook-subscriptions/{subscriptionId}`
80
99
  - Create → `POST /v3/webhook-subscriptions`
81
100
  - Update → `PUT /v3/webhook-subscriptions/{subscriptionId}`
82
101
  - Delete → `DELETE /v3/webhook-subscriptions/{subscriptionId}`
83
102
 
84
- **Contact (extension)**
85
-
86
- - Create → `POST /v3/contacts`
87
- - Get One → `GET /v3/contacts/{contactId}`
88
- - Update → `PUT /v3/contacts/{contactId}`
89
- - Delete → `DELETE /v3/contacts/{contactId}`
90
-
91
- **Chat Message (extension)**
92
-
93
- - Edit → `POST /v3/messages/{messageId}/edit`
94
- - Get Reaction → `GET /v3/chat_message_reactions/{reactionId}`
95
-
96
- > **Note:** The “extension” endpoints above are currently implemented in the node but are not present in `V3.yaml`. They will be aligned once the spec is updated.
97
-
98
103
  ## Linq Trigger Node
99
104
 
100
105
  Add the **Linq Trigger** node to start workflows when Linq events occur. The trigger automatically registers a webhook with Linq when the workflow is activated.
101
106
 
102
107
  **Supported Events**
103
108
 
104
- - `message.sent`
105
- - `message.received`
106
- - `message.read`
107
- - `call.completed`
108
- - `contact.created`
109
- - `contact.updated`
110
- - `contact.deleted`
109
+ The trigger supports the current documented webhook event families, including:
110
+
111
+ - `message.*`
112
+ - `participant.*`
113
+ - `reaction.*`
114
+ - `chat.*`
115
+ - `phone_number.status_updated`
116
+ - `call.*`
111
117
 
112
118
  **Security**
113
119
 
114
120
  - The trigger verifies Linq webhooks using HMAC-SHA256
115
121
  - Uses `X-Webhook-Timestamp` + raw request body for the signature payload: `{timestamp}.{payload}`
116
122
  - Requires the **Webhook Signing Secret** from credentials
123
+ - Accepts both the current documented webhook envelope (`event_type` + `data`) and the older legacy envelope (`event` + `payload`)
117
124
 
118
125
  ## Examples
119
126
 
@@ -4,7 +4,7 @@ exports.LinqApi = void 0;
4
4
  class LinqApi {
5
5
  name = 'linqApi';
6
6
  displayName = 'Linq API';
7
- documentationUrl = 'https://apidocs.linqapp.com/reference/';
7
+ documentationUrl = 'https://apidocs.linqapp.com/documentation/getting-started';
8
8
  properties = [
9
9
  {
10
10
  displayName: 'API Token',
@@ -38,7 +38,7 @@ class LinqApi {
38
38
  test = {
39
39
  request: {
40
40
  baseURL: 'https://api.linqapp.com/api/partner',
41
- url: '/v3/phonenumbers',
41
+ url: '/v3/phone_numbers',
42
42
  method: 'GET',
43
43
  },
44
44
  };
@@ -7,6 +7,86 @@ function formatPhoneNumber(phoneNumber) {
7
7
  // Only remove space characters; do not modify digits/symbols or add prefixes
8
8
  return phoneNumber.trim().replaceAll(' ', '');
9
9
  }
10
+ function splitCommaSeparated(value) {
11
+ return value
12
+ .split(',')
13
+ .map(item => item.trim())
14
+ .filter(Boolean);
15
+ }
16
+ function parseHandles(value) {
17
+ return splitCommaSeparated(value).map(formatPhoneNumber);
18
+ }
19
+ function buildMessageParts(text, attachmentUrlsString, attachmentIdsString, linkUrl) {
20
+ const attachmentUrls = splitCommaSeparated(attachmentUrlsString).map(url => {
21
+ if (!isValidUrl(url)) {
22
+ throw new n8n_workflow_1.ApplicationError(`Invalid attachment URL: "${url}". URLs must be valid HTTP/HTTPS URLs.`);
23
+ }
24
+ return url;
25
+ });
26
+ const attachmentIds = splitCommaSeparated(attachmentIdsString);
27
+ const trimmedLinkUrl = linkUrl.trim();
28
+ if (trimmedLinkUrl) {
29
+ if (!isValidUrl(trimmedLinkUrl)) {
30
+ throw new n8n_workflow_1.ApplicationError(`Invalid link URL: "${trimmedLinkUrl}". URLs must be valid HTTP/HTTPS URLs.`);
31
+ }
32
+ if (text || attachmentUrls.length > 0 || attachmentIds.length > 0) {
33
+ throw new n8n_workflow_1.ApplicationError('Link URL must be the only message part. Remove text, attachment URLs, and attachment IDs before sending a rich link.');
34
+ }
35
+ return [{ type: 'link', value: trimmedLinkUrl }];
36
+ }
37
+ const parts = [];
38
+ if (text) {
39
+ parts.push({ type: 'text', value: text });
40
+ }
41
+ attachmentUrls.forEach(url => {
42
+ parts.push({ type: 'media', url });
43
+ });
44
+ attachmentIds.forEach(attachmentId => {
45
+ parts.push({ type: 'media', attachment_id: attachmentId });
46
+ });
47
+ if (!parts.length) {
48
+ throw new n8n_workflow_1.ApplicationError('A message requires text, attachment URLs, attachment IDs, or a link URL.');
49
+ }
50
+ return parts;
51
+ }
52
+ function buildMessageContent(context, i) {
53
+ const messageText = context.getNodeParameter('messageText', i, '');
54
+ const attachmentUrls = context.getNodeParameter('attachmentUrls', i, '');
55
+ const attachmentIds = context.getNodeParameter('attachmentIds', i, '');
56
+ const linkUrl = context.getNodeParameter('linkUrl', i, '');
57
+ const idempotencyKey = context.getNodeParameter('idempotencyKey', i, '');
58
+ const preferredService = context.getNodeParameter('preferredService', i, '');
59
+ const effectType = context.getNodeParameter('effectType', i, '');
60
+ const effectName = context.getNodeParameter('effectName', i, '');
61
+ const replyToMessageId = context.getNodeParameter('replyToMessageId', i, '');
62
+ const replyToPartIndex = context.getNodeParameter('replyToPartIndex', i, 0);
63
+ const content = {
64
+ parts: buildMessageParts(messageText, attachmentUrls, attachmentIds, linkUrl),
65
+ };
66
+ if (idempotencyKey) {
67
+ content.idempotency_key = idempotencyKey;
68
+ }
69
+ if (preferredService) {
70
+ content.preferred_service = preferredService;
71
+ }
72
+ if (effectType || effectName) {
73
+ if (!effectType || !effectName) {
74
+ throw new n8n_workflow_1.ApplicationError('Both Effect Type and Effect Name are required when sending a message effect.');
75
+ }
76
+ content.effect = {
77
+ type: effectType,
78
+ name: effectName,
79
+ };
80
+ }
81
+ if (replyToMessageId) {
82
+ const replyTo = { message_id: replyToMessageId };
83
+ if (replyToPartIndex > 0) {
84
+ replyTo.part_index = replyToPartIndex;
85
+ }
86
+ content.reply_to = replyTo;
87
+ }
88
+ return content;
89
+ }
10
90
  class Linq {
11
91
  description = {
12
92
  displayName: 'Linq',
@@ -84,6 +164,11 @@ class Linq {
84
164
  value: 'getOne',
85
165
  action: 'Get one chat',
86
166
  },
167
+ {
168
+ name: 'Leave',
169
+ value: 'leave',
170
+ action: 'Leave a group chat',
171
+ },
87
172
  {
88
173
  name: 'Mark as Read',
89
174
  value: 'markAsRead',
@@ -141,15 +226,20 @@ class Linq {
141
226
  action: 'Delete a chat message',
142
227
  },
143
228
  {
144
- name: 'Get',
145
- value: 'get',
146
- action: 'Get a chat message',
229
+ name: 'Edit',
230
+ value: 'edit',
231
+ action: 'Edit a chat message',
147
232
  },
148
233
  {
149
234
  name: 'Get Many',
150
235
  value: 'getAll',
151
236
  action: 'Get many chat messages',
152
237
  },
238
+ {
239
+ name: 'Get One',
240
+ value: 'get',
241
+ action: 'Get a chat message',
242
+ },
153
243
  {
154
244
  name: 'Get Thread',
155
245
  value: 'getThread',
@@ -197,6 +287,11 @@ class Linq {
197
287
  value: 'delete',
198
288
  action: 'Delete a webhook subscription',
199
289
  },
290
+ {
291
+ name: 'Get Events',
292
+ value: 'getEvents',
293
+ action: 'Get available webhook events',
294
+ },
200
295
  {
201
296
  name: 'Get Many',
202
297
  value: 'getAll',
@@ -251,6 +346,7 @@ class Linq {
251
346
  resource: ['chat'],
252
347
  operation: [
253
348
  'getOne',
349
+ 'leave',
254
350
  'shareContact',
255
351
  'update',
256
352
  'addParticipant',
@@ -271,7 +367,15 @@ class Linq {
271
367
  type: 'string',
272
368
  displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
273
369
  default: '',
274
- description: 'Required: your Linq phone number (from)',
370
+ description: 'Optional: filter chats by one of your Linq phone numbers (from)',
371
+ },
372
+ {
373
+ displayName: 'Participant Handle',
374
+ name: 'participantHandle',
375
+ type: 'string',
376
+ displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
377
+ default: '',
378
+ description: 'Optional: filter chats by a participant phone number or email address',
275
379
  },
276
380
  {
277
381
  displayName: 'From Phone Number',
@@ -293,7 +397,9 @@ class Linq {
293
397
  displayName: 'Cursor',
294
398
  name: 'cursor',
295
399
  type: 'string',
296
- displayOptions: { show: { resource: ['chat', 'chatMessage'], operation: ['getAll'] } },
400
+ displayOptions: {
401
+ show: { resource: ['chat', 'chatMessage'], operation: ['getAll', 'getThread'] },
402
+ },
297
403
  default: '',
298
404
  description: 'Cursor for pagination',
299
405
  },
@@ -304,7 +410,9 @@ class Linq {
304
410
  typeOptions: {
305
411
  minValue: 1,
306
412
  },
307
- displayOptions: { show: { resource: ['chat', 'chatMessage'], operation: ['getAll'] } },
413
+ displayOptions: {
414
+ show: { resource: ['chat', 'chatMessage'], operation: ['getAll', 'getThread'] },
415
+ },
308
416
  default: 50,
309
417
  description: 'Max number of results to return',
310
418
  },
@@ -355,7 +463,89 @@ class Linq {
355
463
  type: 'string',
356
464
  displayOptions: { show: { resource: ['chat'], operation: ['create'] } },
357
465
  default: '',
358
- description: 'Comma-separated list of attachment URLs',
466
+ description: 'Comma-separated list of attachment URLs to send directly as media parts',
467
+ },
468
+ {
469
+ displayName: 'Attachment IDs',
470
+ name: 'attachmentIds',
471
+ type: 'string',
472
+ displayOptions: {
473
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
474
+ },
475
+ default: '',
476
+ description: 'Comma-separated list of pre-uploaded Linq attachment IDs to send as media parts',
477
+ },
478
+ {
479
+ displayName: 'Link URL',
480
+ name: 'linkUrl',
481
+ type: 'string',
482
+ displayOptions: {
483
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
484
+ },
485
+ default: '',
486
+ description: 'Optional rich link preview URL. Must be the only message part when provided.',
487
+ },
488
+ {
489
+ displayName: 'Preferred Service',
490
+ name: 'preferredService',
491
+ type: 'options',
492
+ displayOptions: {
493
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
494
+ },
495
+ options: [
496
+ { name: 'Default', value: '' },
497
+ { name: 'iMessage', value: 'iMessage' },
498
+ { name: 'RCS', value: 'RCS' },
499
+ { name: 'SMS', value: 'SMS' },
500
+ ],
501
+ default: '',
502
+ description: 'Optional preferred messaging service for the message',
503
+ },
504
+ {
505
+ displayName: 'Effect Type',
506
+ name: 'effectType',
507
+ type: 'options',
508
+ displayOptions: {
509
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
510
+ },
511
+ options: [
512
+ { name: 'None', value: '' },
513
+ { name: 'Screen', value: 'screen' },
514
+ { name: 'Bubble', value: 'bubble' },
515
+ ],
516
+ default: '',
517
+ description: 'Optional iMessage effect type',
518
+ },
519
+ {
520
+ displayName: 'Effect Name',
521
+ name: 'effectName',
522
+ type: 'string',
523
+ displayOptions: {
524
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
525
+ },
526
+ default: '',
527
+ description: 'Optional effect name such as confetti, fireworks, slam, loud, gentle, or invisible',
528
+ },
529
+ {
530
+ displayName: 'Reply To Message ID',
531
+ name: 'replyToMessageId',
532
+ type: 'string',
533
+ displayOptions: {
534
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
535
+ },
536
+ default: '',
537
+ description: 'Optional message ID to reply to',
538
+ },
539
+ {
540
+ displayName: 'Reply To Part Index',
541
+ name: 'replyToPartIndex',
542
+ type: 'number',
543
+ typeOptions: { minValue: 0 },
544
+ displayOptions: {
545
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
546
+ },
547
+ default: 0,
548
+ description: 'Optional part index to reply to when sending a threaded reply',
359
549
  },
360
550
  // Update Chat parameters
361
551
  {
@@ -394,6 +584,14 @@ class Linq {
394
584
  default: '',
395
585
  description: 'The URL of the voice memo to send',
396
586
  },
587
+ {
588
+ displayName: 'Voice Memo Attachment ID',
589
+ name: 'voiceMemoAttachmentId',
590
+ type: 'string',
591
+ displayOptions: { show: { resource: ['chat'], operation: ['sendVoiceMemo'] } },
592
+ default: '',
593
+ description: 'Optional pre-uploaded Linq attachment ID for the voice memo',
594
+ },
397
595
  // Chat Message parameters
398
596
  {
399
597
  displayName: 'Chat Message ID',
@@ -402,7 +600,7 @@ class Linq {
402
600
  displayOptions: {
403
601
  show: {
404
602
  resource: ['chatMessage'],
405
- operation: ['getOne', 'delete', 'react', 'getThread'],
603
+ operation: ['get', 'edit', 'delete', 'react', 'getThread'],
406
604
  },
407
605
  },
408
606
  default: '',
@@ -438,17 +636,70 @@ class Linq {
438
636
  displayName: 'Idempotency Key',
439
637
  name: 'idempotencyKey',
440
638
  type: 'string',
441
- displayOptions: { show: { resource: ['chatMessage'], operation: ['create'] } },
639
+ displayOptions: {
640
+ show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
641
+ },
442
642
  default: '',
443
643
  description: 'Optional idempotency key for message creation',
444
644
  },
445
645
  {
446
- displayName: 'Reaction',
646
+ displayName: 'Edit Text',
647
+ name: 'editText',
648
+ type: 'string',
649
+ displayOptions: { show: { resource: ['chatMessage'], operation: ['edit'] } },
650
+ default: '',
651
+ description: 'The new text to apply to the selected message part',
652
+ },
653
+ {
654
+ displayName: 'Reaction Type',
447
655
  name: 'reaction',
448
656
  type: 'string',
449
657
  displayOptions: { show: { resource: ['chatMessage'], operation: ['react'] } },
450
658
  default: '',
451
- description: 'The reaction to add',
659
+ description: 'The reaction type to add or remove (for example love, like, custom)',
660
+ },
661
+ {
662
+ displayName: 'Reaction Operation',
663
+ name: 'reactionOperation',
664
+ type: 'options',
665
+ displayOptions: { show: { resource: ['chatMessage'], operation: ['react'] } },
666
+ options: [
667
+ { name: 'Add', value: 'add' },
668
+ { name: 'Remove', value: 'remove' },
669
+ ],
670
+ default: 'add',
671
+ description: 'Whether to add or remove the reaction',
672
+ },
673
+ {
674
+ displayName: 'Custom Emoji',
675
+ name: 'customEmoji',
676
+ type: 'string',
677
+ displayOptions: { show: { resource: ['chatMessage'], operation: ['react'] } },
678
+ default: '',
679
+ description: 'Optional custom emoji value when reaction type is custom',
680
+ },
681
+ {
682
+ displayName: 'Message Part Index',
683
+ name: 'messagePartIndex',
684
+ type: 'number',
685
+ typeOptions: { minValue: 0 },
686
+ displayOptions: {
687
+ show: { resource: ['chatMessage'], operation: ['edit', 'react'] },
688
+ },
689
+ default: 0,
690
+ description: 'Optional message part index for editing or reacting to a specific part',
691
+ },
692
+ {
693
+ displayName: 'Order',
694
+ name: 'threadOrder',
695
+ type: 'options',
696
+ displayOptions: { show: { resource: ['chatMessage'], operation: ['getThread'] } },
697
+ options: [
698
+ { name: 'Ascending', value: 'asc' },
699
+ { name: 'Descending', value: 'desc' },
700
+ ],
701
+ default: 'asc',
702
+ description: 'Sort order for thread messages',
452
703
  },
453
704
  // Webhook Subscription parameters
454
705
  {
@@ -481,15 +732,35 @@ class Linq {
481
732
  default: '',
482
733
  description: 'Comma-separated list of events',
483
734
  },
735
+ {
736
+ displayName: 'Phone Numbers',
737
+ name: 'webhookPhoneNumbers',
738
+ type: 'string',
739
+ displayOptions: {
740
+ show: { resource: ['webhookSubscription'], operation: ['create', 'update'] },
741
+ },
742
+ default: '',
743
+ description: 'Optional comma-separated list of phone numbers to filter webhook deliveries for',
744
+ },
484
745
  {
485
746
  displayName: 'Active',
486
747
  name: 'active',
487
748
  type: 'boolean',
488
749
  displayOptions: {
489
- show: { resource: ['webhookSubscription'], operation: ['create', 'update'] },
750
+ show: { resource: ['webhookSubscription'], operation: ['update'] },
490
751
  },
491
752
  default: true,
492
- description: 'Whether the webhook is active',
753
+ description: 'Whether the webhook subscription is active',
754
+ },
755
+ {
756
+ displayName: 'Clear Phone Number Filter',
757
+ name: 'clearWebhookPhoneNumbers',
758
+ type: 'boolean',
759
+ displayOptions: {
760
+ show: { resource: ['webhookSubscription'], operation: ['update'] },
761
+ },
762
+ default: false,
763
+ description: 'Whether to remove any existing phone number filter on the subscription',
493
764
  },
494
765
  // Attachment parameters
495
766
  {
@@ -530,7 +801,7 @@ class Linq {
530
801
  type: 'string',
531
802
  displayOptions: { show: { resource: ['attachment'], operation: ['updateStatus'] } },
532
803
  default: '',
533
- description: 'The status of the attachment (e.g., uploaded, failed)',
804
+ description: 'The status of the attachment (pending, complete, or failed)',
534
805
  },
535
806
  ],
536
807
  };
@@ -577,35 +848,6 @@ class Linq {
577
848
  }
578
849
  }
579
850
  exports.Linq = Linq;
580
- async function uploadAttachment(context, url) {
581
- const response = await context.helpers.httpRequest({
582
- method: 'GET',
583
- url,
584
- encoding: 'arraybuffer',
585
- });
586
- const buffer = Buffer.from(response);
587
- const contentType = 'application/octet-stream';
588
- const uploadConfig = await context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
589
- method: 'POST',
590
- url: 'https://api.linqapp.com/api/partner/v3/attachments',
591
- headers: {
592
- 'Content-Type': 'application/json',
593
- Accept: 'application/json',
594
- },
595
- body: {
596
- content_type: contentType,
597
- },
598
- });
599
- await context.helpers.httpRequest({
600
- method: 'PUT',
601
- url: uploadConfig.upload_url,
602
- headers: {
603
- 'Content-Type': contentType,
604
- },
605
- body: buffer,
606
- });
607
- return uploadConfig.id;
608
- }
609
851
  function isValidUrl(urlString) {
610
852
  try {
611
853
  const url = new URL(urlString);
@@ -615,44 +857,16 @@ function isValidUrl(urlString) {
615
857
  return false;
616
858
  }
617
859
  }
618
- async function createParts(context, text, attachmentUrlsString) {
619
- const parts = [];
620
- if (text) {
621
- parts.push({ type: 'text', value: text });
622
- }
623
- if (attachmentUrlsString) {
624
- const urls = attachmentUrlsString
625
- .split(',')
626
- .map(u => u.trim())
627
- .filter(Boolean)
628
- .filter(u => {
629
- if (!isValidUrl(u)) {
630
- throw new n8n_workflow_1.ApplicationError(`Invalid attachment URL: "${u}". URLs must be valid HTTP/HTTPS URLs.`);
631
- }
632
- return true;
633
- });
634
- // Upload attachments in batches to limit concurrency and avoid OOM
635
- const CONCURRENCY_LIMIT = 3;
636
- const attachmentIds = [];
637
- for (let i = 0; i < urls.length; i += CONCURRENCY_LIMIT) {
638
- const chunk = urls.slice(i, i + CONCURRENCY_LIMIT);
639
- const chunkResults = await Promise.all(chunk.map(url => uploadAttachment(context, url)));
640
- attachmentIds.push(...chunkResults);
641
- }
642
- attachmentIds.forEach(id => {
643
- parts.push({ type: 'media', attachment_id: id });
644
- });
645
- }
646
- return parts;
647
- }
648
860
  async function chatGetAll(context, i) {
649
861
  const phoneNumber = context.getNodeParameter('phoneNumber', i, '');
862
+ const participantHandle = context.getNodeParameter('participantHandle', i, '');
650
863
  const cursor = context.getNodeParameter('cursor', i, '');
651
864
  const limit = context.getNodeParameter('limit', i);
652
- if (!phoneNumber) {
653
- throw new n8n_workflow_1.ApplicationError('phone_number is required by Linq API');
654
- }
655
- const qs = { from: formatPhoneNumber(phoneNumber) };
865
+ const qs = {};
866
+ if (phoneNumber)
867
+ qs.from = formatPhoneNumber(phoneNumber);
868
+ if (participantHandle)
869
+ qs.to = formatPhoneNumber(participantHandle);
656
870
  if (cursor)
657
871
  qs.cursor = cursor;
658
872
  if (limit && limit !== 50)
@@ -673,12 +887,19 @@ async function chatGetOne(context, i) {
673
887
  async function chatFind(context, i) {
674
888
  const fromPhoneNumber = context.getNodeParameter('fromPhoneNumber', i, '');
675
889
  const phoneNumbers = context.getNodeParameter('phoneNumbers', i, '');
676
- if (!fromPhoneNumber) {
677
- throw new n8n_workflow_1.ApplicationError('phone_number is required when finding chats');
890
+ const handles = parseHandles(phoneNumbers);
891
+ if (!fromPhoneNumber && handles.length === 0) {
892
+ throw new n8n_workflow_1.ApplicationError('Provide a from phone number or at least one participant handle');
893
+ }
894
+ const qs = {};
895
+ if (fromPhoneNumber) {
896
+ qs.from = formatPhoneNumber(fromPhoneNumber);
897
+ }
898
+ if (handles.length === 1) {
899
+ qs.to = handles[0];
678
900
  }
679
- const qs = { from: formatPhoneNumber(fromPhoneNumber) };
680
- if (phoneNumbers) {
681
- qs['phone_numbers[]'] = phoneNumbers.split(',').map(p => formatPhoneNumber(p.trim()));
901
+ else if (handles.length > 1) {
902
+ qs['phone_numbers[]'] = handles;
682
903
  }
683
904
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
684
905
  method: 'GET',
@@ -690,27 +911,44 @@ async function chatCreate(context, i) {
690
911
  const sendFrom = context.getNodeParameter('sendFrom', i);
691
912
  const displayName = context.getNodeParameter('displayName', i);
692
913
  const phoneNumbers = context.getNodeParameter('phoneNumbers', i);
693
- const messageText = context.getNodeParameter('messageText', i);
694
- const attachmentUrls = context.getNodeParameter('attachmentUrls', i, '');
695
914
  if (!sendFrom) {
696
915
  throw new n8n_workflow_1.ApplicationError('send_from is required by Linq when creating chats');
697
916
  }
698
- const parts = await createParts(context, messageText, attachmentUrls);
917
+ const recipients = parseHandles(phoneNumbers);
918
+ if (!recipients.length) {
919
+ throw new n8n_workflow_1.ApplicationError('At least one recipient phone number or email address is required');
920
+ }
699
921
  const body = {
700
922
  from: formatPhoneNumber(sendFrom),
701
- to: phoneNumbers.split(',').map(p => formatPhoneNumber(p.trim())),
702
- message: { parts },
923
+ to: recipients,
924
+ message: buildMessageContent(context, i),
703
925
  };
704
- if (displayName)
705
- body.display_name = displayName;
706
- return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
926
+ const response = (await context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
707
927
  method: 'POST',
708
928
  url: 'https://api.linqapp.com/api/partner/v3/chats',
709
929
  headers: {
710
930
  'Content-Type': 'application/json',
711
931
  },
712
932
  body,
713
- });
933
+ }));
934
+ if (displayName) {
935
+ const createdChat = response.chat;
936
+ const createdChatId = createdChat?.id;
937
+ if (!createdChatId) {
938
+ throw new n8n_workflow_1.ApplicationError('Linq created the chat but did not return a chat ID for the follow-up display name update.');
939
+ }
940
+ await context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
941
+ method: 'PUT',
942
+ url: `https://api.linqapp.com/api/partner/v3/chats/${createdChatId}`,
943
+ headers: {
944
+ 'Content-Type': 'application/json',
945
+ },
946
+ body: {
947
+ display_name: displayName,
948
+ },
949
+ });
950
+ }
951
+ return response;
714
952
  }
715
953
  async function chatShareContact(context, i) {
716
954
  const chatId = context.getNodeParameter('chatId', i);
@@ -747,6 +985,16 @@ async function chatUpdate(context, i) {
747
985
  json: true,
748
986
  });
749
987
  }
988
+ async function chatLeave(context, i) {
989
+ const chatId = context.getNodeParameter('chatId', i);
990
+ if (!chatId) {
991
+ throw new n8n_workflow_1.ApplicationError('chatId is required to leave a chat');
992
+ }
993
+ return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
994
+ method: 'POST',
995
+ url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/leave`,
996
+ });
997
+ }
750
998
  async function chatAddParticipant(context, i) {
751
999
  const chatId = context.getNodeParameter('chatId', i);
752
1000
  const handle = context.getNodeParameter('handle', i);
@@ -817,12 +1065,26 @@ async function chatMarkAsRead(context, i) {
817
1065
  }
818
1066
  async function chatSendVoiceMemo(context, i) {
819
1067
  const chatId = context.getNodeParameter('chatId', i);
820
- const voiceMemoUrl = context.getNodeParameter('voiceMemoUrl', i);
1068
+ const voiceMemoUrl = context.getNodeParameter('voiceMemoUrl', i, '');
1069
+ const voiceMemoAttachmentId = context.getNodeParameter('voiceMemoAttachmentId', i, '');
821
1070
  if (!chatId) {
822
1071
  throw new n8n_workflow_1.ApplicationError('chatId is required to send a voice memo');
823
1072
  }
824
- if (!voiceMemoUrl) {
825
- throw new n8n_workflow_1.ApplicationError('voiceMemoUrl is required to send a voice memo');
1073
+ if (!voiceMemoUrl && !voiceMemoAttachmentId) {
1074
+ throw new n8n_workflow_1.ApplicationError('Either Voice Memo URL or Voice Memo Attachment ID is required to send a voice memo');
1075
+ }
1076
+ if (voiceMemoUrl && voiceMemoAttachmentId) {
1077
+ throw new n8n_workflow_1.ApplicationError('Provide either Voice Memo URL or Voice Memo Attachment ID, but not both.');
1078
+ }
1079
+ const body = {};
1080
+ if (voiceMemoUrl) {
1081
+ if (!isValidUrl(voiceMemoUrl)) {
1082
+ throw new n8n_workflow_1.ApplicationError('Voice Memo URL must be a valid HTTP/HTTPS URL');
1083
+ }
1084
+ body.voice_memo_url = voiceMemoUrl;
1085
+ }
1086
+ if (voiceMemoAttachmentId) {
1087
+ body.attachment_id = voiceMemoAttachmentId;
826
1088
  }
827
1089
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
828
1090
  method: 'POST',
@@ -830,7 +1092,7 @@ async function chatSendVoiceMemo(context, i) {
830
1092
  headers: {
831
1093
  'Content-Type': 'application/json',
832
1094
  },
833
- body: { voice_memo_url: voiceMemoUrl },
1095
+ body,
834
1096
  json: true,
835
1097
  });
836
1098
  }
@@ -864,8 +1126,8 @@ async function attachmentRequestUpload(context, i) {
864
1126
  'Content-Type': 'application/json',
865
1127
  },
866
1128
  body: {
867
- file_name: fileName,
868
- file_size: fileSize,
1129
+ filename: fileName,
1130
+ size_bytes: fileSize,
869
1131
  content_type: contentType,
870
1132
  },
871
1133
  json: true,
@@ -904,6 +1166,8 @@ async function handleChatOperation(context, i, operation) {
904
1166
  return chatShareContact(context, i);
905
1167
  case 'update':
906
1168
  return chatUpdate(context, i);
1169
+ case 'leave':
1170
+ return chatLeave(context, i);
907
1171
  case 'addParticipant':
908
1172
  return chatAddParticipant(context, i);
909
1173
  case 'removeParticipant':
@@ -947,17 +1211,10 @@ async function chatMessageGetOne(context, i) {
947
1211
  }
948
1212
  async function chatMessageCreate(context, i) {
949
1213
  const chatId = context.getNodeParameter('chatId', i);
950
- const messageText = context.getNodeParameter('messageText', i);
951
- const attachmentUrls = context.getNodeParameter('attachmentUrls', i, '');
952
- const idempotencyKey = context.getNodeParameter('idempotencyKey', i, '');
953
1214
  if (!chatId) {
954
1215
  throw new n8n_workflow_1.ApplicationError('chatId is required to create a chat message');
955
1216
  }
956
- const parts = await createParts(context, messageText, attachmentUrls);
957
- const body = { message: { parts } };
958
- if (idempotencyKey) {
959
- body.message.idempotency_key = idempotencyKey;
960
- }
1217
+ const body = { message: buildMessageContent(context, i) };
961
1218
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
962
1219
  method: 'POST',
963
1220
  url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/messages`,
@@ -989,40 +1246,85 @@ async function chatMessageDelete(context, i) {
989
1246
  async function chatMessageReact(context, i) {
990
1247
  const chatMessageId = context.getNodeParameter('chatMessageId', i);
991
1248
  const reaction = context.getNodeParameter('reaction', i);
1249
+ const reactionOperation = context.getNodeParameter('reactionOperation', i, 'add');
1250
+ const customEmoji = context.getNodeParameter('customEmoji', i, '');
1251
+ const messagePartIndex = context.getNodeParameter('messagePartIndex', i, 0);
992
1252
  if (!chatMessageId) {
993
1253
  throw new n8n_workflow_1.ApplicationError('chatMessageId is required to react to a message');
994
1254
  }
995
1255
  if (!reaction) {
996
1256
  throw new n8n_workflow_1.ApplicationError('reaction is required to react to a message');
997
1257
  }
1258
+ const body = { operation: reactionOperation, type: reaction };
1259
+ if (customEmoji) {
1260
+ body.custom_emoji = customEmoji;
1261
+ }
1262
+ if (messagePartIndex > 0) {
1263
+ body.part_index = messagePartIndex;
1264
+ }
998
1265
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
999
1266
  method: 'POST',
1000
1267
  url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}/reactions`,
1001
1268
  headers: {
1002
1269
  'Content-Type': 'application/json',
1003
1270
  },
1004
- body: { operation: 'add', type: reaction },
1271
+ body,
1272
+ json: true,
1273
+ });
1274
+ }
1275
+ async function chatMessageEdit(context, i) {
1276
+ const chatMessageId = context.getNodeParameter('chatMessageId', i);
1277
+ const editText = context.getNodeParameter('editText', i);
1278
+ const messagePartIndex = context.getNodeParameter('messagePartIndex', i, 0);
1279
+ if (!chatMessageId) {
1280
+ throw new n8n_workflow_1.ApplicationError('chatMessageId is required to edit a message');
1281
+ }
1282
+ if (!editText) {
1283
+ throw new n8n_workflow_1.ApplicationError('editText is required to edit a message');
1284
+ }
1285
+ return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
1286
+ method: 'PATCH',
1287
+ url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}`,
1288
+ headers: {
1289
+ 'Content-Type': 'application/json',
1290
+ },
1291
+ body: {
1292
+ part_index: messagePartIndex,
1293
+ text: editText,
1294
+ },
1005
1295
  json: true,
1006
1296
  });
1007
1297
  }
1008
1298
  async function chatMessageGetThread(context, i) {
1009
1299
  const chatMessageId = context.getNodeParameter('chatMessageId', i);
1300
+ const cursor = context.getNodeParameter('cursor', i, '');
1301
+ const limit = context.getNodeParameter('limit', i, 50);
1302
+ const threadOrder = context.getNodeParameter('threadOrder', i, 'asc');
1010
1303
  if (!chatMessageId) {
1011
1304
  throw new n8n_workflow_1.ApplicationError('chatMessageId is required to get message thread');
1012
1305
  }
1306
+ const qs = { order: threadOrder };
1307
+ if (cursor)
1308
+ qs.cursor = cursor;
1309
+ if (limit && limit !== 50)
1310
+ qs.limit = limit;
1013
1311
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
1014
1312
  method: 'GET',
1015
1313
  url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}/thread`,
1314
+ qs,
1016
1315
  });
1017
1316
  }
1018
1317
  async function handleChatMessageOperation(context, i, operation) {
1019
1318
  switch (operation) {
1020
1319
  case 'getAll':
1021
1320
  return chatMessageGetAll(context, i);
1321
+ case 'getOne':
1022
1322
  case 'get':
1023
1323
  return chatMessageGetOne(context, i);
1024
1324
  case 'create':
1025
1325
  return chatMessageCreate(context, i);
1326
+ case 'edit':
1327
+ return chatMessageEdit(context, i);
1026
1328
  case 'delete':
1027
1329
  return chatMessageDelete(context, i);
1028
1330
  case 'react':
@@ -1036,7 +1338,7 @@ async function handleChatMessageOperation(context, i, operation) {
1036
1338
  async function phoneNumberGetAll(context) {
1037
1339
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
1038
1340
  method: 'GET',
1039
- url: 'https://api.linqapp.com/api/partner/v3/phonenumbers',
1341
+ url: 'https://api.linqapp.com/api/partner/v3/phone_numbers',
1040
1342
  });
1041
1343
  }
1042
1344
  async function handlePhoneNumberOperation(context, _i, operation) {
@@ -1053,6 +1355,12 @@ async function webhookSubscriptionGetAll(context) {
1053
1355
  url: 'https://api.linqapp.com/api/partner/v3/webhook-subscriptions',
1054
1356
  });
1055
1357
  }
1358
+ async function webhookSubscriptionGetEvents(context) {
1359
+ return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
1360
+ method: 'GET',
1361
+ url: 'https://api.linqapp.com/api/partner/v3/webhook-events',
1362
+ });
1363
+ }
1056
1364
  async function webhookSubscriptionGetOne(context, i) {
1057
1365
  const webhookSubscriptionId = context.getNodeParameter('webhookSubscriptionId', i);
1058
1366
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
@@ -1063,13 +1371,17 @@ async function webhookSubscriptionGetOne(context, i) {
1063
1371
  async function webhookSubscriptionCreate(context, i) {
1064
1372
  const webhookUrl = context.getNodeParameter('webhookUrl', i);
1065
1373
  const events = context.getNodeParameter('events', i);
1066
- const active = context.getNodeParameter('active', i);
1374
+ const phoneNumbers = parseHandles(context.getNodeParameter('webhookPhoneNumbers', i, ''));
1375
+ const subscribedEvents = splitCommaSeparated(events);
1376
+ if (!subscribedEvents.length) {
1377
+ throw new n8n_workflow_1.ApplicationError('At least one webhook event is required when creating a subscription');
1378
+ }
1067
1379
  const body = {
1068
1380
  target_url: webhookUrl,
1069
- active: active,
1381
+ subscribed_events: subscribedEvents,
1070
1382
  };
1071
- if (events) {
1072
- body.subscribed_events = events.split(',').map(e => e.trim());
1383
+ if (phoneNumbers.length) {
1384
+ body.phone_numbers = phoneNumbers;
1073
1385
  }
1074
1386
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
1075
1387
  method: 'POST',
@@ -1083,13 +1395,21 @@ async function webhookSubscriptionCreate(context, i) {
1083
1395
  async function webhookSubscriptionUpdate(context, i) {
1084
1396
  const webhookSubscriptionId = context.getNodeParameter('webhookSubscriptionId', i);
1085
1397
  const webhookUrl = context.getNodeParameter('webhookUrl', i);
1086
- const events = context.getNodeParameter('events', i);
1398
+ const events = context.getNodeParameter('events', i, '');
1087
1399
  const active = context.getNodeParameter('active', i);
1088
- const body = { active };
1400
+ const clearWebhookPhoneNumbers = context.getNodeParameter('clearWebhookPhoneNumbers', i, false);
1401
+ const phoneNumbers = parseHandles(context.getNodeParameter('webhookPhoneNumbers', i, ''));
1402
+ const body = { is_active: active };
1089
1403
  if (webhookUrl)
1090
1404
  body.target_url = webhookUrl;
1091
1405
  if (events) {
1092
- body.subscribed_events = events.split(',').map(e => e.trim());
1406
+ body.subscribed_events = splitCommaSeparated(events);
1407
+ }
1408
+ if (clearWebhookPhoneNumbers) {
1409
+ body.phone_numbers = [];
1410
+ }
1411
+ else if (phoneNumbers.length) {
1412
+ body.phone_numbers = phoneNumbers;
1093
1413
  }
1094
1414
  return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
1095
1415
  method: 'PUT',
@@ -1109,6 +1429,8 @@ async function webhookSubscriptionDelete(context, i) {
1109
1429
  }
1110
1430
  async function handleWebhookSubscriptionOperation(context, i, operation) {
1111
1431
  switch (operation) {
1432
+ case 'getEvents':
1433
+ return webhookSubscriptionGetEvents(context);
1112
1434
  case 'getAll':
1113
1435
  return webhookSubscriptionGetAll(context);
1114
1436
  case 'getOne':
@@ -1,4 +1,7 @@
1
- import { IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
1
+ import { IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData, IDataObject } from 'n8n-workflow';
2
+ export declare function extractSignatureHex(signatureHeader: string): string;
3
+ export declare function extractIncomingEvent(bodyData: IDataObject, headers: IDataObject): string | undefined;
4
+ export declare function extractWebhookPayload(bodyData: IDataObject): IDataObject;
2
5
  export declare class LinqTrigger implements INodeType {
3
6
  description: INodeTypeDescription;
4
7
  webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
@@ -34,8 +34,32 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LinqTrigger = void 0;
37
+ exports.extractSignatureHex = extractSignatureHex;
38
+ exports.extractIncomingEvent = extractIncomingEvent;
39
+ exports.extractWebhookPayload = extractWebhookPayload;
37
40
  const n8n_workflow_1 = require("n8n-workflow");
38
41
  const crypto = __importStar(require("node:crypto"));
42
+ function extractSignatureHex(signatureHeader) {
43
+ const trimmedSignature = signatureHeader.trim();
44
+ const [, prefixedValue] = trimmedSignature.split('=', 2);
45
+ const hexValue = prefixedValue ?? trimmedSignature;
46
+ if (!/^[a-fA-F0-9]+$/.test(hexValue)) {
47
+ throw new n8n_workflow_1.ApplicationError('Invalid signature format');
48
+ }
49
+ return hexValue;
50
+ }
51
+ function extractIncomingEvent(bodyData, headers) {
52
+ return (bodyData.event_type ??
53
+ bodyData.event ??
54
+ headers['x-webhook-event']);
55
+ }
56
+ function extractWebhookPayload(bodyData) {
57
+ const nestedPayload = (bodyData.data ?? bodyData.payload);
58
+ if (nestedPayload && typeof nestedPayload === 'object' && !Array.isArray(nestedPayload)) {
59
+ return nestedPayload;
60
+ }
61
+ return bodyData;
62
+ }
39
63
  class LinqTrigger {
40
64
  description = {
41
65
  displayName: 'Linq Trigger',
@@ -71,6 +95,38 @@ class LinqTrigger {
71
95
  name: 'events',
72
96
  type: 'multiOptions',
73
97
  options: [
98
+ {
99
+ name: 'Call Answered',
100
+ value: 'call.answered',
101
+ },
102
+ {
103
+ name: 'Call Declined',
104
+ value: 'call.declined',
105
+ },
106
+ {
107
+ name: 'Call Ended',
108
+ value: 'call.ended',
109
+ },
110
+ {
111
+ name: 'Call Failed',
112
+ value: 'call.failed',
113
+ },
114
+ {
115
+ name: 'Call Initiated',
116
+ value: 'call.initiated',
117
+ },
118
+ {
119
+ name: 'Call No Answer',
120
+ value: 'call.no_answer',
121
+ },
122
+ {
123
+ name: 'Call Ringing',
124
+ value: 'call.ringing',
125
+ },
126
+ {
127
+ name: 'Chat Created',
128
+ value: 'chat.created',
129
+ },
74
130
  {
75
131
  name: 'Chat Group Icon Update Failed',
76
132
  value: 'chat.group_icon_update_failed',
@@ -87,10 +143,22 @@ class LinqTrigger {
87
143
  name: 'Chat Group Name Updated',
88
144
  value: 'chat.group_name_updated',
89
145
  },
146
+ {
147
+ name: 'Chat Typing Indicator Started',
148
+ value: 'chat.typing_indicator.started',
149
+ },
150
+ {
151
+ name: 'Chat Typing Indicator Stopped',
152
+ value: 'chat.typing_indicator.stopped',
153
+ },
90
154
  {
91
155
  name: 'Message Delivered',
92
156
  value: 'message.delivered',
93
157
  },
158
+ {
159
+ name: 'Message Edited',
160
+ value: 'message.edited',
161
+ },
94
162
  {
95
163
  name: 'Message Failed',
96
164
  value: 'message.failed',
@@ -115,6 +183,10 @@ class LinqTrigger {
115
183
  name: 'Participant Removed',
116
184
  value: 'participant.removed',
117
185
  },
186
+ {
187
+ name: 'Phone Number Status Updated',
188
+ value: 'phone_number.status_updated',
189
+ },
118
190
  {
119
191
  name: 'Reaction Added',
120
192
  value: 'reaction.added',
@@ -148,10 +220,7 @@ class LinqTrigger {
148
220
  if (Math.abs(now - timestamp) > 300) {
149
221
  throw new n8n_workflow_1.ApplicationError('Request timestamp is too old');
150
222
  }
151
- const [scheme, signature] = signatureHeader.split('=');
152
- if (scheme !== 'sha256' || !signature) {
153
- throw new n8n_workflow_1.ApplicationError('Invalid signature format');
154
- }
223
+ const signature = extractSignatureHex(signatureHeader);
155
224
  // 2. Verify HMAC Signature
156
225
  const credentials = await this.getCredentials('linqApi');
157
226
  const signingSecret = credentials.webhookSigningSecret;
@@ -176,22 +245,34 @@ class LinqTrigger {
176
245
  }
177
246
  // 3. Event Filtering
178
247
  const events = this.getNodeParameter('events', []);
179
- const incomingEvent = bodyData.event;
248
+ const incomingEvent = extractIncomingEvent(bodyData, headers);
249
+ if (!incomingEvent) {
250
+ throw new n8n_workflow_1.ApplicationError('Unable to determine webhook event type');
251
+ }
180
252
  // Filter if events are specified
181
253
  if (events.length > 0 && !events.includes(incomingEvent)) {
182
254
  return {};
183
255
  }
184
- // 4. Extract Payload
185
- // The V3 structure is { event: string, payload: object }
186
- const { payload } = bodyData;
256
+ // 4. Extract payload from both legacy and current webhook envelopes
257
+ const payload = extractWebhookPayload(bodyData);
258
+ const json = {
259
+ event: incomingEvent,
260
+ api_version: bodyData.api_version,
261
+ webhook_version: bodyData.webhook_version,
262
+ event_id: bodyData.event_id,
263
+ trace_id: bodyData.trace_id,
264
+ created_at: bodyData.created_at,
265
+ partner_id: bodyData.partner_id,
266
+ payload,
267
+ };
268
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
269
+ Object.assign(json, payload);
270
+ }
187
271
  return {
188
272
  workflowData: [
189
273
  [
190
274
  {
191
- json: {
192
- event: incomingEvent,
193
- ...payload,
194
- },
275
+ json,
195
276
  },
196
277
  ],
197
278
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-linq",
3
- "version": "4.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "Linq API integration for n8n",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -45,17 +45,17 @@
45
45
  ]
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "^24.3.3",
49
- "@typescript-eslint/parser": "~8.32.0",
50
- "eslint": "^8.57.0",
51
- "eslint-plugin-n8n-nodes-base": "^1.16.3",
52
- "gulp": "^5.0.0",
53
- "prettier": "^3.5.3",
54
- "typescript": "^5.8.2",
55
- "vitest": "^2.1.0"
48
+ "@types/node": "^24.12.0",
49
+ "@typescript-eslint/parser": "~8.58.0",
50
+ "eslint": "^8.57.1",
51
+ "eslint-plugin-n8n-nodes-base": "^1.16.6",
52
+ "gulp": "^5.0.1",
53
+ "prettier": "^3.8.1",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.1.2"
56
56
  },
57
57
  "peerDependencies": {
58
- "n8n-workflow": "^1.120.7"
58
+ "n8n-workflow": "^1.120.10"
59
59
  },
60
60
  "overrides": {
61
61
  "form-data": "^4.0.5",