n8n-nodes-linq 0.2.0 → 3.1.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.
@@ -4,4 +4,12 @@ export declare class LinqApi implements ICredentialType {
4
4
  displayName: string;
5
5
  documentationUrl: string;
6
6
  properties: INodeProperties[];
7
+ authenticate: {
8
+ readonly type: "generic";
9
+ readonly properties: {
10
+ readonly headers: {
11
+ readonly Authorization: "Bearer {{$credentials.integrationToken}}";
12
+ };
13
+ };
14
+ };
7
15
  }
@@ -12,12 +12,20 @@ class LinqApi {
12
12
  name: 'integrationToken',
13
13
  type: 'string',
14
14
  default: '',
15
- description: 'Your X-LINQ-INTEGRATION-TOKEN',
15
+ description: 'Your Linq Integration Token',
16
16
  typeOptions: {
17
17
  password: true,
18
18
  },
19
19
  },
20
20
  ];
21
+ this.authenticate = {
22
+ type: 'generic',
23
+ properties: {
24
+ headers: {
25
+ Authorization: 'Bearer {{$credentials.integrationToken}}',
26
+ },
27
+ },
28
+ };
21
29
  }
22
30
  }
23
31
  exports.LinqApi = LinqApi;
@@ -224,7 +224,7 @@ class Linq {
224
224
  type: 'string',
225
225
  displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
226
226
  default: '',
227
- description: 'Required: your Linq phone number (phone_number)',
227
+ description: 'Required: your Linq phone number (from)',
228
228
  },
