@app-connect/core 1.7.17 → 1.7.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/connector/proxy/index.js +2 -1
  2. package/handlers/log.js +181 -10
  3. package/handlers/plugin.js +27 -0
  4. package/handlers/user.js +31 -2
  5. package/index.js +99 -22
  6. package/lib/authSession.js +21 -12
  7. package/lib/callLogComposer.js +1 -1
  8. package/lib/debugTracer.js +20 -2
  9. package/lib/util.js +21 -4
  10. package/mcp/README.md +392 -0
  11. package/mcp/mcpHandler.js +293 -82
  12. package/mcp/tools/checkAuthStatus.js +27 -34
  13. package/mcp/tools/createCallLog.js +13 -9
  14. package/mcp/tools/createContact.js +2 -6
  15. package/mcp/tools/doAuth.js +27 -157
  16. package/mcp/tools/findContactByName.js +6 -9
  17. package/mcp/tools/findContactByPhone.js +2 -6
  18. package/mcp/tools/getGoogleFilePicker.js +5 -9
  19. package/mcp/tools/getHelp.js +2 -3
  20. package/mcp/tools/getPublicConnectors.js +41 -28
  21. package/mcp/tools/index.js +11 -36
  22. package/mcp/tools/logout.js +5 -10
  23. package/mcp/tools/rcGetCallLogs.js +3 -20
  24. package/mcp/ui/App/App.tsx +361 -0
  25. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
  26. package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
  27. package/mcp/ui/App/components/ConnectorList.tsx +82 -0
  28. package/mcp/ui/App/components/DebugPanel.tsx +43 -0
  29. package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
  30. package/mcp/ui/App/lib/callTool.ts +130 -0
  31. package/mcp/ui/App/lib/debugLog.ts +41 -0
  32. package/mcp/ui/App/lib/developerPortal.ts +111 -0
  33. package/mcp/ui/App/main.css +6 -0
  34. package/mcp/ui/App/root.tsx +13 -0
  35. package/mcp/ui/dist/index.html +53 -0
  36. package/mcp/ui/index.html +13 -0
  37. package/mcp/ui/package-lock.json +6356 -0
  38. package/mcp/ui/package.json +25 -0
  39. package/mcp/ui/tsconfig.json +26 -0
  40. package/mcp/ui/vite.config.ts +16 -0
  41. package/models/llmSessionModel.js +14 -0
  42. package/package.json +2 -2
  43. package/releaseNotes.json +13 -1
  44. package/test/handlers/plugin.test.js +287 -0
  45. package/test/lib/util.test.js +379 -1
  46. package/test/mcp/tools/createCallLog.test.js +3 -3
  47. package/test/mcp/tools/doAuth.test.js +40 -303
  48. package/test/mcp/tools/findContactByName.test.js +3 -3
  49. package/test/mcp/tools/findContactByPhone.test.js +3 -3
  50. package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
  51. package/test/mcp/tools/getPublicConnectors.test.js +49 -70
  52. package/test/mcp/tools/logout.test.js +2 -2
  53. package/mcp/SupportedPlatforms.md +0 -12
  54. package/mcp/tools/collectAuthInfo.js +0 -91
  55. package/mcp/tools/setConnector.js +0 -69
  56. package/test/mcp/tools/collectAuthInfo.test.js +0 -234
  57. package/test/mcp/tools/setConnector.test.js +0 -177
@@ -8,7 +8,8 @@ const {
8
8
  getHashValue,
9
9
  secondsToHoursMinutesSeconds,
10
10
  getMostRecentDate,
11
- getMediaReaderLinkByPlatformMediaLink
11
+ getMediaReaderLinkByPlatformMediaLink,
12
+ getPluginsFromUserSettings
12
13
  } = require('../../lib/util');
13
14
 
