@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
@@ -51,7 +51,7 @@ function getBasicAuth({ apiKey }) {
51
51
  return Buffer.from(`${apiKey}:`).toString('base64');
52
52
  }
53
53
 
54
- async function getUserInfo({ authHeader, hostname, additionalInfo, platform, apiKey, proxyId, proxyConfig } = {}) {
54
+ async function getUserInfo({ authHeader, hostname, additionalInfo, platform, apiKey, proxyId, proxyConfig, userEmail } = {}) {
55
55
  const cfg = proxyConfig ? proxyConfig : (await loadPlatformConfig(proxyId));
56
56
  if (!cfg || !cfg.operations?.getUserInfo) {
57
57
  // Fallback if no getUserInfo operation defined
@@ -72,6 +72,7 @@ async function getUserInfo({ authHeader, hostname, additionalInfo, platform, api
72
72
  apiKey,
73
73
  hostname,
74
74
  platform,
75
+ userEmail,
75
76
  },
76
77
  user: {},
77
78
  authHeader
package/handlers/log.js CHANGED
@@ -2,6 +2,7 @@ const Op = require('sequelize').Op;
2
2
  const { CallLogModel } = require('../models/callLogModel');
3
3
  const { MessageLogModel } = require('../models/messageLogModel');
4
4
  const { UserModel } = require('../models/userModel');
5
+ const { CacheModel } = require('../models/cacheModel');
5
6
  const oauth = require('../lib/oauth');
6
7
  const { composeCallLog } = require('../lib/callLogComposer');
7
8
  const { composeSharedSMSLog } = require('../lib/sharedSMSComposer');
@@ -11,11 +12,14 @@ const { NoteCache } = require('../models/dynamo/noteCacheSchema');
11
12
  const { Connector } = require('../models/dynamo/connectorSchema');
12
13
  const moment = require('moment');
13
14
  const { getMediaReaderLinkByPlatformMediaLink } = require('../lib/util');
15
+ const axios = require('axios');
16
+ const { getPluginsFromUserSettings } = require('../lib/util');
14
17
  const logger = require('../lib/logger');
15
18
  const { handleApiError, handleDatabaseError } = require('../lib/errorHandler');
19
+ const { v4: uuidv4 } = require('uuid');
16
20
  const { AccountDataModel } = require('../models/accountDataModel');
17
21
 
18
- async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
22
+ async function createCallLog({ jwtToken, platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
19
23
  try {
20
24
  let existingCallLog = null;
21
25
  try {
@@ -55,6 +59,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
55
59
  }
56
60
  };
57
61
  }
62
+
58
63
  const platformModule = connectorRegistry.getConnector(platform);
59
64
  const callLog = incomingData.logInfo;
60
65
  const additionalSubmission = incomingData.additionalSubmission;
@@ -115,6 +120,63 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
115
120
  name: incomingData.contactName ?? ""
116
121
  };
117
122
 
123
+
124
+ const pluginAsyncTaskIds = [];
125
+ // Plugins
126
+ const loggingPlugins = getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'call' });
127
+ for (const pluginSetting of loggingPlugins) {
128
+ const pluginId = pluginSetting.id;
129
+ let pluginDataResponse = null;
130
+ switch (pluginSetting.value.access) {
131
+ case 'public':
132
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?type=plugin`);
133
+ break;
134
+ case 'private':
135
+ case 'shared':
136
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
137
+ break;
138
+ default:
139
+ throw new Error('Invalid plugin access');
140
+ }
141
+ const pluginData = pluginDataResponse.data;
142
+ const pluginManifest = pluginData.platforms[pluginSetting.value.name];
143
+ let pluginEndpointUrl = pluginManifest.endpointUrl;
144
+ if (!pluginEndpointUrl) {
145
+ throw new Error('Plugin URL is not set');
146
+ }
147
+ else {
148
+ // check if endpoint has query params already
149
+ if (pluginEndpointUrl.includes('?')) {
150
+ pluginEndpointUrl += `&jwtToken=${jwtToken}`;
151
+ }
152
+ else {
153
+ pluginEndpointUrl += `?jwtToken=${jwtToken}`;
154
+ }
155
+ }
156
+ if (pluginSetting.value.isAsync) {
157
+ const asyncTaskId = `${userId}-${uuidv4()}`;
158
+ pluginAsyncTaskIds.push(asyncTaskId);
159
+ await CacheModel.create({
160
+ id: asyncTaskId,
161
+ status: 'initialized',
162
+ userId,
163
+ cacheKey: `pluginTask-${pluginSetting.value.name}`,
164
+ expiry: moment().add(1, 'hour').toDate()
165
+ });
166
+ axios.post(pluginEndpointUrl, {
167
+ data: incomingData,
168
+ asyncTaskId
169
+ });
170
+ }
171
+ else {
172
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
173
+ data: incomingData
174
+ });
175
+ // eslint-disable-next-line no-param-reassign
176
+ incomingData = processedResultResponse.data;
177
+ }
178
+ }
179
+
118
180
  // Compose call log details centrally
119
181
  const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
120
182
  let composedLogDetails = '';
@@ -179,8 +241,8 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
179
241
  catch (error) {
180
242
  return handleDatabaseError(error, 'Error creating call log');
181
243
  }
244
+ return { successful: !!logId, logId, returnMessage, extraDataTracking, pluginAsyncTaskIds };
182
245
  }
183
- return { successful: !!logId, logId, returnMessage, extraDataTracking };
184
246
  } catch (e) {
185
247
  return handleApiError(e, platform, 'createCallLog', { userId });
186
248
  }
@@ -285,7 +347,7 @@ async function getCallLog({ userId, sessionIds, platform, requireDetails }) {
285
347
  }
286
348
  }
287
349
 
288
- async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
350
+ async function updateCallLog({ jwtToken, platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
289
351
  try {
290
352
  let existingCallLog = null;
291
353
  try {
@@ -299,11 +361,11 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
299
361
  return handleDatabaseError(error, 'Error finding existing call log');
300
362
  }
301
363
  if (existingCallLog) {
302
- const platformModule = connectorRegistry.getConnector(platform);
303
364
  let user = await UserModel.findByPk(userId);
304
365
  if (!user || !user.accessToken) {
305
366
  return { successful: false, message: `Contact not found` };
306
367
  }
368
+ const platformModule = connectorRegistry.getConnector(platform);
307
369
  const proxyId = user.platformAdditionalInfo?.proxyId;
308
370
  let proxyConfig = null;
309
371
  if (proxyId) {
@@ -334,6 +396,61 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
334
396
  break;
335
397
  }
336
398
 
399
+ const pluginAsyncTaskIds = [];
400
+ // Plugins
401
+ const plugins = getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'call' });
402
+ for (const pluginSetting of plugins) {
403
+ const pluginId = pluginSetting.id;
404
+ let pluginDataResponse = null;
405
+ switch (pluginSetting.value.access) {
406
+ case 'public':
407
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?type=plugin`);
408
+ break;
409
+ case 'private':
410
+ case 'shared':
411
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
412
+ break;
413
+ default:
414
+ throw new Error('Invalid plugin access');
415
+ }
416
+ const pluginData = pluginDataResponse.data;
417
+ const pluginManifest = pluginData.platforms[pluginSetting.value.name];
418
+ let pluginEndpointUrl = pluginManifest.endpointUrl;
419
+ if (!pluginEndpointUrl) {
420
+ throw new Error('Plugin URL is not set');
421
+ }
422
+ else {
423
+ if (pluginEndpointUrl.includes('?')) {
424
+ pluginEndpointUrl += `&jwtToken=${jwtToken}`;
425
+ }
426
+ else {
427
+ pluginEndpointUrl += `?jwtToken=${jwtToken}`;
428
+ }
429
+ }
430
+ if (pluginSetting.value.isAsync) {
431
+ const asyncTaskId = `${userId}-${uuidv4()}`;
432
+ pluginAsyncTaskIds.push(asyncTaskId);
433
+ await CacheModel.create({
434
+ id: asyncTaskId,
435
+ status: 'initialized',
436
+ userId,
437
+ cacheKey: `pluginTask-${pluginSetting.value.name}`,
438
+ expiry: moment().add(1, 'hour').toDate()
439
+ });
440
+ axios.post(pluginEndpointUrl, {
441
+ data: { logInfo: incomingData },
442
+ asyncTaskId
443
+ });
444
+ }
445
+ else {
446
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
447
+ data: incomingData
448
+ });
449
+ // eslint-disable-next-line no-param-reassign
450
+ incomingData = processedResultResponse.data;
451
+ }
452
+ }
453
+
337
454
  // Fetch existing call log details once to avoid duplicate API calls
