@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.
Files changed (49) hide show
  1. package/connector/mock.js +9 -4
  2. package/connector/proxy/engine.js +3 -2
  3. package/connector/proxy/index.js +2 -2
  4. package/docs/architecture.md +1 -0
  5. package/docs/handlers.md +1 -1
  6. package/docs/models.md +2 -0
  7. package/docs/routes.md +1 -1
  8. package/handlers/disposition.js +3 -2
  9. package/handlers/log.js +18 -6
  10. package/handlers/plugin.js +13 -6
  11. package/index.js +22 -11
  12. package/lib/callLogLookup.js +82 -9
  13. package/lib/migrateCallLogsSchema.js +128 -7
  14. package/models/callLogModel.js +6 -0
  15. package/package.json +1 -1
  16. package/releaseNotes.json +44 -0
  17. package/test/connector/developerPortal.test.js +166 -0
  18. package/test/connector/mock.test.js +131 -0
  19. package/test/connector/proxy/engine.test.js +85 -0
  20. package/test/connector/proxy/index.test.js +246 -0
  21. package/test/connector/proxy/sample.json +1 -0
  22. package/test/handlers/admin.test.js +344 -0
  23. package/test/handlers/appointment.test.js +260 -0
  24. package/test/handlers/auth.test.js +6 -2
  25. package/test/handlers/calldown.test.js +310 -0
  26. package/test/handlers/disposition.test.js +396 -0
  27. package/test/handlers/log.test.js +327 -3
  28. package/test/handlers/managedOAuth.test.js +262 -0
  29. package/test/handlers/plugin.test.js +305 -0
  30. package/test/handlers/user.test.js +381 -0
  31. package/test/index.test.js +166 -1
  32. package/test/lib/analytics.test.js +146 -0
  33. package/test/lib/authSession.test.js +173 -0
  34. package/test/lib/encode.test.js +59 -0
  35. package/test/lib/errorHandler.test.js +246 -0
  36. package/test/lib/generalErrorMessage.test.js +82 -0
  37. package/test/lib/s3ErrorLogReport.test.js +187 -0
  38. package/test/mcp/mcpHandlerMore.test.js +384 -0
  39. package/test/mcp/tools/appointmentTools.test.js +362 -0
  40. package/test/models/callDownListModel.test.js +125 -0
  41. package/test/models/dynamo/lockSchema.test.js +37 -0
  42. package/test/models/dynamo/noteCacheSchema.test.js +45 -0
  43. package/test/models/llmSessionModel.test.js +91 -0
  44. package/test/models/models.test.js +92 -0
  45. package/test/routes/calldownRoutes.test.js +224 -0
  46. package/test/routes/coreRouterBroadRoutes.test.js +855 -0
  47. package/test/routes/dispositionRoutes.test.js +192 -0
  48. package/test/routes/managedAuthRoutes.test.js +151 -0
  49. 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?.findContact?.responseMapping;
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
  };
@@ -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,
@@ -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 |
@@ -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
  }
@@ -48,7 +48,14 @@ async function getPluginLicenseStatus({ rcAccountId, pluginId }) {
48
48
  'Authorization': `Bearer ${accountData.data.jwtToken}`
49
49
  }
50
50
  });
51
- return licenseStatusResponse.data;
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, rcAccountId, pluginName }) {
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=${rcAccountId}`);
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=${rcAccountId}`);
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, rcAccountId, pluginName });
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 (!bearerToken && queryToken) {
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
- // Rotate legacy 120y tokens only for Bearer auth clients (they can persist refreshed headers).
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 {
@@ -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)?.toString() ?? '';
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
- const extensionNumberValue = extensionNumber?.toString() ?? '';
18
- if (extensionNumberValue) {
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 findMatchingCallLog(callLogs, sessionId, extensionNumber) {
25
- const extensionNumberValue = extensionNumber?.toString() ?? '';
26
- return callLogs.find(callLog => (
27
- callLog.sessionId === sessionId &&
28
- (!extensionNumberValue || (callLog.extensionNumber?.toString() ?? '') === extensionNumberValue)
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;