@app-connect/core 1.7.34 → 1.7.36
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/mock.js +9 -4
- package/connector/proxy/engine.js +3 -2
- package/connector/proxy/index.js +2 -2
- package/docs/architecture.md +1 -0
- package/docs/handlers.md +1 -1
- package/docs/models.md +2 -0
- package/docs/routes.md +1 -1
- package/handlers/disposition.js +3 -2
- package/handlers/log.js +18 -6
- package/handlers/plugin.js +13 -6
- package/index.js +22 -11
- package/lib/callLogLookup.js +82 -9
- package/lib/migrateCallLogsSchema.js +128 -7
- package/models/callLogModel.js +6 -0
- package/package.json +1 -1
- package/releaseNotes.json +44 -0
- package/test/connector/developerPortal.test.js +166 -0
- package/test/connector/mock.test.js +131 -0
- package/test/connector/proxy/engine.test.js +85 -0
- package/test/connector/proxy/index.test.js +246 -0
- package/test/connector/proxy/sample.json +1 -0
- package/test/handlers/admin.test.js +344 -0
- package/test/handlers/appointment.test.js +260 -0
- package/test/handlers/auth.test.js +6 -2
- package/test/handlers/calldown.test.js +310 -0
- package/test/handlers/disposition.test.js +396 -0
- package/test/handlers/log.test.js +327 -3
- package/test/handlers/managedOAuth.test.js +262 -0
- package/test/handlers/plugin.test.js +305 -0
- package/test/handlers/user.test.js +381 -0
- package/test/index.test.js +166 -1
- package/test/lib/analytics.test.js +146 -0
- package/test/lib/authSession.test.js +173 -0
- package/test/lib/encode.test.js +59 -0
- package/test/lib/errorHandler.test.js +246 -0
- package/test/lib/generalErrorMessage.test.js +82 -0
- package/test/lib/s3ErrorLogReport.test.js +187 -0
- package/test/mcp/mcpHandlerMore.test.js +384 -0
- package/test/mcp/tools/appointmentTools.test.js +362 -0
- package/test/models/callDownListModel.test.js +125 -0
- package/test/models/dynamo/lockSchema.test.js +37 -0
- package/test/models/dynamo/noteCacheSchema.test.js +45 -0
- package/test/models/llmSessionModel.test.js +91 -0
- package/test/models/models.test.js +92 -0
- package/test/routes/calldownRoutes.test.js +224 -0
- package/test/routes/coreRouterBroadRoutes.test.js +855 -0
- package/test/routes/dispositionRoutes.test.js +192 -0
- package/test/routes/managedAuthRoutes.test.js +151 -0
- package/test/routes/pluginRoutes.test.js +262 -0
package/connector/mock.js
CHANGED
|
@@ -27,19 +27,21 @@ async function deleteUser() {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async function getCallLog({ sessionIds, extensionNumber }) {
|
|
30
|
+
async function getCallLog({ sessionIds, extensionNumber, hashedExtensionId }) {
|
|
31
31
|
const sessionIdsArray = sessionIds.split(',');
|
|
32
32
|
const extensionNumberValue = extensionNumber?.toString() ?? '';
|
|
33
|
+
const hashedExtensionIdValue = hashedExtensionId?.toString() ?? '';
|
|
33
34
|
const callLogs = await CallLogModel.findAll({
|
|
34
35
|
where: buildCallLogSessionWhere({
|
|
35
36
|
sessionIds: sessionIdsArray,
|
|
36
37
|
extensionNumber: extensionNumberValue,
|
|
38
|
+
hashedExtensionId: hashedExtensionIdValue,
|
|
37
39
|
}),
|
|
38
|
-
order: [['extensionNumber', 'ASC']]
|
|
40
|
+
order: [['hashedExtensionId', 'ASC'], ['extensionNumber', 'ASC']]
|
|
39
41
|
});
|
|
40
42
|
const logs = [];
|
|
41
43
|
for (const sId of sessionIdsArray) {
|
|
42
|
-
const callLog = findMatchingCallLog(callLogs, sId, extensionNumberValue);
|
|
44
|
+
const callLog = findMatchingCallLog(callLogs, sId, extensionNumberValue, hashedExtensionIdValue);
|
|
43
45
|
if (!callLog) {
|
|
44
46
|
logs.push({ sessionId: sId, matched: false });
|
|
45
47
|
}
|
|
@@ -51,12 +53,14 @@ async function getCallLog({ sessionIds, extensionNumber }) {
|
|
|
51
53
|
return logs;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
async function createCallLog({ sessionId, extensionNumber }) {
|
|
56
|
+
async function createCallLog({ sessionId, extensionNumber, hashedExtensionId }) {
|
|
55
57
|
const extensionNumberValue = extensionNumber?.toString() ?? '';
|
|
58
|
+
const hashedExtensionIdValue = hashedExtensionId?.toString() ?? '';
|
|
56
59
|
let callLog = await CallLogModel.findOne({
|
|
57
60
|
where: buildCallLogSessionWhere({
|
|
58
61
|
sessionId,
|
|
59
62
|
extensionNumber: extensionNumberValue,
|
|
63
|
+
hashedExtensionId: hashedExtensionIdValue,
|
|
60
64
|
})
|
|
61
65
|
});
|
|
62
66
|
if (!callLog) {
|
|
@@ -64,6 +68,7 @@ async function createCallLog({ sessionId, extensionNumber }) {
|
|
|
64
68
|
id: shortid.generate(),
|
|
65
69
|
sessionId,
|
|
66
70
|
extensionNumber: extensionNumberValue,
|
|
71
|
+
hashedExtensionId: hashedExtensionIdValue,
|
|
67
72
|
userId: 'mockUser'
|
|
68
73
|
});
|
|
69
74
|
}
|
|
@@ -109,8 +109,8 @@ async function performRequest({ config, opName, inputs, user, authHeader }) {
|
|
|
109
109
|
return response;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function mapFindContactResponse({ config, response }) {
|
|
113
|
-
const map = config.operations?.
|
|
112
|
+
function mapFindContactResponse({ config, response, opName = 'findContact' }) {
|
|
113
|
+
const map = config.operations?.[opName]?.responseMapping;
|
|
114
114
|
if (!map) return [];
|
|
115
115
|
const __ctx = { body: response.data };
|
|
116
116
|
const list = getByPath(__ctx, map.listPath || 'body') || [];
|
|
@@ -123,6 +123,7 @@ function mapFindContactResponse({ config, response }) {
|
|
|
123
123
|
type: getByPath(it, itemMap.typePath || 'type') || 'Contact',
|
|
124
124
|
title: getByPath(it, itemMap.titlePath || 'title') || "",
|
|
125
125
|
company: getByPath(it, itemMap.companyPath || 'company') || "",
|
|
126
|
+
createdDate: getByPath(it, itemMap.createdDatePath || 'createdDate') || undefined,
|
|
126
127
|
mostRecentActivityDate: getByPath(it, itemMap.mostRecentActivityDatePath || 'mostRecentActivityDate') || undefined,
|
|
127
128
|
additionalInfo: getByPath(it, itemMap.additionalInfoPath || 'additionalInfo') || null
|
|
128
129
|
};
|
package/connector/proxy/index.js
CHANGED
|
@@ -179,7 +179,7 @@ async function findContact({ user, authHeader, phoneNumber, overridingFormat, is
|
|
|
179
179
|
user,
|
|
180
180
|
authHeader
|
|
181
181
|
});
|
|
182
|
-
const matchedContactInfo = mapFindContactResponse({ config: cfg, response });
|
|
182
|
+
const matchedContactInfo = mapFindContactResponse({ config: cfg, response, opName: 'findContact' });
|
|
183
183
|
return {
|
|
184
184
|
successful: true,
|
|
185
185
|
matchedContactInfo,
|
|
@@ -225,7 +225,7 @@ async function findContactWithName({ user, authHeader, name, proxyConfig }) {
|
|
|
225
225
|
user,
|
|
226
226
|
authHeader
|
|
227
227
|
});
|
|
228
|
-
const matchedContactInfo = mapFindContactResponse({ config: cfg, response });
|
|
228
|
+
const matchedContactInfo = mapFindContactResponse({ config: cfg, response, opName: 'findContactWithName' });
|
|
229
229
|
return {
|
|
230
230
|
successful: true,
|
|
231
231
|
matchedContactInfo,
|
package/docs/architecture.md
CHANGED
|
@@ -26,6 +26,7 @@ Its job is to expose a stable HTTP surface, persist shared state, and delegate C
|
|
|
26
26
|
- installs an axios interceptor in local-style environments
|
|
27
27
|
- syncs Sequelize models on startup unless `DISABLE_SYNC_DB_TABLE` or `skipDatabaseInit` disables it
|
|
28
28
|
- adds the `hashedRcExtensionId` column to `users` if an older schema is missing it
|
|
29
|
+
- migrates `callLogs` to include `hashedExtensionId` in the local call-log identity key when an older schema is missing it
|
|
29
30
|
- mounts all shared HTTP routes
|
|
30
31
|
- exposes dev-only mock routes when `IS_PROD === 'false'`
|
|
31
32
|
|
package/docs/handlers.md
CHANGED
|
@@ -59,7 +59,7 @@ This is the heaviest handler in the package.
|
|
|
59
59
|
|
|
60
60
|
Key responsibilities:
|
|
61
61
|
|
|
62
|
-
- prevents duplicate call-log creation by checking `CallLogModel` on `sessionId`
|
|
62
|
+
- prevents duplicate call-log creation by checking `CallLogModel` on `sessionId` plus extension identity (`hashedExtensionId` first, with legacy `extensionNumber` fallback)
|
|
63
63
|
- loads note cache from DynamoDB when `USE_CACHE` and server-side call logging are enabled
|
|
64
64
|
- runs configured synchronous plugins before creating or updating logs
|
|
65
65
|
- dispatches configured asynchronous call plugins after call-log create or update succeeds
|
package/docs/models.md
CHANGED
|
@@ -35,6 +35,8 @@ Stores the mapping between telephony sessions and CRM call logs.
|
|
|
35
35
|
| --- | --- |
|
|
36
36
|
| `id` | Telephony call id |
|
|
37
37
|
| `sessionId` | Session id; also part of the composite primary key |
|
|
38
|
+
| `extensionNumber` | Legacy RingCentral extension number identity; part of the composite primary key |
|
|
39
|
+
| `hashedExtensionId` | Hashed RingCentral extension id used for current call-log identity; part of the composite primary key |
|
|
38
40
|
| `platform` | Connector platform |
|
|
39
41
|
| `thirdPartyLogId` | CRM log id |
|
|
40
42
|
| `userId` | App Connect user id |
|
package/docs/routes.md
CHANGED
|
@@ -75,7 +75,7 @@ This page documents the non-MCP HTTP routes defined in `index.js`.
|
|
|
75
75
|
| Method | Path | Purpose |
|
|
76
76
|
| --- | --- | --- |
|
|
77
77
|
| `POST` | `/callLog/cacheNote` | Stores a temporary note for server-side call logging |
|
|
78
|
-
| `GET` | `/callLog` | Looks up existing call logs by session id |
|
|
78
|
+
| `GET` | `/callLog` | Looks up existing call logs by session id plus extension identity |
|
|
79
79
|
| `POST` | `/callLog` | Creates a CRM call log and stores the local linkage record |
|
|
80
80
|
| `PATCH` | `/callLog` | Updates an existing CRM call log |
|
|
81
81
|
| `PUT` | `/callDisposition` | Upserts call disposition data through the connector |
|
package/handlers/disposition.js
CHANGED
|
@@ -6,12 +6,13 @@ const { Connector } = require('../models/dynamo/connectorSchema');
|
|
|
6
6
|
const { handleApiError } = require('../lib/errorHandler');
|
|
7
7
|
const { buildCallLogSessionWhere } = require('../lib/callLogLookup');
|
|
8
8
|
|
|
9
|
-
async function upsertCallDisposition({ platform, userId, sessionId, extensionNumber, dispositions }) {
|
|
9
|
+
async function upsertCallDisposition({ platform, userId, sessionId, extensionNumber, hashedExtensionId, dispositions }) {
|
|
10
10
|
try {
|
|
11
11
|
const existingCallLog = await CallLogModel.findOne({
|
|
12
12
|
where: buildCallLogSessionWhere({
|
|
13
13
|
sessionId,
|
|
14
14
|
extensionNumber,
|
|
15
|
+
hashedExtensionId,
|
|
15
16
|
})
|
|
16
17
|
});
|
|
17
18
|
if (!existingCallLog) {
|
|
@@ -75,7 +76,7 @@ async function upsertCallDisposition({ platform, userId, sessionId, extensionNum
|
|
|
75
76
|
return { successful: !!logId, logId, returnMessage, extraDataTracking };
|
|
76
77
|
}
|
|
77
78
|
catch (e) {
|
|
78
|
-
return handleApiError(e, platform, 'upsertCallDisposition', { userId, sessionId, extensionNumber, dispositions });
|
|
79
|
+
return handleApiError(e, platform, 'upsertCallDisposition', { userId, sessionId, extensionNumber, hashedExtensionId, dispositions });
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
package/handlers/log.js
CHANGED
|
@@ -20,6 +20,7 @@ const { AccountDataModel } = require('../models/accountDataModel');
|
|
|
20
20
|
const pluginCore = require('./plugin');
|
|
21
21
|
const {
|
|
22
22
|
getCallLogExtensionNumber,
|
|
23
|
+
getCallLogHashedExtensionId,
|
|
23
24
|
buildCallLogSessionWhere,
|
|
24
25
|
findMatchingCallLog,
|
|
25
26
|
} = require('../lib/callLogLookup');
|
|
@@ -73,6 +74,7 @@ function buildAsyncPluginTaskData({ taskId, callbackUrl, pluginId, platform, ope
|
|
|
73
74
|
rcAccountId: user.rcAccountId,
|
|
74
75
|
sessionId: logInfo.sessionId ?? incomingData?.sessionId,
|
|
75
76
|
extensionNumber: getCallLogExtensionNumber(incomingData),
|
|
77
|
+
hashedExtensionId: getCallLogHashedExtensionId(incomingData),
|
|
76
78
|
callLogId: existingCallLog?.id ?? logInfo.telephonySessionId ?? logInfo.id,
|
|
77
79
|
thirdPartyLogId: existingCallLog?.thirdPartyLogId,
|
|
78
80
|
contactId: existingCallLog?.contactId ?? incomingData?.contactId,
|
|
@@ -103,6 +105,7 @@ async function runSyncCallPlugins({ syncCallPlugins, incomingData, user, platfor
|
|
|
103
105
|
const processedResultResponse = await axios.post(pluginEndpointUrl, {
|
|
104
106
|
data: processedIncomingData,
|
|
105
107
|
config: userConfig,
|
|
108
|
+
hashedExtensionId: user.hashedRcExtensionId,
|
|
106
109
|
}, {
|
|
107
110
|
headers: {
|
|
108
111
|
Authorization: `Bearer ${pluginJwtToken}`,
|
|
@@ -182,6 +185,7 @@ async function dispatchAsyncCallPlugin({ plugin, incomingData, user, platform, o
|
|
|
182
185
|
config: pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId }),
|
|
183
186
|
asyncTaskId: taskId,
|
|
184
187
|
callbackUrl,
|
|
188
|
+
hashedExtensionId: user.hashedRcExtensionId,
|
|
185
189
|
}, {
|
|
186
190
|
headers: {
|
|
187
191
|
Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
|
|
@@ -305,6 +309,7 @@ async function appendAsyncPluginNoteToCallLog({ taskCache, note }) {
|
|
|
305
309
|
...buildCallLogSessionWhere({
|
|
306
310
|
sessionId: taskData.sessionId,
|
|
307
311
|
extensionNumber: taskData.extensionNumber,
|
|
312
|
+
hashedExtensionId: taskData.hashedExtensionId,
|
|
308
313
|
}),
|
|
309
314
|
platform: taskData.platform,
|
|
310
315
|
userId: taskData.userId,
|
|
@@ -446,12 +451,14 @@ async function handleAsyncPluginCallback({ taskId, body }) {
|
|
|
446
451
|
async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
|
|
447
452
|
try {
|
|
448
453
|
const extensionNumber = getCallLogExtensionNumber(incomingData);
|
|
454
|
+
const hashedExtensionId = getCallLogHashedExtensionId(incomingData);
|
|
449
455
|
let existingCallLog = null;
|
|
450
456
|
try {
|
|
451
457
|
existingCallLog = await CallLogModel.findOne({
|
|
452
458
|
where: buildCallLogSessionWhere({
|
|
453
459
|
sessionId: incomingData.logInfo.sessionId,
|
|
454
460
|
extensionNumber,
|
|
461
|
+
hashedExtensionId,
|
|
455
462
|
})
|
|
456
463
|
});
|
|
457
464
|
}
|
|
@@ -610,6 +617,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
|
|
|
610
617
|
id: incomingData.logInfo.telephonySessionId || incomingData.logInfo.id,
|
|
611
618
|
sessionId: incomingData.logInfo.sessionId,
|
|
612
619
|
extensionNumber,
|
|
620
|
+
hashedExtensionId,
|
|
613
621
|
platform,
|
|
614
622
|
thirdPartyLogId: logId,
|
|
615
623
|
userId,
|
|
@@ -649,7 +657,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
|
|
|
649
657
|
}
|
|
650
658
|
}
|
|
651
659
|
|
|
652
|
-
async function getCallLog({ userId, sessionIds, extensionNumber, platform, requireDetails }) {
|
|
660
|
+
async function getCallLog({ userId, sessionIds, extensionNumber, hashedExtensionId, platform, requireDetails }) {
|
|
653
661
|
try {
|
|
654
662
|
let user = await UserModel.findByPk(userId);
|
|
655
663
|
if (!user || !user.accessToken) {
|
|
@@ -704,15 +712,16 @@ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requi
|
|
|
704
712
|
where: buildCallLogSessionWhere({
|
|
705
713
|
sessionIds: sessionIdsArray,
|
|
706
714
|
extensionNumber,
|
|
715
|
+
hashedExtensionId,
|
|
707
716
|
}),
|
|
708
|
-
order: [['extensionNumber', 'ASC']]
|
|
717
|
+
order: [['hashedExtensionId', 'ASC'], ['extensionNumber', 'ASC']]
|
|
709
718
|
});
|
|
710
719
|
for (const sId of sessionIdsArray) {
|
|
711
720
|
if (sId == 0) {
|
|
712
721
|
logs.push({ sessionId: sId, matched: false });
|
|
713
722
|
continue;
|
|
714
723
|
}
|
|
715
|
-
const callLog = findMatchingCallLog(callLogs, sId, extensionNumber);
|
|
724
|
+
const callLog = findMatchingCallLog(callLogs, sId, extensionNumber, hashedExtensionId);
|
|
716
725
|
if (!callLog) {
|
|
717
726
|
logs.push({ sessionId: sId, matched: false });
|
|
718
727
|
}
|
|
@@ -729,11 +738,12 @@ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requi
|
|
|
729
738
|
where: buildCallLogSessionWhere({
|
|
730
739
|
sessionIds: sessionIdsArray,
|
|
731
740
|
extensionNumber,
|
|
741
|
+
hashedExtensionId,
|
|
732
742
|
}),
|
|
733
|
-
order: [['extensionNumber', 'ASC']]
|
|
743
|
+
order: [['hashedExtensionId', 'ASC'], ['extensionNumber', 'ASC']]
|
|
734
744
|
});
|
|
735
745
|
for (const sId of sessionIdsArray) {
|
|
736
|
-
const callLog = findMatchingCallLog(callLogs, sId, extensionNumber);
|
|
746
|
+
const callLog = findMatchingCallLog(callLogs, sId, extensionNumber, hashedExtensionId);
|
|
737
747
|
if (!callLog) {
|
|
738
748
|
logs.push({ sessionId: sId, matched: false });
|
|
739
749
|
}
|
|
@@ -745,19 +755,21 @@ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requi
|
|
|
745
755
|
return { successful: true, logs, returnMessage, extraDataTracking };
|
|
746
756
|
}
|
|
747
757
|
catch (e) {
|
|
748
|
-
return handleApiError(e, platform, 'getCallLog', { userId, sessionIds, requireDetails, extensionNumber });
|
|
758
|
+
return handleApiError(e, platform, 'getCallLog', { userId, sessionIds, requireDetails, extensionNumber, hashedExtensionId });
|
|
749
759
|
}
|
|
750
760
|
}
|
|
751
761
|
|
|
752
762
|
async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
|
|
753
763
|
try {
|
|
754
764
|
const extensionNumber = getCallLogExtensionNumber(incomingData);
|
|
765
|
+
const hashedExtensionId = getCallLogHashedExtensionId(incomingData);
|
|
755
766
|
let existingCallLog = null;
|
|
756
767
|
try {
|
|
757
768
|
existingCallLog = await CallLogModel.findOne({
|
|
758
769
|
where: buildCallLogSessionWhere({
|
|
759
770
|
sessionId: incomingData.sessionId,
|
|
760
771
|
extensionNumber,
|
|
772
|
+
hashedExtensionId,
|
|
761
773
|
})
|
|
762
774
|
});
|
|
763
775
|
}
|
package/handlers/plugin.js
CHANGED
|
@@ -48,7 +48,14 @@ async function getPluginLicenseStatus({ rcAccountId, pluginId }) {
|
|
|
48
48
|
'Authorization': `Bearer ${accountData.data.jwtToken}`
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
|
-
|
|
51
|
+
const licenseStatus = licenseStatusResponse.data;
|
|
52
|
+
if (!licenseStatus || !Object.prototype.hasOwnProperty.call(licenseStatus, 'licenseStatus')) {
|
|
53
|
+
return {
|
|
54
|
+
licenseStatus: false,
|
|
55
|
+
licenseStatusDescription: 'Plugin license status unavailable'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return licenseStatus;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
function getRefreshedJwtTokenFromHeaders({ headers }) {
|
|
@@ -58,15 +65,15 @@ function getRefreshedJwtTokenFromHeaders({ headers }) {
|
|
|
58
65
|
return headers['x-refreshed-jwt-token'] || headers['X-Refreshed-Jwt-Token'] || null;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
async function resolvePluginManifest({ pluginId, pluginAccess,
|
|
68
|
+
async function resolvePluginManifest({ pluginId, pluginAccess, ownerRcAccountId, pluginName }) {
|
|
62
69
|
const manifestFetchers = [];
|
|
63
70
|
if (pluginAccess === 'public') {
|
|
64
71
|
manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?type=plugin`);
|
|
65
72
|
} else if (pluginAccess === 'private' || pluginAccess === 'shared') {
|
|
66
|
-
manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${
|
|
73
|
+
manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${ownerRcAccountId}`);
|
|
67
74
|
} else {
|
|
68
75
|
manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?type=plugin`);
|
|
69
|
-
manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${
|
|
76
|
+
manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${ownerRcAccountId}`);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
let pluginData = null;
|
|
@@ -132,8 +139,8 @@ async function persistPluginData({ rcAccountId, pluginId, jwtToken, pluginData =
|
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
async function registerPluginAccount({ pluginId, rcAccessToken, rcAccountId, pluginAccess, pluginName }) {
|
|
136
|
-
const { pluginManifest } = await resolvePluginManifest({ pluginId, pluginAccess,
|
|
142
|
+
async function registerPluginAccount({ pluginId, rcAccessToken, rcAccountId, pluginAccess, pluginName, ownerRcAccountId }) {
|
|
143
|
+
const { pluginManifest } = await resolvePluginManifest({ pluginId, pluginAccess, ownerRcAccountId, pluginName });
|
|
137
144
|
if (!pluginManifest?.endpointUrl) {
|
|
138
145
|
throw new Error(`Plugin endpoint URL not found for ${pluginId}`);
|
|
139
146
|
}
|
package/index.js
CHANGED
|
@@ -33,6 +33,8 @@ const logger = require('./lib/logger');
|
|
|
33
33
|
const { DebugTracer } = require('./lib/debugTracer');
|
|
34
34
|
const s3ErrorLogReport = require('./lib/s3ErrorLogReport');
|
|
35
35
|
const pluginCore = require('./handlers/plugin');
|
|
36
|
+
const { sequelize } = require('./models/sequelize');
|
|
37
|
+
const { ensureCallLogsHashedExtensionIdSchema } = require('./lib/migrateCallLogsSchema');
|
|
36
38
|
const { handleDatabaseError } = require('./lib/errorHandler');
|
|
37
39
|
const { updateAuthSession } = require('./lib/authSession');
|
|
38
40
|
const managedAuthCore = require('./handlers/managedAuth');
|
|
@@ -71,6 +73,7 @@ async function initDB() {
|
|
|
71
73
|
await UserModel.sync();
|
|
72
74
|
await LlmSessionModel.sync();
|
|
73
75
|
await CallLogModel.sync();
|
|
76
|
+
await ensureCallLogsHashedExtensionIdSchema(sequelize);
|
|
74
77
|
await MessageLogModel.sync();
|
|
75
78
|
await AdminConfigModel.sync();
|
|
76
79
|
await CacheModel.sync();
|
|
@@ -118,13 +121,12 @@ function normalizeJwtFromRequest(req, res, next) {
|
|
|
118
121
|
const originalBearerToken = getBearerTokenFromRequest(req);
|
|
119
122
|
const queryToken = req.query?.jwtToken;
|
|
120
123
|
let bearerToken = originalBearerToken;
|
|
124
|
+
const isQueryAuth = !originalBearerToken && !!queryToken;
|
|
121
125
|
|
|
122
126
|
// Backward compatibility: promote query jwtToken to Authorization Bearer.
|
|
123
|
-
if (
|
|
127
|
+
if (isQueryAuth) {
|
|
124
128
|
req.headers.authorization = `Bearer ${queryToken}`;
|
|
125
129
|
bearerToken = queryToken;
|
|
126
|
-
// Don't refresh JWT because old version cannot update its local token storage to support refreshed token.
|
|
127
|
-
return next();
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
const token = bearerToken;
|
|
@@ -136,7 +138,7 @@ function normalizeJwtFromRequest(req, res, next) {
|
|
|
136
138
|
const decodedToken = jwt.decodeJwt(token);
|
|
137
139
|
if (!decodedToken?.id) {
|
|
138
140
|
req.invalidJwtToken = true;
|
|
139
|
-
if (req.query?.jwtToken) {
|
|
141
|
+
if (originalBearerToken && req.query?.jwtToken) {
|
|
140
142
|
delete req.query.jwtToken;
|
|
141
143
|
}
|
|
142
144
|
return next();
|
|
@@ -148,16 +150,17 @@ function normalizeJwtFromRequest(req, res, next) {
|
|
|
148
150
|
if (typeof decodedToken.exp === 'number') {
|
|
149
151
|
const now = Math.floor(Date.now() / 1000);
|
|
150
152
|
const timeLeft = decodedToken.exp - now;
|
|
151
|
-
const isBearerAuth = !!originalBearerToken;
|
|
152
153
|
const shouldRefreshNearExpiry = timeLeft <= JWT_REFRESH_THRESHOLD_SECONDS;
|
|
153
|
-
|
|
154
|
-
const shouldRefreshLegacyLongLivedBearer = isBearerAuth && timeLeft > JWT_LEGACY_LONG_LIVED_THRESHOLD_SECONDS;
|
|
155
|
-
if (shouldRefreshNearExpiry || shouldRefreshLegacyLongLivedBearer) {
|
|
154
|
+
if (shouldRefreshNearExpiry) {
|
|
156
155
|
const refreshedToken = jwt.generateJwt({
|
|
157
156
|
id: decodedToken.id.toString(),
|
|
158
157
|
platform: decodedToken.platform
|
|
159
158
|
});
|
|
160
159
|
res.setHeader('x-refreshed-jwt-token', refreshedToken);
|
|
160
|
+
req.headers.authorization = `Bearer ${refreshedToken}`;
|
|
161
|
+
if (isQueryAuth && req.query?.jwtToken) {
|
|
162
|
+
req.query.jwtToken = refreshedToken;
|
|
163
|
+
}
|
|
161
164
|
req.jwtToken = refreshedToken;
|
|
162
165
|
req.jwtAuth = jwt.decodeJwt(refreshedToken) || decodedToken;
|
|
163
166
|
}
|
|
@@ -1040,6 +1043,11 @@ function createCoreRouter() {
|
|
|
1040
1043
|
if (jwtToken) {
|
|
1041
1044
|
const unAuthData = jwt.decodeJwt(jwtToken);
|
|
1042
1045
|
platformName = unAuthData?.platform ?? 'Unknown';
|
|
1046
|
+
if (!unAuthData || !unAuthData?.id) {
|
|
1047
|
+
tracer?.trace('getUserSettings:noToken', {});
|
|
1048
|
+
res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1043
1051
|
const user = await UserModel.findByPk(unAuthData?.id);
|
|
1044
1052
|
if (!user) {
|
|
1045
1053
|
tracer?.trace('getUserSettings:userNotFound', {});
|
|
@@ -2031,6 +2039,7 @@ function createCoreRouter() {
|
|
|
2031
2039
|
userId,
|
|
2032
2040
|
sessionIds: req.query.sessionIds,
|
|
2033
2041
|
extensionNumber: req.query.extensionNumber,
|
|
2042
|
+
hashedExtensionId: req.query.hashedExtensionId,
|
|
2034
2043
|
platform,
|
|
2035
2044
|
requireDetails: req.query.requireDetails === 'true'
|
|
2036
2045
|
});
|
|
@@ -2227,6 +2236,7 @@ function createCoreRouter() {
|
|
|
2227
2236
|
userId,
|
|
2228
2237
|
sessionId: req.body.sessionId,
|
|
2229
2238
|
extensionNumber: req.body.extensionNumber,
|
|
2239
|
+
hashedExtensionId: req.body.hashedExtensionId,
|
|
2230
2240
|
dispositions: req.body.dispositions,
|
|
2231
2241
|
additionalSubmission: req.body.additionalSubmission
|
|
2232
2242
|
});
|
|
@@ -2753,7 +2763,7 @@ function createCoreRouter() {
|
|
|
2753
2763
|
let success = false;
|
|
2754
2764
|
const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
|
|
2755
2765
|
try {
|
|
2756
|
-
const { pluginId, rcAccountId, pluginAccess, pluginName } = req.body || {};
|
|
2766
|
+
const { pluginId, rcAccountId, pluginAccess, pluginName, ownerRcAccountId } = req.body || {};
|
|
2757
2767
|
const rcAccessToken = req.query?.rcAccessToken;
|
|
2758
2768
|
if (!pluginId || !rcAccountId) {
|
|
2759
2769
|
res.status(400).send(tracer ? tracer.wrapResponse({ successful: false, returnMessage: 'pluginId and rcAccountId are required' }) : { successful: false, returnMessage: 'pluginId and rcAccountId are required' });
|
|
@@ -2777,6 +2787,7 @@ function createCoreRouter() {
|
|
|
2777
2787
|
pluginId,
|
|
2778
2788
|
rcAccessToken,
|
|
2779
2789
|
rcAccountId: rcAccountId?.toString(),
|
|
2790
|
+
ownerRcAccountId: ownerRcAccountId?.toString(),
|
|
2780
2791
|
pluginAccess,
|
|
2781
2792
|
pluginName,
|
|
2782
2793
|
});
|
|
@@ -2915,7 +2926,7 @@ function createCoreRouter() {
|
|
|
2915
2926
|
router.get('/mockCallLog', async function (req, res) {
|
|
2916
2927
|
const secretKey = req.query.secretKey;
|
|
2917
2928
|
if (secretKey === process.env.APP_SERVER_SECRET_KEY) {
|
|
2918
|
-
const callLogs = await mock.getCallLog({ sessionIds: req.query.sessionIds, extensionNumber: req.query.extensionNumber });
|
|
2929
|
+
const callLogs = await mock.getCallLog({ sessionIds: req.query.sessionIds, extensionNumber: req.query.extensionNumber, hashedExtensionId: req.query.hashedExtensionId });
|
|
2919
2930
|
res.status(200).send(callLogs);
|
|
2920
2931
|
}
|
|
2921
2932
|
else {
|
|
@@ -2925,7 +2936,7 @@ function createCoreRouter() {
|
|
|
2925
2936
|
router.post('/mockCallLog', async function (req, res) {
|
|
2926
2937
|
const secretKey = req.query.secretKey;
|
|
2927
2938
|
if (secretKey === process.env.APP_SERVER_SECRET_KEY) {
|
|
2928
|
-
await mock.createCallLog({ sessionId: req.body.sessionId, extensionNumber: req.body.extensionNumber });
|
|
2939
|
+
await mock.createCallLog({ sessionId: req.body.sessionId, extensionNumber: req.body.extensionNumber, hashedExtensionId: req.body.hashedExtensionId });
|
|
2929
2940
|
res.status(200).send('Mock call log created');
|
|
2930
2941
|
}
|
|
2931
2942
|
else {
|
package/lib/callLogLookup.js
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
const Op = require('sequelize').Op;
|
|
2
|
+
const { getHashValue } = require('./util');
|
|
3
|
+
|
|
4
|
+
function stringOrEmpty(value) {
|
|
5
|
+
return value == null ? '' : value.toString();
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
function getCallLogExtensionNumber(incomingData) {
|
|
4
|
-
return (incomingData?.extensionNumber ?? incomingData?.logInfo?.extensionNumber)
|
|
9
|
+
return stringOrEmpty(incomingData?.extensionNumber ?? incomingData?.logInfo?.extensionNumber);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getCallLogHashedExtensionId(incomingData, hashKey = process.env.HASH_KEY) {
|
|
13
|
+
const hashedExtensionId = incomingData?.hashedExtensionId ?? incomingData?.logInfo?.hashedExtensionId;
|
|
14
|
+
if (hashedExtensionId) {
|
|
15
|
+
return hashedExtensionId.toString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rcExtensionId = incomingData?.rcExtensionId ?? incomingData?.logInfo?.rcExtensionId;
|
|
19
|
+
if (rcExtensionId && hashKey) {
|
|
20
|
+
return getHashValue(rcExtensionId.toString(), hashKey);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildLegacyIdentityWhere(extensionNumberValue) {
|
|
27
|
+
return {
|
|
28
|
+
extensionNumber: extensionNumberValue,
|
|
29
|
+
[Op.or]: [
|
|
30
|
+
{ hashedExtensionId: '' },
|
|
31
|
+
{ hashedExtensionId: null },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
5
34
|
}
|
|
6
35
|
|
|
7
|
-
function buildCallLogSessionWhere({ sessionId, sessionIds, extensionNumber }) {
|
|
36
|
+
function buildCallLogSessionWhere({ sessionId, sessionIds, extensionNumber, hashedExtensionId }) {
|
|
8
37
|
const where = {};
|
|
9
38
|
if (sessionIds) {
|
|
10
39
|
where.sessionId = {
|
|
@@ -14,21 +43,65 @@ function buildCallLogSessionWhere({ sessionId, sessionIds, extensionNumber }) {
|
|
|
14
43
|
else {
|
|
15
44
|
where.sessionId = sessionId;
|
|
16
45
|
}
|
|
17
|
-
|
|
18
|
-
|
|
46
|
+
|
|
47
|
+
const extensionNumberValue = stringOrEmpty(extensionNumber);
|
|
48
|
+
const hashedExtensionIdValue = stringOrEmpty(hashedExtensionId);
|
|
49
|
+
|
|
50
|
+
if (hashedExtensionIdValue) {
|
|
51
|
+
const identityWhere = [
|
|
52
|
+
{ hashedExtensionId: hashedExtensionIdValue },
|
|
53
|
+
];
|
|
54
|
+
if (extensionNumberValue) {
|
|
55
|
+
identityWhere.push(buildLegacyIdentityWhere(extensionNumberValue));
|
|
56
|
+
}
|
|
57
|
+
identityWhere.push(buildLegacyIdentityWhere(''));
|
|
58
|
+
where[Op.or] = identityWhere;
|
|
59
|
+
}
|
|
60
|
+
else if (extensionNumberValue) {
|
|
19
61
|
where.extensionNumber = extensionNumberValue;
|
|
20
62
|
}
|
|
21
63
|
return where;
|
|
22
64
|
}
|
|
23
65
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
66
|
+
function hasEmptyHashedExtensionId(callLog) {
|
|
67
|
+
return !stringOrEmpty(callLog.hashedExtensionId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findMatchingCallLog(callLogs, sessionId, extensionNumber, hashedExtensionId) {
|
|
71
|
+
const extensionNumberValue = stringOrEmpty(extensionNumber);
|
|
72
|
+
const hashedExtensionIdValue = stringOrEmpty(hashedExtensionId);
|
|
73
|
+
const sessionLogs = callLogs.filter(callLog => callLog.sessionId === sessionId);
|
|
74
|
+
|
|
75
|
+
if (hashedExtensionIdValue) {
|
|
76
|
+
const exactHashedLog = sessionLogs.find(callLog => (
|
|
77
|
+
stringOrEmpty(callLog.hashedExtensionId) === hashedExtensionIdValue
|
|
78
|
+
));
|
|
79
|
+
if (exactHashedLog) {
|
|
80
|
+
return exactHashedLog;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (extensionNumberValue) {
|
|
84
|
+
const legacyExtensionLog = sessionLogs.find(callLog => (
|
|
85
|
+
hasEmptyHashedExtensionId(callLog) &&
|
|
86
|
+
stringOrEmpty(callLog.extensionNumber) === extensionNumberValue
|
|
87
|
+
));
|
|
88
|
+
if (legacyExtensionLog) {
|
|
89
|
+
return legacyExtensionLog;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return sessionLogs.find(callLog => (
|
|
94
|
+
hasEmptyHashedExtensionId(callLog) &&
|
|
95
|
+
!stringOrEmpty(callLog.extensionNumber)
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sessionLogs.find(callLog => (
|
|
100
|
+
!extensionNumberValue || stringOrEmpty(callLog.extensionNumber) === extensionNumberValue
|
|
29
101
|
));
|
|
30
102
|
}
|
|
31
103
|
|
|
32
104
|
exports.getCallLogExtensionNumber = getCallLogExtensionNumber;
|
|
105
|
+
exports.getCallLogHashedExtensionId = getCallLogHashedExtensionId;
|
|
33
106
|
exports.buildCallLogSessionWhere = buildCallLogSessionWhere;
|
|
34
107
|
exports.findMatchingCallLog = findMatchingCallLog;
|