338
455
  let existingCallLogDetails = null; // Compose updated call log details centrally
339
456
  const logFormat = platformModule.getLogFormatType ? platformModule.getLogFormatType(platform, proxyConfig) : LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT;
@@ -416,12 +533,7 @@ async function updateCallLog({ platform, userId, incomingData, hashedAccountId,
416
533
  isFromSSCL,
417
534
  proxyConfig,
418
535
  });
419
- if (!extraDataTracking) {
420
- extraDataTracking = {};
421
- }
422
- extraDataTracking.withSmartNoteLog = !!incomingData.aiNote;
423
- extraDataTracking.withTranscript = !!incomingData.transcript;
424
- return { successful: true, logId: existingCallLog.thirdPartyLogId, updatedNote, returnMessage, extraDataTracking };
536
+ return { successful: true, logId: existingCallLog.thirdPartyLogId, updatedNote, returnMessage, extraDataTracking, pluginAsyncTaskIds };
425
537
  }
426
538
  return { successful: false };
427
539
  } catch (e) {
@@ -518,6 +630,65 @@ async function createMessageLog({ platform, userId, incomingData }) {
518
630
  const ownerName = incomingData.logInfo.owner?.name;
519
631
  const isSharedSMS = !!ownerName;
520
632
 
633
+ const pluginAsyncTaskIds = [];
634
+ // Plugins
635
+ const isSMS = incomingData.logInfo.messages.some(m => m.type === 'SMS');
636
+ const isFax = incomingData.logInfo.messages.some(m => m.type === 'Fax');
637
+ const smsPlugins = isSMS ? getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'sms' }) : [];
638
+ const faxPlugins = isFax ? getPluginsFromUserSettings({ userSettings: user.userSettings, logType: 'fax' }) : [];
639
+ const plugins = [...smsPlugins, ...faxPlugins];
640
+ for (const pluginSetting of plugins) {
641
+ const pluginId = pluginSetting.id;
642
+ let pluginDataResponse = null;
643
+ switch (pluginSetting.value.access) {
644
+ case 'public':
645
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?type=plugin`);
646
+ break;
647
+ case 'private':
648
+ case 'shared':
649
+ pluginDataResponse = await axios.get(`${process.env.DEV_PORTAL_URL}/public-api/connectors/${pluginId}/manifest?access=internal&type=connector&accountId=${user.rcAccountId}`);
650
+ break;
651
+ default:
652
+ throw new Error('Invalid plugin access');
653
+ }
654
+ const pluginData = pluginDataResponse.data;
655
+ const pluginManifest = pluginData.platforms[pluginSetting.value.name];
656
+ let pluginEndpointUrl = pluginManifest.endpointUrl;
657
+ if (!pluginEndpointUrl) {
658
+ throw new Error('Plugin URL is not set');
659
+ }
660
+ else {
661
+ if (pluginEndpointUrl.includes('?')) {
662
+ pluginEndpointUrl += `&jwtToken=${jwtToken}`;
663
+ }
664
+ else {
665
+ pluginEndpointUrl += `?jwtToken=${jwtToken}`;
666
+ }
667
+ }
668
+ if (pluginSetting.value.isAsync) {
669
+ const asyncTaskId = `${userId}-${uuidv4()}`;
670
+ pluginAsyncTaskIds.push(asyncTaskId);
671
+ await CacheModel.create({
672
+ id: asyncTaskId,
673
+ status: 'initialized',
674
+ userId,
675
+ cacheKey: `pluginTask-${pluginSetting.value.name}`,
676
+ expiry: moment().add(1, 'hour').toDate()
677
+ });
678
+ axios.post(pluginEndpointUrl, {
679
+ data: { logInfo: incomingData },
680
+ asyncTaskId
681
+ });
682
+ }
683
+ else {
684
+ const processedResultResponse = await axios.post(pluginEndpointUrl, {
685
+ data: incomingData
686
+ });
687
+ // eslint-disable-next-line no-param-reassign
688
+ incomingData = processedResultResponse.data;
689
+ }
690
+ }
691
+
521
692
  let messageIds = [];
522
693
  const correspondents = [];
523
694
  if (isGroupSMS) {
@@ -0,0 +1,27 @@
1
+ const { CacheModel } = require('../models/cacheModel');
2
+ const { Op } = require('sequelize');
3
+
4
+ async function getPluginAsyncTasks({ asyncTaskIds }) {
5
+ const caches = await CacheModel.findAll({
6
+ where: {
7
+ id: {
8
+ [Op.in]: asyncTaskIds
9
+ }
10
+ }
11
+ });
12
+ const result = caches.map(cache => ({
13
+ cacheKey: cache.cacheKey,
14
+ status: cache.status
15
+ }));
16
+ const toRemoveCaches = caches.filter(cache => cache.status === 'completed' || cache.status === 'failed');
17
+ await CacheModel.destroy({
18
+ where: {
19
+ id: {
20
+ [Op.in]: toRemoveCaches.map(cache => cache.id)
21
+ }
22
+ }
23
+ });
24
+ return result;
25
+ }
26
+
27
+ exports.getPluginAsyncTasks = getPluginAsyncTasks;
package/handlers/user.js CHANGED
@@ -49,7 +49,10 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
49
49
  const keys = Object.keys(userSettingsByAdmin.userSettings).concat(Object.keys(userSettings));
50
50
  // distinct keys
51
51
  for (const key of new Set(keys)) {
52
- // from user's own settings
52
+ // marked as removed
53
+ if (userSettingsByAdmin.userSettings[key]?.isRemoved) {
54
+ continue;
55
+ }
53
56
  if ((userSettingsByAdmin.userSettings[key] === undefined || userSettingsByAdmin.userSettings[key].customizable) && userSettings[key] !== undefined) {
54
57
  result[key] = {
55
58
  customizable: true,
@@ -57,6 +60,27 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
57
60
  defaultValue: userSettings[key].defaultValue,
58
61
  options: userSettings[key].options
59
62
  };
63
+ // Special case: plugins
64
+ if (key.startsWith('plugin_')) {
65
+ const config = Object.keys(result[key].value.config)?.length === 0 ? null : result[key].value.config;
66
+ if (config) {
67
+ const configFromadminSettings = userSettingsByAdmin.userSettings[key].value.config ?? {};
68
+ for (const k in config) {
69
+ // use admin setting to replace, if not customizable
70
+ if (configFromadminSettings[k] && !configFromadminSettings[k].customizable || !config[k].value && configFromadminSettings[k].value) {
71
+ config[k] = configFromadminSettings[k];
72
+ }
73
+ else {
74
+ config[k].customizable = configFromadminSettings[k].customizable;
75
+ }
76
+ }
77
+ result[key].value.config = config;
78
+ }
79
+ //Case: no config at all, use admin setting directly
80
+ else {
81
+ result[key].value.config = userSettingsByAdmin.userSettings[key].value.config;
82
+ }
83
+ }
60
84
  }
61
85
  // from admin settings
62
86
  else {
@@ -68,7 +92,7 @@ async function getUserSettings({ user, rcAccessToken, rcAccountId }) {
68
92
  return result;
69
93
  }
70
94
 
71
- async function updateUserSettings({ user, userSettings, platformName }) {
95
+ async function updateUserSettings({ user, userSettings, settingKeysToRemove, platformName }) {
72
96
  const keys = Object.keys(userSettings || {});
73
97
  let updatedSettings = {
74
98
  ...(user.userSettings || {})
@@ -76,6 +100,11 @@ async function updateUserSettings({ user, userSettings, platformName }) {
76
100
  for (const k of keys) {
77
101
  updatedSettings[k] = userSettings[k];
78
102
  }
103
+ for (const k of settingKeysToRemove) {
104
+ if (updatedSettings[k]) {
105
+ delete updatedSettings[k];
106
+ }
107
+ }
79
108
  const platformModule = connectorRegistry.getConnector(platformName);
80
109
  if (platformModule.onUpdateUserSettings) {
81
110
  const { successful, returnMessage } = await platformModule.onUpdateUserSettings({ user, userSettings, updatedSettings });
package/index.js CHANGED
@@ -3,8 +3,10 @@ const cors = require('cors')
3
3
  const bodyParser = require('body-parser');
4
4
  require('body-parser-xml')(bodyParser);
5
5
  const dynamoose = require('dynamoose');
6
+ const { DynamoDB } = require('@aws-sdk/client-dynamodb');
6
7
  const axios = require('axios');
7
8
  const { UserModel } = require('./models/userModel');
9
+ const { LlmSessionModel } = require('./models/llmSessionModel');
8
10
  const { CallDownListModel } = require('./models/callDownListModel');
9
11
  const { CallLogModel } = require('./models/callLogModel');
10
12
  const { MessageLogModel } = require('./models/messageLogModel');
@@ -29,6 +31,7 @@ const mcpHandler = require('./mcp/mcpHandler');
29
31
  const logger = require('./lib/logger');
30
32
  const { DebugTracer } = require('./lib/debugTracer');
31
33
  const s3ErrorLogReport = require('./lib/s3ErrorLogReport');
34
+ const pluginCore = require('./handlers/plugin');
32
35
  const { handleDatabaseError } = require('./lib/errorHandler');
33
36
  const { updateAuthSession } = require('./lib/authSession');
34
37
 
@@ -37,13 +40,18 @@ try {
37
40
  packageJson = require('./package.json');
38
41
  }
39
42
  catch (e) {
40
- logger.error('Error loading package.json', { stack: e.stack });
43
+ logger.error('Error loading package.json', { stack: e.stack });
41
44
  packageJson = require('../package.json');
42
45
  }
43
46
 
44
47
  // For using dynamodb in local env
48
+ // AWS SDK v3 requires a region even for local; ddb.local() omits it, so set manually.
45
49
  if (process.env.DYNAMODB_LOCALHOST) {
46
- dynamoose.aws.ddb.local(process.env.DYNAMODB_LOCALHOST);
50
+ dynamoose.aws.ddb.set(new DynamoDB({
51
+ endpoint: process.env.DYNAMODB_LOCALHOST,
52
+ region: 'local',
53
+ credentials: { accessKeyId: 'local', secretAccessKey: 'local' },
54
+ }));
47
55
  }
48
56
  // log axios requests
49
57
  if (process.env.IS_PROD === 'false') {
@@ -58,6 +66,7 @@ async function initDB() {
58
66
  if (!process.env.DISABLE_SYNC_DB_TABLE) {
59
67
  logger.info('creating db tables if not exist...');
60
68
  await UserModel.sync();
69
+ await LlmSessionModel.sync();
61
70
  await CallLogModel.sync();
62
71
  await MessageLogModel.sync();
63
72
  await AdminConfigModel.sync();
@@ -756,7 +765,7 @@ function createCoreRouter() {
756
765
  res.status(400).send(tracer ? tracer.wrapResponse('User not found') : 'User not found');
757
766
  return;
758
767
  }
759
- const { userSettings } = await userCore.updateUserSettings({ user, userSettings: req.body.userSettings, platformName });
768
+ const { userSettings } = await userCore.updateUserSettings({ user, userSettings: req.body.userSettings, settingKeysToRemove: req.body.settingKeysToRemove || [], platformName });
760
769
  res.status(200).send(tracer ? tracer.wrapResponse({ userSettings }) : { userSettings });
761
770
  success = true;
762
771
  }
@@ -1323,7 +1332,7 @@ function createCoreRouter() {
1323
1332
  }
1324
1333
  const { id: userId, platform } = decodedToken;
1325
1334
  platformName = platform;
1326
- const { successful, logId, returnMessage, extraDataTracking, isRevokeUserSession } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1335
+ const { successful, logId, returnMessage, extraDataTracking, pluginAsyncTaskIds, isRevokeUserSession } = await logCore.createCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1327
1336
  if (isRevokeUserSession) {
1328
1337
  res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1329
1338
  success = false;
@@ -1332,7 +1341,7 @@ function createCoreRouter() {
1332
1341
  if (extraDataTracking) {
1333
1342
  extraData = extraDataTracking;
1334
1343
  }
1335
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage }) : { successful, logId, returnMessage });
1344
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage, pluginAsyncTaskIds }) : { successful, logId, returnMessage, pluginAsyncTaskIds });
1336
1345
  success = true;
1337
1346
  }
1338
1347
  }
@@ -1386,11 +1395,11 @@ function createCoreRouter() {
1386
1395
  }
1387
1396
  const { id: userId, platform } = decodedToken;
1388
1397
  platformName = platform;
1389
- const { successful, logId, updatedNote, returnMessage, extraDataTracking } = await logCore.updateCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1398
+ const { successful, logId, updatedNote, returnMessage, extraDataTracking, pluginAsyncTaskIds } = await logCore.updateCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1390
1399
  if (extraDataTracking) {
1391
1400
  extraData = extraDataTracking;
1392
1401
  }
1393
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, updatedNote, returnMessage }) : { successful, logId, updatedNote, returnMessage });
1402
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, updatedNote, returnMessage, pluginAsyncTaskIds }) : { successful, logId, updatedNote, returnMessage, pluginAsyncTaskIds });
1394
1403
  success = true;
1395
1404
  }
1396
1405
  else {
@@ -1941,6 +1950,55 @@ function createCoreRouter() {
1941
1950
  });
1942
1951
  });
1943
1952
 
1953
+ router.post('/pluginAsyncTask', async function (req, res) {
1954
+ const requestStartTime = new Date().getTime();
1955
+ const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
1956
+ tracer?.trace('pluginAsyncTask:start', { query: req.query });
1957
+ let platformName = null;
1958
+ let success = false;
1959
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1960
+ const { jwtToken } = req.query;
1961
+ try {
1962
+ if (!jwtToken) {
1963
+ tracer?.trace('pluginAsyncTask:noToken', {});
1964
+ res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
1965
+ return;
1966
+ }
1967
+ const unAuthData = jwt.decodeJwt(jwtToken);
1968
+ const user = await UserModel.findByPk(unAuthData?.id);
1969
+ if (!user) {
1970
+ tracer?.trace('pluginAsyncTask:userNotFound', {});
1971
+ res.status(400).send(tracer ? tracer.wrapResponse('User not found') : 'User not found');
1972
+ return;
1973
+ }
1974
+ const { asyncTaskIds } = req.body;
1975
+ const filteredTasksIds = asyncTaskIds.filter(taskId => taskId.startsWith(user.id));
1976
+ const tasks = await pluginCore.getPluginAsyncTasks({ asyncTaskIds: filteredTasksIds });
1977
+ res.status(200).send(tracer ? tracer.wrapResponse({ tasks }) : { tasks });
1978
+ success = true;
1979
+ }
1980
+ catch (e) {
1981
+ console.log(`platform: ${platformName} \n${e.stack}`);
1982
+ res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
1983
+ tracer?.traceError('pluginAsyncTask:error', e, { platform: platformName });
1984
+ success = false;
1985
+ }
1986
+ const requestEndTime = new Date().getTime();
1987
+ analytics.track({
1988
+ eventName: 'Plugin Async Task',
1989
+ interfaceName: 'pluginAsyncTask',
1990
+ connectorName: platformName,
1991
+ accountId: hashedAccountId,
1992
+ extensionId: hashedExtensionId,
1993
+ success,
1994
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
1995
+ userAgent,
1996
+ ip,
1997
+ author,
1998
+ eventAddedVia
1999
+ });
2000
+ });
2001
+
1944
2002
  if (process.env.IS_PROD === 'false') {
1945
2003
  router.post('/registerMockUser', async function (req, res) {
1946
2004
  const secretKey = req.query.secretKey;
@@ -2057,24 +2115,32 @@ function createCoreRouter() {
2057
2115
  });
2058
2116
  });
2059
2117
 
2060
- router.use('/mcp', (req, res, next) => {// LOG EVERYTHING
2061
- console.log(`[${req.method}] /mcp`);
2062
- console.log("Headers:", JSON.stringify(req.headers['authorization'] ? "Auth Token Present" : "No Auth"));
2063
- console.log("Body:", JSON.stringify(req.body));
2064
- // return next();
2065
- // Capture the response finish to see the status code
2066
- res.on('finish', () => {
2067
- console.log(`[Response] Status: ${res.statusCode}`);
2068
- console.log(`[Response] data: ${JSON.stringify(res.data)}`);
2069
- });
2118
+ router.use('/mcp', (req, res, next) => {
2119
+ // Widget tool calls are unauthenticated — they come from the iframe
2120
+ // which has no access to the RC bearer token.
2121
+ if (req.path === '/widget-tool-call') {
2122
+ return next();
2123
+ }
2070
2124
 
2071
2125
  const authHeader = req.headers.authorization;
2072
2126
  const token = authHeader?.split(' ')[1]; // Remove "Bearer "
2073
- // Allow the initial connection (GET) and CORS checks (OPTIONS) to pass freely.
2074
- // We only want to block the actual commands (POST).
2127
+ // Allow GET and OPTIONS (CORS preflight) to pass freely.
2075
2128
  if (req.method === 'GET' || req.method === 'OPTIONS') {
2076
2129
  return next();
2077
2130
  }
2131
+ // Allow MCP discovery/handshake methods — these carry no user data and must be
2132
+ // reachable without auth so the ChatGPT developer portal can scan tools.
2133
+ const mcpMethod = req.body?.method;
2134
+ const UNAUTHENTICATED_MCP_METHODS = new Set([
2135
+ 'initialize',
2136
+ 'tools/list',
2137
+ 'ping',
2138
+ 'notifications/initialized',
2139
+ 'notifications/cancelled',
2140
+ ]);
2141
+ if (mcpMethod && UNAUTHENTICATED_MCP_METHODS.has(mcpMethod)) {
2142
+ return next();
2143
+ }
2078
2144
  // SCENARIO 1: No Token provided. Kick off the OAuth flow.
2079
2145
  if (!token) {
2080
2146
  res.setHeader('WWW-Authenticate', `Bearer realm="mcp", resource_metadata="${process.env.APP_SERVER}/.well-known/oauth-protected-resource"`);
@@ -2084,9 +2150,7 @@ function createCoreRouter() {
2084
2150
  // SCENARIO 2: Token provided. Verify it.
2085
2151
  try {
2086
2152
  next();
2087
- } catch (error) {
2088
- console.error("Token validation failed:", error.message);
2089
- // Token is invalid or expired
2153
+ } catch {
2090
2154
  res.setHeader('WWW-Authenticate', `Bearer realm="mcp", resource_metadata="${process.env.APP_SERVER}/.well-known/oauth-protected-resource"`);
2091
2155
  return res.status(401).send();
2092
2156
  }
@@ -2109,6 +2173,19 @@ function createCoreRouter() {
2109
2173
  await mcpHandler.handleMcpRequest(req, res);
2110
2174
  });
2111
2175
 
2176
+ // Lightweight endpoint for widget tool calls (bypasses MCP protocol)
2177
+ router.options('/mcp/widget-tool-call', (req, res) => {
2178
+ res.setHeader('Access-Control-Allow-Origin', '*');
2179
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
2180
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
2181
+ res.status(200).end();
2182
+ });
2183
+ router.post('/mcp/widget-tool-call', async (req, res) => {
2184
+ res.setHeader('Access-Control-Allow-Origin', '*');
2185
+ res.setHeader('Content-Type', 'application/json');
2186
+ await mcpHandler.handleWidgetToolCall(req, res);
2187
+ });
2188
+
2112
2189
  return router;
2113
2190
  }
2114
2191
 
@@ -10,20 +10,29 @@ const AUTH_SESSION_PREFIX = 'auth-session';
10
10
  const SESSION_EXPIRY_MINUTES = 5;
11
11
 
12
12
  /**
13
- * Create a new auth session
13
+ * Create (or reset) an auth session.
14
+ * If a record already exists for the sessionId (e.g., user retries auth within
15
+ * the same ChatGPT conversation), it is reset to 'pending' so polling works
16
+ * correctly for the new attempt.
14
17
  */
15
18
  async function createAuthSession(sessionId, data) {
16
- await CacheModel.create({
17
- id: `${AUTH_SESSION_PREFIX}-${sessionId}`,
18
- cacheKey: AUTH_SESSION_PREFIX,
19
- userId: sessionId,
20
- status: 'pending',
21
- data: {
22
- ...data,
23
- createdAt: new Date().toISOString()
24
- },
25
- expiry: new Date(Date.now() + SESSION_EXPIRY_MINUTES * 60 * 1000)
26
- });
19
+ const id = `${AUTH_SESSION_PREFIX}-${sessionId}`;
20
+ const expiry = new Date(Date.now() + SESSION_EXPIRY_MINUTES * 60 * 1000);
21
+ const sessionData = { ...data, createdAt: new Date().toISOString() };
22
+
23
+ const existing = await CacheModel.findByPk(id);
24
+ if (existing) {
25
+ await existing.update({ status: 'pending', data: sessionData, expiry });
26
+ } else {
27
+ await CacheModel.create({
28
+ id,
29
+ cacheKey: AUTH_SESSION_PREFIX,
30
+ userId: sessionId,
31
+ status: 'pending',
32
+ data: sessionData,
33
+ expiry,
34
+ });
35
+ }
27
36
  }
28
37
 
29
38
  /**
@@ -115,7 +115,7 @@ function composeCallLog(params) {
115
115
  body = upsertCallRecording({ body, recordingLink, logFormat });
116
116
  }
117
117
 
118
- if (aiNote && (userSettings?.addCallLogAINote?.value ?? true)) {
118
+ if (aiNote && (userSettings?.addCallLogAiNote?.value ?? true)) {
119
119
  body = upsertAiNote({ body, aiNote, logFormat });
120
120
  }
121
121