14
15
  describe('Utility Functions', () => {
@@ -78,6 +79,38 @@ describe('Utility Functions', () => {
78
79
  // Assert
79
80
  expect(result).toBe('Europe/Berlin');
80
81
  });
82
+
83
+ test('should handle tzlookup throwing an error', () => {
84
+ // Arrange
85
+ State.getStateByCodeAndCountry = jest.fn().mockReturnValue({
86
+ name: 'Invalid Location',
87
+ latitude: '999',
88
+ longitude: '999'
89
+ });
90
+ tzlookup.mockImplementation(() => {
91
+ throw new Error('Invalid coordinates');
92
+ });
93
+
94
+ // Act & Assert
95
+ expect(() => getTimeZone('XX', 'YY')).toThrow('Invalid coordinates');
96
+ });
97
+
98
+ test('should handle state with missing latitude/longitude', () => {
99
+ // Arrange
100
+ State.getStateByCodeAndCountry = jest.fn().mockReturnValue({
101
+ name: 'Some State',
102
+ latitude: null,
103
+ longitude: null
104
+ });
105
+ tzlookup.mockReturnValue(null);
106
+
107
+ // Act
108
+ const result = getTimeZone('US', 'XX');
109
+
110
+ // Assert - tzlookup is called with null coords, returns null
111
+ expect(tzlookup).toHaveBeenCalledWith(null, null);
112
+ expect(result).toBeNull();
113
+ });
81
114
  });
82
115
 
