@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.
- package/connector/proxy/index.js +2 -1
- package/handlers/log.js +181 -10
- package/handlers/plugin.js +27 -0
- package/handlers/user.js +31 -2
- package/index.js +99 -22
- package/lib/authSession.js +21 -12
- package/lib/callLogComposer.js +1 -1
- package/lib/debugTracer.js +20 -2
- package/lib/util.js +21 -4
- package/mcp/README.md +392 -0
- package/mcp/mcpHandler.js +293 -82
- package/mcp/tools/checkAuthStatus.js +27 -34
- package/mcp/tools/createCallLog.js +13 -9
- package/mcp/tools/createContact.js +2 -6
- package/mcp/tools/doAuth.js +27 -157
- package/mcp/tools/findContactByName.js +6 -9
- package/mcp/tools/findContactByPhone.js +2 -6
- package/mcp/tools/getGoogleFilePicker.js +5 -9
- package/mcp/tools/getHelp.js +2 -3
- package/mcp/tools/getPublicConnectors.js +41 -28
- package/mcp/tools/index.js +11 -36
- package/mcp/tools/logout.js +5 -10
- package/mcp/tools/rcGetCallLogs.js +3 -20
- package/mcp/ui/App/App.tsx +361 -0
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
- package/mcp/ui/App/components/ConnectorList.tsx +82 -0
- package/mcp/ui/App/components/DebugPanel.tsx +43 -0
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
- package/mcp/ui/App/lib/callTool.ts +130 -0
- package/mcp/ui/App/lib/debugLog.ts +41 -0
- package/mcp/ui/App/lib/developerPortal.ts +111 -0
- package/mcp/ui/App/main.css +6 -0
- package/mcp/ui/App/root.tsx +13 -0
- package/mcp/ui/dist/index.html +53 -0
- package/mcp/ui/index.html +13 -0
- package/mcp/ui/package-lock.json +6356 -0
- package/mcp/ui/package.json +25 -0
- package/mcp/ui/tsconfig.json +26 -0
- package/mcp/ui/vite.config.ts +16 -0
- package/models/llmSessionModel.js +14 -0
- package/package.json +2 -2
- package/releaseNotes.json +13 -1
- package/test/handlers/plugin.test.js +287 -0
- package/test/lib/util.test.js +379 -1
- package/test/mcp/tools/createCallLog.test.js +3 -3
- package/test/mcp/tools/doAuth.test.js +40 -303
- package/test/mcp/tools/findContactByName.test.js +3 -3
- package/test/mcp/tools/findContactByPhone.test.js +3 -3
- package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
- package/test/mcp/tools/getPublicConnectors.test.js +49 -70
- package/test/mcp/tools/logout.test.js +2 -2
- package/mcp/SupportedPlatforms.md +0 -12
- package/mcp/tools/collectAuthInfo.js +0 -91
- package/mcp/tools/setConnector.js +0 -69
- package/test/mcp/tools/collectAuthInfo.test.js +0 -234
- package/test/mcp/tools/setConnector.test.js +0 -177
package/connector/proxy/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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) => {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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
|
|
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
|
|
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
|
|
package/lib/authSession.js
CHANGED
|
@@ -10,20 +10,29 @@ const AUTH_SESSION_PREFIX = 'auth-session';
|
|
|
10
10
|
const SESSION_EXPIRY_MINUTES = 5;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Create
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
/**
|
package/lib/callLogComposer.js
CHANGED
|
@@ -115,7 +115,7 @@ function composeCallLog(params) {
|
|
|
115
115
|
body = upsertCallRecording({ body, recordingLink, logFormat });
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
if (aiNote && (userSettings?.
|
|
118
|
+
if (aiNote && (userSettings?.addCallLogAiNote?.value ?? true)) {
|
|
119
119
|
body = upsertAiNote({ body, aiNote, logFormat });
|
|
120
120
|
}
|
|
121
121
|
|