229
229
  {
230
230
  displayName: 'From Phone Number',
@@ -243,18 +243,29 @@ class Linq {
243
243
  description: 'Comma-separated list of phone numbers to find chat between them',
244
244
  },
245
245
  {
246
- displayName: 'Page',
247
- name: 'page',
246
+ displayName: 'Cursor',
247
+ name: 'cursor',
248
+ type: 'string',
249
+ displayOptions: { show: { resource: ['chat', 'chatMessage'], operation: ['getAll'] } },
250
+ default: '',
251
+ description: 'Cursor for pagination',
252
+ },
253
+ {
254
+ displayName: 'Limit',
255
+ name: 'limit',
248
256
  type: 'number',
249
- displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
250
- default: 1,
251
- description: 'Page number for pagination',
257
+ typeOptions: {
258
+ minValue: 1,
259
+ },
260
+ displayOptions: { show: { resource: ['chat', 'chatMessage'], operation: ['getAll'] } },
261
+ default: 50,
262
+ description: 'Max number of results to return',
252
263
  },
253
264
  {
254
265
  displayName: 'Per Page',
255
266
  name: 'perPage',
256
267
  type: 'number',
257
- displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
268
+ displayOptions: { show: { resource: [], operation: [] } }, // Deprecated, hiding but keeping for compatibility if needed, though we should probably migrate logic
258
269
  default: 25,
259
270
  description: 'Number of items per page',
260
271
  },
@@ -291,6 +302,14 @@ class Linq {
291
302
  default: '',
292
303
  description: 'The text of the message to send',
293
304
  },
305
+ {
306
+ displayName: 'Attachment URLs',
307
+ name: 'attachmentUrls',
308
+ type: 'string',
309
+ displayOptions: { show: { resource: ['chat'], operation: ['create'] } },
310
+ default: '',
311
+ description: 'Comma-separated list of attachment URLs',
312
+ },
294
313
  // Chat Message parameters
295
314
  {
296
315
  displayName: 'Chat Message ID',
@@ -488,6 +507,67 @@ class Linq {
488
507
  const items = this.getInputData();
489
508
  const returnData = [];
490
509
  const credentials = await this.getCredentials('linqApi');
510
+ const uploadAttachment = async (url) => {
511
+ const fileResponse = await this.helpers.request({
512
+ method: 'GET',
513
+ url,
514
+ encoding: null,
515
+ resolveWithFullResponse: true,
516
+ });
517
+ const buffer = fileResponse.body;
518
+ // Limit file size to 50MB to avoid OOM
519
+ // Note: This loads the file into memory. For larger files, streaming would be preferred
520
+ // but is not implemented here to maintain compatibility with n8n helper request patterns.
521
+ const MAX_SIZE = 50 * 1024 * 1024;
522
+ if (buffer.length > MAX_SIZE) {
523
+ throw new n8n_workflow_1.ApplicationError(`File at ${url} exceeds the 50MB size limit.`);
524
+ }
525
+ const contentType = fileResponse.headers['content-type'];
526
+ const uploadConfig = await this.helpers.request({
527
+ method: 'POST',
528
+ url: 'https://api.linqapp.com/api/partner/v3/attachments',
529
+ headers: {
530
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
531
+ 'Content-Type': 'application/json',
532
+ 'Accept': 'application/json',
533
+ },
534
+ body: {
535
+ content_type: contentType,
536
+ },
537
+ json: true,
538
+ });
539
+ await this.helpers.request({
540
+ method: 'PUT',
541
+ url: uploadConfig.upload_url,
542
+ headers: {
543
+ 'Content-Type': contentType,
544
+ },
545
+ body: buffer,
546
+ json: false,
547
+ });
548
+ return uploadConfig.id;
549
+ };
550
+ const createParts = async (text, attachmentUrlsString) => {
551
+ const parts = [];
552
+ if (text) {
553
+ parts.push({ type: 'text', value: text });
554
+ }
555
+ if (attachmentUrlsString) {
556
+ const urls = attachmentUrlsString.split(',').map(u => u.trim()).filter(Boolean);
557
+ // Upload attachments in batches to limit concurrency and avoid OOM
558
+ const CONCURRENCY_LIMIT = 3;
559
+ const attachmentIds = [];
560
+ for (let i = 0; i < urls.length; i += CONCURRENCY_LIMIT) {
561
+ const chunk = urls.slice(i, i + CONCURRENCY_LIMIT);
562
+ const chunkResults = await Promise.all(chunk.map(url => uploadAttachment(url)));
563
+ attachmentIds.push(...chunkResults);
564
+ }
565
+ attachmentIds.forEach(id => {
566
+ parts.push({ type: 'media', attachment_id: id });
567
+ });
568
+ }
569
+ return parts;
570
+ };
491
571
  for (let i = 0; i < items.length; i++) {
492
572
  const resource = this.getNodeParameter('resource', i);
493
573
  const operation = this.getNodeParameter('operation', i);
@@ -497,24 +577,23 @@ class Linq {
497
577
  if (resource === 'chat') {
498
578
  if (operation === 'getAll') {
499
579
  const phoneNumber = this.getNodeParameter('phoneNumber', i, '');
500
- const page = this.getNodeParameter('page', i);
501
- const perPage = this.getNodeParameter('perPage', i);
580
+ const cursor = this.getNodeParameter('cursor', i, '');
581
+ const limit = this.getNodeParameter('limit', i);
502
582
  if (!phoneNumber) {
503
583
  throw new n8n_workflow_1.ApplicationError('phone_number is required by Linq API');
504
584
  }
505
585
  const qs = {};
506
- qs.phone_number = formatPhoneNumber(phoneNumber);
507
- if (page && page !== 1)
508
- qs.page = page;
509
- if (perPage && perPage !== 25)
510
- qs.per_page = perPage;
511
- qs.phone_number = formatPhoneNumber(phoneNumber);
586
+ qs.from = formatPhoneNumber(phoneNumber);
587
+ if (cursor)
588
+ qs.cursor = cursor;
589
+ if (limit && limit !== 50)
590
+ qs.limit = limit;
512
591
  responseData = await this.helpers.request({
513
592
  method: 'GET',
514
- url: 'https://api.linqapp.com/api/partner/v2/chats',
593
+ url: 'https://api.linqapp.com/api/partner/v3/chats',
515
594
  qs,
516
595
  headers: {
517
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
596
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
518
597
  'Accept': 'application/json'
519
598
  },
520
599
  json: true,
@@ -524,9 +603,9 @@ class Linq {
524
603
  const chatId = this.getNodeParameter('chatId', i);
525
604
  responseData = await this.helpers.request({
526
605
  method: 'GET',
527
- url: `https://api.linqapp.com/api/partner/v2/chats/${chatId}`,
606
+ url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}`,
528
607
  headers: {
529
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
608
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
530
609
  'Accept': 'application/json'
531
610
  },
532
611
  json: true,
@@ -540,16 +619,15 @@ class Linq {
540
619
  }
541
620
  const qs = {};
542
621
  if (phoneNumbers) {
543
- // Check if we have a single phone number or multiple
544
622
  const phoneNumbersArray = phoneNumbers.split(',').map(p => formatPhoneNumber(p.trim()));
545
623
  qs['phone_numbers[]'] = phoneNumbersArray;
546
624
  }
547
625
  responseData = await this.helpers.request({
548
626
  method: 'GET',
549
- url: 'https://api.linqapp.com/api/partner/v2/chats/find',
627
+ url: 'https://api.linqapp.com/api/partner/v3/chats/find',
550
628
  qs,
551
629
  headers: {
552
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
630
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
553
631
  'Accept': 'application/json'
554
632
  },
555
633
  json: true,
@@ -560,26 +638,26 @@ class Linq {
560
638
  const displayName = this.getNodeParameter('displayName', i);
561
639
  const phoneNumbers = this.getNodeParameter('phoneNumbers', i);
562
640
  const messageText = this.getNodeParameter('messageText', i);
641
+ const attachmentUrls = this.getNodeParameter('attachmentUrls', i, '');
563
642
  if (!sendFrom) {
564
643
  throw new n8n_workflow_1.ApplicationError('send_from is required by Linq when creating chats');
565
644
  }
645
+ const parts = await createParts(messageText, attachmentUrls);
566
646
  const body = {
567
- chat: {
568
- phone_numbers: phoneNumbers.split(',').map(p => formatPhoneNumber(p.trim()))
569
- },
647
+ from: formatPhoneNumber(sendFrom),
648
+ to: phoneNumbers.split(',').map(p => formatPhoneNumber(p.trim())),
570
649
  message: {
571
- text: messageText
650
+ parts
572
651
  }
573
652
  };
574
- body.send_from = formatPhoneNumber(sendFrom);
575
653
  if (displayName) {
576
- body.chat.display_name = displayName;
654
+ body.display_name = displayName;
577
655
  }
578
656
  responseData = await this.helpers.request({
579
657
  method: 'POST',
580
- url: 'https://api.linqapp.com/api/partner/v2/chats',
658
+ url: 'https://api.linqapp.com/api/partner/v3/chats',
581
659
  headers: {
582
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
660
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
583
661
  'Content-Type': 'application/json',
584
662
  'Accept': 'application/json'
585
663
  },
@@ -594,9 +672,9 @@ class Linq {
594
672
  }
595
673
  responseData = await this.helpers.request({
596
674
  method: 'POST',
597
- url: `https://api.linqapp.com/api/partner/v2/chats/${chatId}/share_contact`,
675
+ url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/share_contact`,
598
676
  headers: {
599
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
677
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
600
678
  'Content-Type': 'application/json',
601
679
  'Accept': 'application/json'
602
680
  },
@@ -608,14 +686,22 @@ class Linq {
608
686
  if (resource === 'chatMessage') {
609
687
  if (operation === 'getAll') {
610
688
  const chatId = this.getNodeParameter('chatId', i);
689
+ const cursor = this.getNodeParameter('cursor', i, '');
690
+ const limit = this.getNodeParameter('limit', i);
611
691
  if (!chatId) {
612
692
  throw new n8n_workflow_1.ApplicationError('chatId is required to list chat messages');
613
693
  }
694
+ const qs = {};
695
+ if (cursor)
696
+ qs.cursor = cursor;
697
+ if (limit && limit !== 50)
698
+ qs.limit = limit;
614
699
  responseData = await this.helpers.request({
615
700
  method: 'GET',
616
- url: `https://api.linqapp.com/api/partner/v2/chats/${chatId}/chat_messages`,
701
+ url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/messages`,
702
+ qs,
617
703
  headers: {
618
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
704
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
619
705
  'Accept': 'application/json'
620
706
  },
621
707
  json: true,
@@ -625,9 +711,9 @@ class Linq {
625
711
  const chatMessageId = this.getNodeParameter('chatMessageId', i);
626
712
  responseData = await this.helpers.request({
627
713
  method: 'GET',
628
- url: `https://api.linqapp.com/api/partner/v2/chat_messages/${chatMessageId}`,
714
+ url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}`,
629
715
  headers: {
630
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
716
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
631
717
  'Accept': 'application/json'
632
718
  },
633
719
  json: true,
@@ -641,23 +727,20 @@ class Linq {
641
727
  if (!chatId) {
642
728
  throw new n8n_workflow_1.ApplicationError('chatId is required to create a chat message');
643
729
  }
730
+ const parts = await createParts(messageText, attachmentUrls);
644
731
  const body = {
645
- text: messageText
646
- };
647
- if (attachmentUrls) {
648
- const parsed = attachmentUrls.split(',').map(u => u.trim()).filter(Boolean);
649
- if (parsed.length) {
650
- body.attachment_urls = parsed;
732
+ message: {
733
+ parts
651
734
  }
652
- }
735
+ };
653
736
  if (idempotencyKey) {
654
- body.idempotency_key = idempotencyKey;
737
+ body.message.idempotency_key = idempotencyKey;
655
738
  }
656
739
  responseData = await this.helpers.request({
657
740
  method: 'POST',
658
- url: `https://api.linqapp.com/api/partner/v2/chats/${chatId}/chat_messages`,
741
+ url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/messages`,
659
742
  headers: {
660
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
743
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
661
744
  'Content-Type': 'application/json',
662
745
  'Accept': 'application/json'
663
746
  },
@@ -669,9 +752,9 @@ class Linq {
669
752
  const chatMessageId = this.getNodeParameter('chatMessageId', i);
670
753
  responseData = await this.helpers.request({
671
754
  method: 'DELETE',
672
- url: `https://api.linqapp.com/api/partner/v2/chat_messages/${chatMessageId}`,
755
+ url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}`,
673
756
  headers: {
674
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
757
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
675
758
  'Accept': 'application/json'
676
759
  },
677
760
  json: true,
@@ -685,9 +768,9 @@ class Linq {
685
768
  };
686
769
  responseData = await this.helpers.request({
687
770
  method: 'POST',
688
- url: `https://api.linqapp.com/api/partner/v2/chat_messages/${chatMessageId}/edit`,
771
+ url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}/edit`,
689
772
  headers: {
690
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
773
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
691
774
  'Content-Type': 'application/json',
692
775
  'Accept': 'application/json'
693
776
  },
@@ -699,13 +782,14 @@ class Linq {
699
782
  const chatMessageId = this.getNodeParameter('chatMessageId', i);
700
783
  const reaction = this.getNodeParameter('reaction', i);
701
784
  const body = {
702
- reaction: reaction
785
+ operation: 'add',
786
+ type: reaction
703
787
  };
704
788
  responseData = await this.helpers.request({
705
789
  method: 'POST',
706
- url: `https://api.linqapp.com/api/partner/v2/chat_messages/${chatMessageId}/reactions`,
790
+ url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}/reactions`,
707
791
  headers: {
708
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
792
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
709
793
  'Content-Type': 'application/json',
710
794
  'Accept': 'application/json'
711
795
  },
@@ -720,9 +804,9 @@ class Linq {
720
804
  }
721
805
  responseData = await this.helpers.request({
722
806
  method: 'GET',
723
- url: `https://api.linqapp.com/api/partner/v2/chat_message_reactions/${reactionId}`,
807
+ url: `https://api.linqapp.com/api/partner/v3/chat_message_reactions/${reactionId}`,
724
808
  headers: {
725
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
809
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
726
810
  'Accept': 'application/json'
727
811
  },
728
812
  json: true,
@@ -734,9 +818,9 @@ class Linq {
734
818
  if (operation === 'getAll') {
735
819
  responseData = await this.helpers.request({
736
820
  method: 'GET',
737
- url: 'https://api.linqapp.com/api/partner/v2/phone_numbers',
821
+ url: 'https://api.linqapp.com/api/partner/v3/phone_numbers',
738
822
  headers: {
739
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
823
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
740
824
  'Accept': 'application/json'
741
825
  },
742
826
  json: true,
@@ -758,9 +842,9 @@ class Linq {
758
842
  }
759
843
  responseData = await this.helpers.request({
760
844
  method: 'PUT',
761
- url: `https://api.linqapp.com/api/partner/v2/phone_numbers/${phoneNumberId}`,
845
+ url: `https://api.linqapp.com/api/partner/v3/phone_numbers/${phoneNumberId}`,
762
846
  headers: {
763
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
847
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
764
848
  'Content-Type': 'application/json',
765
849
  'Accept': 'application/json'
766
850
  },
@@ -774,9 +858,9 @@ class Linq {
774
858
  if (operation === 'getAll') {
775
859
  responseData = await this.helpers.request({
776
860
  method: 'GET',
777
- url: 'https://api.linqapp.com/api/partner/v2/webhook_subscriptions',
861
+ url: 'https://api.linqapp.com/api/partner/v3/webhook_subscriptions',
778
862
  headers: {
779
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
863
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
780
864
  'Accept': 'application/json'
781
865
  },
782
866
  json: true,
@@ -786,9 +870,9 @@ class Linq {
786
870
  const webhookSubscriptionId = this.getNodeParameter('webhookSubscriptionId', i);
787
871
  responseData = await this.helpers.request({
788
872
  method: 'GET',
789
- url: `https://api.linqapp.com/api/partner/v2/webhook_subscriptions/${webhookSubscriptionId}`,
873
+ url: `https://api.linqapp.com/api/partner/v3/webhook_subscriptions/${webhookSubscriptionId}`,
790
874
  headers: {
791
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
875
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
792
876
  'Accept': 'application/json'
793
877
  },
794
878
  json: true,
@@ -800,18 +884,18 @@ class Linq {
800
884
  const version = this.getNodeParameter('version', i);
801
885
  const active = this.getNodeParameter('active', i);
802
886
  const body = {
803
- webhook_url: webhookUrl,
887
+ target_url: webhookUrl,
804
888
  version: version,
805
889
  active: active
806
890
  };
807
891
  if (events) {
808
- body.events = events.split(',').map(e => e.trim());
892
+ body.subscribed_events = events.split(',').map(e => e.trim());
809
893
  }
810
894
  responseData = await this.helpers.request({
811
895
  method: 'POST',
812
- url: 'https://api.linqapp.com/api/partner/v2/webhook_subscriptions',
896
+ url: 'https://api.linqapp.com/api/partner/v3/webhook_subscriptions',
813
897
  headers: {
814
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
898
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
815
899
  'Content-Type': 'application/json',
816
900
  'Accept': 'application/json'
817
901
  },
@@ -830,16 +914,16 @@ class Linq {
830
914
  active: active
831
915
  };
832
916
  if (webhookUrl) {
833
- body.webhook_url = webhookUrl;
917
+ body.target_url = webhookUrl;
834
918
  }
835
919
  if (events) {
836
- body.events = events.split(',').map(e => e.trim());
920
+ body.subscribed_events = events.split(',').map(e => e.trim());
837
921
  }
838
922
  responseData = await this.helpers.request({
839
923
  method: 'PUT',
840
- url: `https://api.linqapp.com/api/partner/v2/webhook_subscriptions/${webhookSubscriptionId}`,
924
+ url: `https://api.linqapp.com/api/partner/v3/webhook_subscriptions/${webhookSubscriptionId}`,
841
925
  headers: {
842
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
926
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
843
927
  'Content-Type': 'application/json',
844
928
  'Accept': 'application/json'
845
929
  },
@@ -851,9 +935,9 @@ class Linq {
851
935
  const webhookSubscriptionId = this.getNodeParameter('webhookSubscriptionId', i);
852
936
  responseData = await this.helpers.request({
853
937
  method: 'DELETE',
854
- url: `https://api.linqapp.com/api/partner/v2/webhook_subscriptions/${webhookSubscriptionId}`,
938
+ url: `https://api.linqapp.com/api/partner/v3/webhook_subscriptions/${webhookSubscriptionId}`,
855
939
  headers: {
856
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
940
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
857
941
  'Accept': 'application/json'
858
942
  },
859
943
  json: true,
@@ -889,9 +973,9 @@ class Linq {
889
973
  body.contact.location = location;
890
974
  responseData = await this.helpers.request({
891
975
  method: 'POST',
892
- url: 'https://api.linqapp.com/api/partner/v2/contacts',
976
+ url: 'https://api.linqapp.com/api/partner/v3/contacts',
893
977
  headers: {
894
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
978
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
895
979
  'Content-Type': 'application/json',
896
980
  'Accept': 'application/json'
897
981
  },
@@ -903,9 +987,9 @@ class Linq {
903
987
  const contactId = this.getNodeParameter('contactId', i);
904
988
  responseData = await this.helpers.request({
905
989
  method: 'GET',
906
- url: `https://api.linqapp.com/api/partner/v2/contacts/${contactId}`,
990
+ url: `https://api.linqapp.com/api/partner/v3/contacts/${contactId}`,
907
991
  headers: {
908
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
992
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
909
993
  'Accept': 'application/json'
910
994
  },
911
995
  json: true,
@@ -939,9 +1023,9 @@ class Linq {
939
1023
  body.contact.location = location;
940
1024
  responseData = await this.helpers.request({
941
1025
  method: 'PUT',
942
- url: `https://api.linqapp.com/api/partner/v2/contacts/${contactId}`,
1026
+ url: `https://api.linqapp.com/api/partner/v3/contacts/${contactId}`,
943
1027
  headers: {
944
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
1028
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
945
1029
  'Content-Type': 'application/json',
946
1030
  'Accept': 'application/json'
947
1031
  },
@@ -953,9 +1037,9 @@ class Linq {
953
1037
  const contactId = this.getNodeParameter('contactId', i);
954
1038
  responseData = await this.helpers.request({
955
1039
  method: 'DELETE',
956
- url: `https://api.linqapp.com/api/partner/v2/contacts/${contactId}`,
1040
+ url: `https://api.linqapp.com/api/partner/v3/contacts/${contactId}`,
957
1041
  headers: {
958
- 'X-LINQ-INTEGRATION-TOKEN': credentials.integrationToken,
1042
+ 'Authorization': `Bearer ${credentials.integrationToken}`,
959
1043
  'Accept': 'application/json'
960
1044
  },
961
1045
  json: true,
@@ -1,5 +1,5 @@
1
- import { INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
1
+ import { IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
2
2
  export declare class LinqTrigger implements INodeType {
3
3
  description: INodeTypeDescription;
4
- webhook(this: any): Promise<IWebhookResponseData>;
4
+ webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
5
5
  }
@@ -1,7 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.LinqTrigger = void 0;
4
37
  const n8n_workflow_1 = require("n8n-workflow");
38
+ const crypto = __importStar(require("crypto"));
5
39
  class LinqTrigger {
6
40
  constructor() {
7
41
  this.description = {
@@ -74,31 +108,67 @@ class LinqTrigger {
74
108
  };
75
109
  }
76
110
  async webhook() {
77
- const bodyData = this.getBodyData();
111
+ const req = this.getRequestObject();
78
112
  const headers = this.getHeaderData();
79
- const event = headers['x-linq-event'];
80
- const signature = headers['x-linq-signature'];
81
- // Verify signature (optional but recommended)
113
+ const bodyData = this.getBodyData();
114
+ // 1. Validate and Parse Signature Header
115
+ const signatureHeader = headers['x-webhook-signature'];
116
+ const timestampHeader = headers['x-webhook-timestamp'];
117
+ if (!signatureHeader) {
118
+ throw new n8n_workflow_1.ApplicationError('Missing X-Webhook-Signature header');
119
+ }
120
+ if (!timestampHeader) {
121
+ throw new n8n_workflow_1.ApplicationError('Missing X-Webhook-Timestamp header');
122
+ }
123
+ // Verify timestamp freshness (within 5 minutes)
124
+ const timestamp = parseInt(timestampHeader, 10);
125
+ const now = Math.floor(Date.now() / 1000);
126
+ if (Math.abs(now - timestamp) > 300) {
127
+ throw new n8n_workflow_1.ApplicationError('Request timestamp is too old');
128
+ }
129
+ const [scheme, signature] = signatureHeader.split('=');
130
+ if (scheme !== 'sha256' || !signature) {
131
+ throw new n8n_workflow_1.ApplicationError('Invalid signature format');
132
+ }
133
+ // 2. Verify HMAC Signature
82
134
  const credentials = await this.getCredentials('linqApi');
83
- const crypto = require('crypto');
135
+ const token = credentials.integrationToken;
136
+ // Use rawBody for accurate HMAC verification
137
+ const rawBody = req.rawBody;
138
+ if (!rawBody) {
139
+ console.warn('LinqTrigger: rawBody is missing, falling back to JSON.stringify. Signature verification might fail.');
140
+ }
141
+ const bodyString = rawBody || JSON.stringify(bodyData);
142
+ const payloadToHash = `${timestampHeader}.${bodyString}`;
84
143
  const expectedSignature = crypto
85
- .createHmac('sha256', credentials.integrationToken)
86
- .update(JSON.stringify(bodyData))
87
- .digest('hex');
88
- if (signature !== expectedSignature) {
144
+ .createHmac('sha256', token)
145
+ .update(payloadToHash)
146
+ .digest();
147
+ const sourceSignature = Buffer.from(signature, 'hex');
148
+ if (sourceSignature.length !== expectedSignature.length || !crypto.timingSafeEqual(sourceSignature, expectedSignature)) {
89
149
  throw new n8n_workflow_1.ApplicationError('Invalid signature');
90
150
  }
91
- // Return the event data to start the workflow
92
- const returnData = [
93
- {
94
- json: {
95
- event,
96
- payload: bodyData,
97
- },
98
- },
99
- ];
151
+ // 3. Event Filtering
152
+ const events = this.getNodeParameter('events', []);
153
+ const incomingEvent = bodyData.event;
154
+ // Filter if events are specified
155
+ if (events.length > 0 && !events.includes(incomingEvent)) {
156
+ return {};
157
+ }
158
+ // 4. Extract Payload
159
+ // The V3 structure is { event: string, payload: object }
160
+ const { payload } = bodyData;
100
161
  return {
101
- workflowData: [returnData],
162
+ workflowData: [
163
+ [
164
+ {
165
+ json: {
166
+ event: incomingEvent,
167
+ ...payload,
168
+ },
169
+ },
170
+ ],
171
+ ],
102
172
  };
103
173
  }
104
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-linq",
3
- "version": "0.2.0",
3
+ "version": "3.1.1",
4
4
  "description": "Linq API integration for n8n",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",