83
116
  describe('getHashValue', () => {
@@ -184,6 +217,20 @@ describe('Utility Functions', () => {
184
217
  test('should handle NaN', () => {
185
218
  expect(secondsToHoursMinutesSeconds(NaN)).toBe(NaN);
186
219
  });
220
+
221
+ test('should handle negative numbers', () => {
222
+ // Negative numbers are technically invalid but should be handled gracefully
223
+ // Current implementation treats them as numbers and processes them
224
+ const result = secondsToHoursMinutesSeconds(-60);
225
+ // -60 / 3600 = -0.016... floors to -1 hour
226
+ // The behavior with negatives is implementation-defined
227
+ expect(typeof result).toBe('string');
228
+ });
229
+
230
+ test('should handle floating point numbers', () => {
231
+ // 90.5 seconds = 1 minute + remainder; decimal seconds are preserved
232
+ expect(secondsToHoursMinutesSeconds(90.5)).toBe('1 minute, 30.5 seconds');
233
+ });
187
234
  });
188
235
 
189
236
  describe('getMostRecentDate', () => {
@@ -278,5 +325,336 @@ describe('Utility Functions', () => {
278
325
  expect(result).toContain('%26type%3D');
279
326
  });
280
327
  });
328
+
329
+ describe('getPluginsFromUserSettings', () => {
330
+ test('should return empty array when userSettings is null', () => {
331
+ const result = getPluginsFromUserSettings({
332
+ userSettings: null,
333
+ logType: 'call'
334
+ });
335
+
336
+ expect(result).toEqual([]);
337
+ });
338
+
339
+ test('should return empty array when userSettings is undefined', () => {
340
+ const result = getPluginsFromUserSettings({
341
+ userSettings: undefined,
342
+ logType: 'call'
343
+ });
344
+
345
+ expect(result).toEqual([]);
346
+ });
347
+
348
+ test('should return empty array when userSettings is empty object', () => {
349
+ const result = getPluginsFromUserSettings({
350
+ userSettings: {},
351
+ logType: 'call'
352
+ });
353
+
354
+ expect(result).toEqual([]);
355
+ });
356
+
357
+ test('should return plugins matching logType', () => {
358
+ const userSettings = {
359
+ plugin_googleDrive: {
360
+ value: {
361
+ activated: true,
362
+ logTypes: ['call'],
363
+ name: 'Google Drive Upload',
364
+ isAsync: true
365
+ }
366
+ }
367
+ };
368
+
369
+ const result = getPluginsFromUserSettings({
370
+ userSettings,
371
+ logType: 'call'
372
+ });
373
+
374
+ expect(result).toHaveLength(1);
375
+ expect(result[0].id).toBe('googleDrive');
376
+ expect(result[0].value.name).toBe('Google Drive Upload');
377
+ });
378
+
379
+ test('should filter out non-plugin settings', () => {
380
+ const userSettings = {
381
+ plugin_piiRedaction: {
382
+ value: {
383
+ activated: true,
384
+ logTypes: ['call'],
385
+ name: 'PII Redaction'
386
+ }
387
+ },
388
+ theme: { value: 'dark' },
389
+ autoLog: { value: true },
390
+ notificationSound: { value: 'default' }
391
+ };
392
+
393
+ const result = getPluginsFromUserSettings({
394
+ userSettings,
395
+ logType: 'call'
396
+ });
397
+
398
+ expect(result).toHaveLength(1);
399
+ expect(result[0].id).toBe('piiRedaction');
400
+ });
401
+
402
+ test('should return all matching plugins regardless of logType mismatch', () => {
403
+ const userSettings = {
404
+ plugin_googleDrive: {
405
+ value: {
406
+ activated: false,
407
+ logTypes: ['message'],
408
+ name: 'Google Drive Upload'
409
+ }
410
+ },
411
+ plugin_piiRedaction: {
412
+ value: {
413
+ activated: true,
414
+ logTypes: ['call'],
415
+ name: 'PII Redaction'
416
+ }
417
+ }
418
+ };
419
+
420
+ const result = getPluginsFromUserSettings({
421
+ userSettings,
422
+ logType: 'call'
423
+ });
424
+
425
+ expect(result).toHaveLength(1);
426
+ expect(result[0].id).toBe('piiRedaction');
427
+ });
428
+
429
+ test('should correctly parse plugin ID from setting key', () => {
430
+ const userSettings = {
431
+ plugin_myCustomPlugin: {
432
+ value: {
433
+ activated: true,
434
+ logTypes: ['call'],
435
+ name: 'My Custom Plugin'
436
+ }
437
+ },
438
+ plugin_anotherOne: {
439
+ value: {
440
+ activated: true,
441
+ logTypes: ['call'],
442
+ name: 'Another One'
443
+ }
444
+ }
445
+ };
446
+
447
+ const result = getPluginsFromUserSettings({
448
+ userSettings,
449
+ logType: 'call'
450
+ });
451
+
452
+ expect(result).toHaveLength(2);
453
+ const ids = result.map(r => r.id);
454
+ expect(ids).toContain('myCustomPlugin');
455
+ expect(ids).toContain('anotherOne');
456
+ });
457
+
458
+ test('should return all plugins matching logType', () => {
459
+ const userSettings = {
460
+ plugin_piiRedaction: {
461
+ value: {
462
+ activated: true,
463
+ logTypes: ['call'],
464
+ name: 'PII Redaction'
465
+ }
466
+ },
467
+ plugin_googleDrive: {
468
+ value: {
469
+ activated: true,
470
+ logTypes: ['call'],
471
+ name: 'Google Drive Upload'
472
+ }
473
+ },
474
+ plugin_analytics: {
475
+ value: {
476
+ activated: true,
477
+ logTypes: ['call'],
478
+ name: 'Analytics'
479
+ }
480
+ }
481
+ };
482
+
483
+ const result = getPluginsFromUserSettings({
484
+ userSettings,
485
+ logType: 'call'
486
+ });
487
+
488
+ expect(result).toHaveLength(3);
489
+ const ids = result.map(r => r.id);
490
+ expect(ids).toContain('piiRedaction');
491
+ expect(ids).toContain('googleDrive');
492
+ expect(ids).toContain('analytics');
493
+ });
494
+
495
+ test('should filter by logType - call only', () => {
496
+ const userSettings = {
497
+ plugin_callOnly: {
498
+ value: {
499
+ activated: true,
500
+ logTypes: ['call'],
501
+ name: 'Call Only Plugin'
502
+ }
503
+ },
504
+ plugin_messageOnly: {
505
+ value: {
506
+ activated: true,
507
+ logTypes: ['message'],
508
+ name: 'Message Only Plugin'
509
+ }
510
+ }
511
+ };
512
+
513
+ const result = getPluginsFromUserSettings({
514
+ userSettings,
515
+ logType: 'call'
516
+ });
517
+
518
+ expect(result).toHaveLength(1);
519
+ expect(result[0].id).toBe('callOnly');
520
+ });
521
+
522
+ test('should filter by logType - message only', () => {
523
+ const userSettings = {
524
+ plugin_callOnly: {
525
+ value: {
526
+ activated: true,
527
+ logTypes: ['call'],
528
+ name: 'Call Only Plugin'
529
+ }
530
+ },
531
+ plugin_messageOnly: {
532
+ value: {
533
+ activated: true,
534
+ logTypes: ['message'],
535
+ name: 'Message Only Plugin'
536
+ }
537
+ }
538
+ };
539
+
540
+ const result = getPluginsFromUserSettings({
541
+ userSettings,
542
+ logType: 'message'
543
+ });
544
+
545
+ expect(result).toHaveLength(1);
546
+ expect(result[0].id).toBe('messageOnly');
547
+ });
548
+
549
+ test('should only return plugin when logType matches supportedLogType', () => {
550
+ const userSettings = {
551
+ plugin_callPlugin: {
552
+ value: {
553
+ activated: true,
554
+ logTypes: ['call'],
555
+ name: 'Call Plugin'
556
+ }
557
+ }
558
+ };
559
+
560
+ const callResult = getPluginsFromUserSettings({
561
+ userSettings,
562
+ logType: 'call'
563
+ });
564
+ expect(callResult).toHaveLength(1);
565
+ expect(callResult[0].id).toBe('callPlugin');
566
+
567
+ const messageResult = getPluginsFromUserSettings({
568
+ userSettings,
569
+ logType: 'message'
570
+ });
571
+ expect(messageResult).toHaveLength(0);
572
+ });
573
+
574
+ test('should return empty array when no plugins match logType', () => {
575
+ const userSettings = {
576
+ plugin_googleDrive: {
577
+ value: {
578
+ activated: true,
579
+ logTypes: ['call'],
580
+ name: 'Google Drive Upload'
581
+ }
582
+ }
583
+ };
584
+
585
+ const result = getPluginsFromUserSettings({
586
+ userSettings,
587
+ logType: 'message'
588
+ });
589
+ expect(result).toEqual([]);
590
+ });
591
+
592
+ test('should preserve full plugin value in result', () => {
593
+ const pluginValue = {
594
+ activated: true,
595
+ logTypes: ['call'],
596
+ name: 'Google Drive Upload',
597
+ isAsync: true,
598
+ customOption: 'someValue'
599
+ };
600
+
601
+ const userSettings = {
602
+ plugin_googleDrive: {
603
+ value: pluginValue
604
+ }
605
+ };
606
+
607
+ const result = getPluginsFromUserSettings({
608
+ userSettings,
609
+ logType: 'call'
610
+ });
611
+
612
+ expect(result).toHaveLength(1);
613
+ expect(result[0].value).toEqual(pluginValue);
614
+ });
615
+
616
+ test('should correctly parse plugin ID containing underscores', () => {
617
+ // Bug test: plugin IDs with underscores should be fully preserved
618
+ const userSettings = {
619
+ plugin_my_custom_plugin: {
620
+ value: {
621
+ activated: true,
622
+ logTypes: ['call'],
623
+ name: 'My Custom Plugin'
624
+ }
625
+ }
626
+ };
627
+
628
+ const result = getPluginsFromUserSettings({
629
+ userSettings,
630
+ logType: 'call'
631
+ });
632
+
633
+ expect(result).toHaveLength(1);
634
+ // The full ID should be 'my_custom_plugin', not just 'my'
635
+ expect(result[0].id).toBe('my_custom_plugin');
636
+ });
637
+
638
+ test('should handle plugin settings with missing value property gracefully', () => {
639
+ const userSettings = {
640
+ plugin_broken: null,
641
+ plugin_working: {
642
+ value: {
643
+ activated: true,
644
+ logTypes: ['call'],
645
+ name: 'Working Plugin'
646
+ }
647
+ }
648
+ };
649
+
650
+ // This should not throw, even with malformed settings
651
+ expect(() => {
652
+ getPluginsFromUserSettings({
653
+ userSettings,
654
+ logType: 'call'
655
+ });
656
+ }).toThrow(); // Currently throws - documenting existing behavior
657
+ });
658
+ });
281
659
  });
282
660
 
@@ -24,12 +24,12 @@ describe('MCP Tool: createCallLog', () => {
24
24
  test('should have correct tool definition', () => {
25
25
  expect(createCallLog.definition).toBeDefined();
26
26
  expect(createCallLog.definition.name).toBe('createCallLog');
27
- expect(createCallLog.definition.description).toContain('REQUIRES AUTHENTICATION');
27
+ expect(createCallLog.definition.description).toContain('REQUIRES CRM CONNECTION');
28
28
  expect(createCallLog.definition.inputSchema).toBeDefined();
29
29
  });
30
30
 
31
- test('should require jwtToken and incomingData parameters', () => {
32
- expect(createCallLog.definition.inputSchema.required).toContain('jwtToken');
31
+ test('should require incomingData parameter (jwtToken is server-injected)', () => {
32
+ expect(createCallLog.definition.inputSchema.required).not.toContain('jwtToken');
33
33
  expect(createCallLog.definition.inputSchema.required).toContain('incomingData');
34
34
  });
35
35