@app-connect/core 1.7.23 → 1.7.24

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/handlers/admin.js CHANGED
@@ -6,6 +6,7 @@ const { RingCentral } = require('../lib/ringcentral');
6
6
  const { Connector } = require('../models/dynamo/connectorSchema');
7
7
  const logger = require('../lib/logger');
8
8
  const { handleDatabaseError } = require('../lib/errorHandler');
9
+ const { getHashValue } = require('../lib/util');
9
10
 
10
11
  const CALL_AGGREGATION_GROUPS = ["Company", "CompanyNumbers", "Users", "Queues", "IVRs", "IVAs", "SharedLines", "UserGroups", "Sites", "Departments"]
11
12
  const RC_EXTENSION_ENDPOINT = 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~';
@@ -108,7 +109,8 @@ async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo, groupBy
108
109
  clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
109
110
  redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
110
111
  });
111
- let adminConfig = await AdminConfigModel.findByPk(rcAccountId);
112
+ const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
113
+ let adminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
112
114
  const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
113
115
  if (isTokenExpired) {
114
116
  const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
@@ -116,7 +118,7 @@ async function getAdminReport({ rcAccountId, timezone, timeFrom, timeTo, groupBy
116
118
  expires_in: adminConfig.adminTokenExpiry,
117
119
  refresh_token_expires_in: adminConfig.adminTokenExpiry
118
120
  });
119
- adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: rcAccountId } });
121
+ adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: hashedRcAccountId } });
120
122
  }
121
123
  const callsAggregationData = await rcSDK.getCallsAggregationData({
122
124
  token: { access_token: adminConfig.adminAccessToken, token_type: 'Bearer' },
@@ -177,7 +179,8 @@ async function getUserReport({ rcAccountId, rcExtensionId, timezone, timeFrom, t
177
179
  clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
178
180
  redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
179
181
  });
180
- let adminConfig = await AdminConfigModel.findByPk(rcAccountId);
182
+ const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
183
+ let adminConfig = await AdminConfigModel.findByPk(hashedRcAccountId);
181
184
  const isTokenExpired = adminConfig.adminTokenExpiry < new Date();
182
185
  if (isTokenExpired) {
183
186
  const { access_token, refresh_token, expire_time } = await rcSDK.refreshToken({
@@ -185,7 +188,7 @@ async function getUserReport({ rcAccountId, rcExtensionId, timezone, timeFrom, t
185
188
  expires_in: adminConfig.adminTokenExpiry,
186
189
  refresh_token_expires_in: adminConfig.adminTokenExpiry
187
190
  });
188
- adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: rcAccountId } });
191
+ adminConfig = await AdminConfigModel.update({ adminAccessToken: access_token, adminRefreshToken: refresh_token, adminTokenExpiry: expire_time }, { where: { id: hashedRcAccountId } });
189
192
  }
190
193
  const callLogData = await rcSDK.getCallLogData({
191
194
  extensionId: rcExtensionId,
package/handlers/auth.js CHANGED
@@ -7,6 +7,7 @@ const adminCore = require('./admin');
7
7
  const { Connector } = require('../models/dynamo/connectorSchema');
8
8
  const { handleDatabaseError } = require('../lib/errorHandler');
9
9
  const managedAuthCore = require('./managedAuth');
10
+ const { getHashValue } = require('../lib/util');
10
11
 
11
12
  async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcExtensionId, isFromMCP = false }) {
12
13
  const callbackUri = query.callbackUri;
@@ -279,8 +280,9 @@ async function onRingcentralOAuthCallback({ code, rcAccountId }) {
279
280
  redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
280
281
  });
281
282
  const { access_token, refresh_token, expire_time } = await rcSDK.generateToken({ code });
283
+ const hashedRcAccountId = getHashValue(rcAccountId, process.env.HASH_KEY);
282
284
  await adminCore.updateAdminRcTokens({
283
- hashedRcAccountId: rcAccountId,
285
+ hashedRcAccountId,
284
286
  adminAccessToken: access_token,
285
287
  adminRefreshToken: refresh_token,
286
288
  adminTokenExpiry: expire_time
package/index.js CHANGED
@@ -89,6 +89,19 @@ async function initDB() {
89
89
  await UserModel.sync();
90
90
  logger.info('hashedRcExtensionId column added to users table');
91
91
  }
92
+
93
+ // if LlmSessionModel doesn't have expiry column, add it
94
+ const llmSessionTableName = LlmSessionModel.getTableName();
95
+ const llmSessionTableSchema = await queryInterface.describeTable(llmSessionTableName);
96
+ if (!llmSessionTableSchema.expiry) {
97
+ logger.info('adding expiry column to llmSessions table...');
98
+ await queryInterface.addColumn(llmSessionTableName, 'expiry', {
99
+ type: Sequelize.DATE,
100
+ allowNull: true,
101
+ });
102
+ await LlmSessionModel.sync();
103
+ logger.info('expiry column added to llmSessions table');
104
+ }
92
105
  }
93
106
  }
94
107
 
@@ -7,7 +7,20 @@
7
7
  const { CacheModel } = require('../models/cacheModel');
8
8
 
9
9
  const AUTH_SESSION_PREFIX = 'auth-session';
10
- const SESSION_EXPIRY_MINUTES = 5;
10
+ const PENDING_SESSION_EXPIRY_MINUTES = 5;
11
+ const SETTLED_SESSION_EXPIRY_MINUTES = 15;
12
+
13
+ function getSessionRecordId(sessionId) {
14
+ return `${AUTH_SESSION_PREFIX}-${sessionId}`;
15
+ }
16
+
17
+ function getExpiry(minutes) {
18
+ return new Date(Date.now() + minutes * 60 * 1000);
19
+ }
20
+
21
+ function isExpired(record) {
22
+ return Boolean(record?.expiry && record.expiry <= new Date());
23
+ }
11
24
 
12
25
  /**
13
26
  * Create (or reset) an auth session.
@@ -16,9 +29,13 @@ const SESSION_EXPIRY_MINUTES = 5;
16
29
  * correctly for the new attempt.
17
30
  */
18
31
  async function createAuthSession(sessionId, data) {
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() };
32
+ const id = getSessionRecordId(sessionId);
33
+ const expiry = getExpiry(PENDING_SESSION_EXPIRY_MINUTES);
34
+ const sessionData = {
35
+ platform: data.platform,
36
+ hostname: data.hostname || '',
37
+ createdAt: new Date().toISOString(),
38
+ };
22
39
 
23
40
  const existing = await CacheModel.findByPk(id);
24
41
  if (existing) {
@@ -39,10 +56,21 @@ async function createAuthSession(sessionId, data) {
39
56
  * Get an auth session by ID
40
57
  */
41
58
  async function getAuthSession(sessionId) {
42
- const record = await CacheModel.findByPk(`${AUTH_SESSION_PREFIX}-${sessionId}`);
43
-
59
+ const record = await CacheModel.findByPk(getSessionRecordId(sessionId));
60
+
44
61
  if (!record) return null;
45
-
62
+
63
+ if (isExpired(record)) {
64
+ if (record.status !== 'expired') {
65
+ await record.update({ status: 'expired' });
66
+ }
67
+ return {
68
+ sessionId: record.userId,
69
+ status: 'expired',
70
+ ...record.data
71
+ };
72
+ }
73
+
46
74
  return {
47
75
  sessionId: record.userId,
48
76
  status: record.status,
@@ -54,18 +82,22 @@ async function getAuthSession(sessionId) {
54
82
  * Update an auth session
55
83
  */
56
84
  async function updateAuthSession(sessionId, data) {
57
- const record = await CacheModel.findByPk(`${AUTH_SESSION_PREFIX}-${sessionId}`);
58
-
85
+ const record = await CacheModel.findByPk(getSessionRecordId(sessionId));
86
+
59
87
  if (!record) return;
60
-
88
+
61
89
  const existingData = record.data || {};
90
+ const nextStatus = data.status || record.status;
62
91
  await record.update({
63
- status: data.status || record.status,
92
+ status: nextStatus,
64
93
  data: {
65
94
  ...existingData,
66
95
  ...data,
67
96
  updatedAt: new Date().toISOString()
68
- }
97
+ },
98
+ expiry: nextStatus === 'pending'
99
+ ? getExpiry(PENDING_SESSION_EXPIRY_MINUTES)
100
+ : getExpiry(SETTLED_SESSION_EXPIRY_MINUTES)
69
101
  });
70
102
  }
71
103
 
package/mcp/mcpHandler.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const axios = require('axios');
10
+ const { Op } = require('sequelize');
10
11
  const tools = require('./tools');
11
12
  const { LlmSessionModel } = require('../models/llmSessionModel');
12
13
  const { CacheModel } = require('../models/cacheModel');
@@ -23,6 +24,8 @@ const path = require('path');
23
24
  */
24
25
  const WIDGET_VERSION = 10;
25
26
  const WIDGET_URI = `ui://widget/ConnectorList-v${WIDGET_VERSION}.html`;
27
+ const RC_EXTENSION_CACHE_KEY = 'rcExtensionId';
28
+ const RC_EXTENSION_CACHE_STATUS = 'resolved';
26
29
 
27
30
  const JSON_RPC_INTERNAL_ERROR = -32603;
28
31
  const JSON_RPC_METHOD_NOT_FOUND = -32601;
@@ -104,7 +107,7 @@ async function resolveSessionContext(rcAccessToken, openaiSessionId) {
104
107
 
105
108
  if (openaiSessionId) {
106
109
  try {
107
- const cached = await CacheModel.findByPk(`${openaiSessionId}-rcExtensionId`);
110
+ const cached = await CacheModel.findByPk(`${openaiSessionId}-${RC_EXTENSION_CACHE_KEY}`);
108
111
  if (cached?.data?.rcExtensionId && (!cached.expiry || cached.expiry > new Date())) {
109
112
  return { rcExtensionId: cached.data.rcExtensionId };
110
113
  }
@@ -118,11 +121,11 @@ async function resolveSessionContext(rcAccessToken, openaiSessionId) {
118
121
  rcExtensionId = await resolveRcExtensionId(rcAccessToken);
119
122
  if (openaiSessionId && rcExtensionId) {
120
123
  await CacheModel.upsert({
121
- id: `${openaiSessionId}-rcExtensionId`,
124
+ id: `${openaiSessionId}-${RC_EXTENSION_CACHE_KEY}`,
122
125
  userId: openaiSessionId,
123
- cacheKey: 'rcExtensionId',
126
+ cacheKey: RC_EXTENSION_CACHE_KEY,
124
127
  data: { rcExtensionId },
125
- status: 'active',
128
+ status: RC_EXTENSION_CACHE_STATUS,
126
129
  expiry: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24h TTL
127
130
  });
128
131
  }
@@ -203,15 +206,34 @@ async function handleMcpRequest(req, res) {
203
206
  toolArgs.rcExtensionId = rcExtensionId;
204
207
  if (!toolArgs.jwtToken) {
205
208
  let llmSession = await LlmSessionModel.findByPk(rcExtensionId);
209
+ if (llmSession?.expiry && llmSession.expiry < new Date()) {
210
+ await LlmSessionModel.destroy({ where: { id: rcExtensionId } });
211
+ llmSession = null;
212
+ }
206
213
  if (!llmSession?.jwtToken && openaiSessionId) {
207
214
  const fallback = await LlmSessionModel.findByPk(openaiSessionId);
208
215
  if (fallback?.jwtToken) {
209
- await LlmSessionModel.upsert({ id: rcExtensionId, jwtToken: fallback.jwtToken });
210
- llmSession = fallback;
216
+ const { id: fallbackUserId } = jwt.decodeJwt(fallback.jwtToken);
217
+ const fallbackUser = fallbackUserId
218
+ ? await UserModel.findByPk(fallbackUserId)
219
+ : null;
220
+ if (fallbackUser?.accessToken) {
221
+ await LlmSessionModel.upsert({ id: rcExtensionId, jwtToken: fallback.jwtToken, expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) });
222
+ llmSession = fallback;
223
+ }
211
224
  }
212
- else {
225
+ if (!llmSession?.jwtToken) {
213
226
  const hashedRcExtensionId = getHashValue(rcExtensionId, process.env.HASH_KEY);
214
- const user = await UserModel.findOne({ where: { hashedRcExtensionId } });
227
+ const user = await UserModel.findOne({
228
+ where: {
229
+ hashedRcExtensionId,
230
+ [Op.and]: [
231
+ { accessToken: { [Op.not]: null } },
232
+ { accessToken: { [Op.ne]: '' } },
233
+ ],
234
+ },
235
+ order: [['updatedAt', 'DESC']],
236
+ });
215
237
  if (user?.accessToken) {
216
238
  await LlmSessionModel.upsert({
217
239
  id: rcExtensionId,
@@ -219,6 +241,7 @@ async function handleMcpRequest(req, res) {
219
241
  id: user.id.toString(),
220
242
  platform: user.platform
221
243
  }),
244
+ expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
222
245
  });
223
246
  llmSession = await LlmSessionModel.findByPk(rcExtensionId);
224
247
  }
@@ -3,7 +3,7 @@ const { LlmSessionModel } = require('../../models/llmSessionModel');
3
3
 
4
4
  /**
5
5
  * MCP Tool: Check Auth Status
6
- *
6
+ *
7
7
  * Polls the status of an ongoing OAuth authentication session
8
8
  */
9
9
 
@@ -37,7 +37,7 @@ const toolDefinition = {
37
37
  async function execute(args) {
38
38
  try {
39
39
  // rcExtensionId is injected by mcpHandler after verifying the RC access
40
- // token. Using it as the DB key binds the CRM credential to a verified
40
+ // token. Using it as the DB key binds the CRM credential to a verified
41
41
  // RC identity.
42
42
  const { sessionId, rcExtensionId } = args;
43
43
  if (!rcExtensionId) {
@@ -53,16 +53,12 @@ async function execute(args) {
53
53
  }
54
54
 
55
55
  switch (session.status) {
56
- case 'completed': {
57
- // Guard against duplicate DB writes if polled concurrently
58
- try {
59
- await LlmSessionModel.create({
60
- id: rcExtensionId,
61
- jwtToken: session.jwtToken
62
- });
63
- } catch {
64
- // Record already exists from a prior poll — safe to ignore
65
- }
56
+ case 'completed':
57
+ await LlmSessionModel.upsert({
58
+ id: rcExtensionId,
59
+ jwtToken: session.jwtToken,
60
+ expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
61
+ });
66
62
  return {
67
63
  data: {
68
64
  status: 'completed',
@@ -71,7 +67,14 @@ async function execute(args) {
71
67
  message: 'IMPORTANT: Authentication successful! Keep jwtToken in memory for future use. DO NOT directly show it to user.'
72
68
  }
73
69
  };
74
- }
70
+
71
+ case 'expired':
72
+ return {
73
+ data: {
74
+ status: 'expired',
75
+ errorMessage: 'Authentication session expired. Ask the user to start the auth flow again.'
76
+ }
77
+ };
75
78
 
76
79
  case 'failed':
77
80
  return {
@@ -100,4 +103,3 @@ async function execute(args) {
100
103
 
101
104
  exports.definition = toolDefinition;
102
105
  exports.execute = execute;
103
-
@@ -1,4 +1,5 @@
1
1
  const axios = require('axios');
2
+ const { Op } = require('sequelize');
2
3
  const { UserModel } = require('../../models/userModel');
3
4
  const { getHashValue } = require('../../lib/util');
4
5
 
@@ -56,7 +57,16 @@ async function execute({ rcAccessToken, openaiSessionId } = {}) {
56
57
 
57
58
  // Check if user session already exists from Chrome extension
58
59
  const hashedRcExtensionId = getHashValue(rcExtensionId, process.env.HASH_KEY);
59
- const user = await UserModel.findOne({ where: { hashedRcExtensionId } });
60
+ const user = await UserModel.findOne({
61
+ where: {
62
+ hashedRcExtensionId,
63
+ [Op.and]: [
64
+ { accessToken: { [Op.not]: null } },
65
+ { accessToken: { [Op.ne]: '' } },
66
+ ],
67
+ },
68
+ order: [['updatedAt', 'DESC']],
69
+ });
60
70
  // Case: user exists, return user info in plain message
61
71
  if (user?.accessToken) {
62
72
  return {
@@ -0,0 +1,90 @@
1
+ const jwt = require('../../lib/jwt');
2
+ const { UserModel } = require('../../models/userModel');
3
+ const { RingCentral } = require('../../lib/ringcentral');
4
+
5
+ /**
6
+ * MCP Tool: Get Session Info
7
+ *
8
+ * Returns non-sensitive information about the current MCP/CRM session.
9
+ */
10
+
11
+ const toolDefinition = {
12
+ name: 'getSessionInfo',
13
+ description: 'Get the current user session info, including RingCentral identity and CRM connection status.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {},
17
+ required: []
18
+ },
19
+ annotations: {
20
+ readOnlyHint: true,
21
+ openWorldHint: false,
22
+ destructiveHint: false
23
+ }
24
+ };
25
+
26
+ /**
27
+ * Execute the getSessionInfo tool
28
+ * @param {Object} args - Tool arguments injected by mcpHandler
29
+ * @param {string} [args.openaiSessionId] - OpenAI session identifier
30
+ * @param {string} [args.rcExtensionId] - Verified RingCentral extension identifier
31
+ * @param {string} [args.jwtToken] - CRM JWT token
32
+ * @param {string} [args.rcAccessToken] - RingCentral access token
33
+ * @returns {Object} Result object with session information
34
+ */
35
+ async function execute(args = {}) {
36
+ try {
37
+ const {
38
+ openaiSessionId = null,
39
+ rcExtensionId = null,
40
+ jwtToken,
41
+ rcAccessToken,
42
+ } = args;
43
+
44
+ const decodedToken = jwtToken ? jwt.decodeJwt(jwtToken) : null;
45
+ const userId = decodedToken?.id ?? null;
46
+ const user = userId ? await UserModel.findByPk(userId) : null;
47
+
48
+ let rcExtensionInfo = null;
49
+ if (rcExtensionId && rcAccessToken) {
50
+ const rcSDK = new RingCentral({
51
+ server: process.env.RINGCENTRAL_SERVER,
52
+ clientId: process.env.RINGCENTRAL_CLIENT_ID,
53
+ clientSecret: process.env.RINGCENTRAL_CLIENT_SECRET,
54
+ redirectUri: `${process.env.APP_SERVER}/ringcentral/oauth/callback`
55
+ });
56
+ rcExtensionInfo = await rcSDK.getExtensionInfo(rcExtensionId, {
57
+ access_token: rcAccessToken,
58
+ token_type: 'Bearer'
59
+ });
60
+ }
61
+ return {
62
+ success: true,
63
+ data: {
64
+ openaiSessionId,
65
+ dataToShow: {
66
+ isCrmAuthenticated: Boolean(decodedToken && user?.accessToken),
67
+ ringcentral: {
68
+ extensionId: rcExtensionId ?? null,
69
+ name: rcExtensionInfo?.name ?? null,
70
+ },
71
+ crm: {
72
+ userId,
73
+ platform: decodedToken?.platform ?? user?.platform ?? null,
74
+ hostname: user?.hostname ?? null
75
+ }
76
+ }
77
+ }
78
+ };
79
+ }
80
+ catch (error) {
81
+ return {
82
+ success: false,
83
+ error: error.message || 'Unknown error occurred',
84
+ errorDetails: error.stack
85
+ };
86
+ }
87
+ }
88
+
89
+ exports.definition = toolDefinition;
90
+ exports.execute = execute;
@@ -8,6 +8,7 @@
8
8
 
9
9
  const getHelp = require('./getHelp');
10
10
  const getPublicConnectors = require('./getPublicConnectors');
11
+ const getSessionInfo = require('./getSessionInfo');
11
12
  const doAuth = require('./doAuth');
12
13
  const checkAuthStatus = require('./checkAuthStatus');
13
14
  const logout = require('./logout');
@@ -15,13 +16,14 @@ const findContact = require('./findContactByPhone');
15
16
  const findContactWithName = require('./findContactByName');
16
17
  const createCallLog = require('./createCallLog');
17
18
  const rcGetCallLogs = require('./rcGetCallLogs');
18
- const getGoogleFilePicker = require('./getGoogleFilePicker');
19
+ // const getGoogleFilePicker = require('./getGoogleFilePicker');
19
20
  const createContact = require('./createContact');
20
21
 
21
22
  // AI-visible MCP tools — registered in the MCP server
22
23
  module.exports.tools = [
23
24
  getHelp,
24
25
  getPublicConnectors,
26
+ getSessionInfo,
25
27
  logout,
26
28
  findContact,
27
29
  findContactWithName,
@@ -1,8 +1,11 @@
1
1
  const jwt = require('../../lib/jwt');
2
2
  const { UserModel } = require('../../models/userModel');
3
3
  const { LlmSessionModel } = require('../../models/llmSessionModel');
4
+ const { CacheModel } = require('../../models/cacheModel');
4
5
  const connectorRegistry = require('../../connector/registry');
5
6
 
7
+ const RC_EXTENSION_CACHE_KEY = 'rcExtensionId';
8
+
6
9
  /**
7
10
  * MCP Tool: Logout
8
11
  *
@@ -38,7 +41,7 @@ function isMissingSessionTableError(error) {
38
41
  */
39
42
  async function execute(args) {
40
43
  try {
41
- const { jwtToken } = args;
44
+ const { jwtToken, rcExtensionId, openaiSessionId } = args;
42
45
  const session = jwt.decodeJwt(jwtToken);
43
46
  if (!session?.platform || !session?.id) {
44
47
  throw new Error('Invalid JWT token');
@@ -46,6 +49,12 @@ async function execute(args) {
46
49
  const { platform, id } = session;
47
50
  try {
48
51
  await LlmSessionModel.destroy({ where: { id } });
52
+ if (rcExtensionId && rcExtensionId !== id) {
53
+ await LlmSessionModel.destroy({ where: { id: rcExtensionId } });
54
+ }
55
+ if (openaiSessionId) {
56
+ await CacheModel.destroy({ where: { id: `${openaiSessionId}-${RC_EXTENSION_CACHE_KEY}` } });
57
+ }
49
58
  }
50
59
  catch (error) {
51
60
  if (!isMissingSessionTableError(error)) {
@@ -10,5 +10,8 @@ exports.LlmSessionModel = sequelize.define('llmSessions', {
10
10
  },
11
11
  jwtToken: {
12
12
  type: Sequelize.STRING,
13
+ },
14
+ expiry: {
15
+ type: Sequelize.DATE
13
16
  }
14
17
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.23",
3
+ "version": "1.7.24",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  {
2
+ "1.7.24": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Click-to-dial number matcher selection in User Setting -> General"
7
+ },
8
+ {
9
+ "type": "Fix",
10
+ "description": "Error report upload"
11
+ }
12
+ ]
13
+ },
2
14
  "1.7.23": {
3
15
  "global": [
4
16
  {
@@ -0,0 +1,82 @@
1
+ const checkAuthStatus = require('../../../mcp/tools/checkAuthStatus');
2
+ const { getAuthSession } = require('../../../lib/authSession');
3
+ const { LlmSessionModel } = require('../../../models/llmSessionModel');
4
+
5
+ jest.mock('../../../lib/authSession');
6
+ jest.mock('../../../models/llmSessionModel');
7
+
8
+ describe('MCP Tool: checkAuthStatus', () => {
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ LlmSessionModel.upsert.mockResolvedValue([{}, true]);
12
+ });
13
+
14
+ test('should return pending when auth session is still waiting', async () => {
15
+ getAuthSession.mockResolvedValue({ status: 'pending' });
16
+
17
+ const result = await checkAuthStatus.execute({
18
+ sessionId: 'session-1',
19
+ rcExtensionId: 'rc-ext-1'
20
+ });
21
+
22
+ expect(result).toEqual({
23
+ data: {
24
+ status: 'pending'
25
+ }
26
+ });
27
+ expect(LlmSessionModel.upsert).not.toHaveBeenCalled();
28
+ });
29
+
30
+ test('should persist completed auth against rcExtensionId', async () => {
31
+ getAuthSession.mockResolvedValue({
32
+ status: 'completed',
33
+ jwtToken: 'jwt-token',
34
+ userInfo: { id: 'user-1', name: 'Casey' }
35
+ });
36
+
37
+ const result = await checkAuthStatus.execute({
38
+ sessionId: 'session-1',
39
+ rcExtensionId: 'rc-ext-1'
40
+ });
41
+
42
+ expect(LlmSessionModel.upsert).toHaveBeenCalledWith({
43
+ id: 'rc-ext-1',
44
+ jwtToken: 'jwt-token'
45
+ });
46
+ expect(result).toEqual({
47
+ data: {
48
+ status: 'completed',
49
+ jwtToken: 'jwt-token',
50
+ userInfo: { id: 'user-1', name: 'Casey' },
51
+ message: expect.stringContaining('IMPORTANT')
52
+ }
53
+ });
54
+ });
55
+
56
+ test('should return expired when the auth session TTL has elapsed', async () => {
57
+ getAuthSession.mockResolvedValue({ status: 'expired' });
58
+
59
+ const result = await checkAuthStatus.execute({
60
+ sessionId: 'session-1',
61
+ rcExtensionId: 'rc-ext-1'
62
+ });
63
+
64
+ expect(result).toEqual({
65
+ data: {
66
+ status: 'expired',
67
+ errorMessage: 'Authentication session expired. Ask the user to start the auth flow again.'
68
+ }
69
+ });
70
+ });
71
+
72
+ test('should return an error when rcExtensionId is missing', async () => {
73
+ const result = await checkAuthStatus.execute({
74
+ sessionId: 'session-1'
75
+ });
76
+
77
+ expect(result).toEqual({
78
+ success: false,
79
+ error: 'CRM auth status check error: rcExtensionId is required'
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,127 @@
1
+ const getSessionInfo = require('../../../mcp/tools/getSessionInfo');
2
+ const jwt = require('../../../lib/jwt');
3
+ const { UserModel } = require('../../../models/userModel');
4
+ const { RingCentral } = require('../../../lib/ringcentral');
5
+
6
+ jest.mock('../../../lib/jwt');
7
+ jest.mock('../../../models/userModel');
8
+ jest.mock('../../../lib/ringcentral');
9
+
10
+ describe('MCP Tool: getSessionInfo', () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ RingCentral.mockImplementation(() => ({
14
+ getExtensionInfo: jest.fn().mockResolvedValue({ name: 'Demo Extension' })
15
+ }));
16
+ });
17
+
18
+ describe('tool definition', () => {
19
+ test('should have correct tool definition', () => {
20
+ expect(getSessionInfo.definition).toBeDefined();
21
+ expect(getSessionInfo.definition.name).toBe('getSessionInfo');
22
+ expect(getSessionInfo.definition.description).toContain('session info');
23
+ expect(getSessionInfo.definition.inputSchema).toBeDefined();
24
+ expect(getSessionInfo.definition.inputSchema.properties).toEqual({});
25
+ });
26
+ });
27
+
28
+ describe('execute', () => {
29
+ test('should return unauthenticated session info when no CRM jwtToken exists', async () => {
30
+ const result = await getSessionInfo.execute({
31
+ openaiSessionId: 'session-123',
32
+ rcExtensionId: 'ext-456',
33
+ rcAccessToken: 'rc-token'
34
+ });
35
+
36
+ expect(result).toEqual({
37
+ success: true,
38
+ data: {
39
+ openaiSessionId: 'session-123',
40
+ dataToShow: {
41
+ isCrmAuthenticated: false,
42
+ ringcentral: {
43
+ extensionId: 'ext-456',
44
+ name: 'Demo Extension',
45
+ },
46
+ crm: {
47
+ userId: null,
48
+ platform: null,
49
+ hostname: null
50
+ }
51
+ }
52
+ }
53
+ });
54
+ expect(jwt.decodeJwt).not.toHaveBeenCalled();
55
+ expect(UserModel.findByPk).not.toHaveBeenCalled();
56
+ });
57
+
58
+ test('should return connected CRM session info when jwtToken resolves to a saved user', async () => {
59
+ jwt.decodeJwt.mockReturnValue({
60
+ id: 'crm-user-1',
61
+ platform: 'clio'
62
+ });
63
+ UserModel.findByPk.mockResolvedValue({
64
+ id: 'crm-user-1',
65
+ platform: 'clio',
66
+ hostname: 'app.clio.com',
67
+ accessToken: 'crm-access-token',
68
+ });
69
+
70
+ const result = await getSessionInfo.execute({
71
+ openaiSessionId: 'session-123',
72
+ rcExtensionId: 'ext-456',
73
+ rcAccessToken: 'rc-token',
74
+ jwtToken: 'jwt-token'
75
+ });
76
+
77
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('jwt-token');
78
+ expect(UserModel.findByPk).toHaveBeenCalledWith('crm-user-1');
79
+ expect(result).toEqual({
80
+ success: true,
81
+ data: {
82
+ openaiSessionId: 'session-123',
83
+ dataToShow: {
84
+ isCrmAuthenticated: true,
85
+ ringcentral: {
86
+ extensionId: 'ext-456',
87
+ name: 'Demo Extension',
88
+ },
89
+ crm: {
90
+ userId: 'crm-user-1',
91
+ platform: 'clio',
92
+ hostname: 'app.clio.com'
93
+ }
94
+ }
95
+ }
96
+ });
97
+ });
98
+
99
+ test('should report not authenticated when jwtToken is invalid', async () => {
100
+ jwt.decodeJwt.mockReturnValue(null);
101
+
102
+ const result = await getSessionInfo.execute({
103
+ jwtToken: 'bad-token'
104
+ });
105
+
106
+ expect(result).toEqual({
107
+ success: true,
108
+ data: {
109
+ openaiSessionId: null,
110
+ dataToShow: {
111
+ isCrmAuthenticated: false,
112
+ ringcentral: {
113
+ extensionId: null,
114
+ name: null,
115
+ },
116
+ crm: {
117
+ userId: null,
118
+ platform: null,
119
+ hostname: null
120
+ }
121
+ }
122
+ }
123
+ });
124
+ expect(UserModel.findByPk).not.toHaveBeenCalled();
125
+ });
126
+ });
127
+ });
@@ -1,16 +1,22 @@
1
1
  const logout = require('../../../mcp/tools/logout');
2
2
  const jwt = require('../../../lib/jwt');
3
3
  const { UserModel } = require('../../../models/userModel');
4
+ const { LlmSessionModel } = require('../../../models/llmSessionModel');
5
+ const { CacheModel } = require('../../../models/cacheModel');
4
6
  const connectorRegistry = require('../../../connector/registry');
5
7
 
6
8
  // Mock dependencies
7
9
  jest.mock('../../../lib/jwt');
8
10
  jest.mock('../../../models/userModel');
11
+ jest.mock('../../../models/llmSessionModel');
12
+ jest.mock('../../../models/cacheModel');
9
13
  jest.mock('../../../connector/registry');
10
14
 
11
15
  describe('MCP Tool: logout', () => {
12
16
  beforeEach(() => {
13
17
  jest.clearAllMocks();
18
+ LlmSessionModel.destroy.mockResolvedValue(1);
19
+ CacheModel.destroy.mockResolvedValue(1);
14
20
  });
15
21
 
16
22
  describe('tool definition', () => {
@@ -65,6 +71,7 @@ describe('MCP Tool: logout', () => {
65
71
  }
66
72
  });
67
73
  expect(jwt.decodeJwt).toHaveBeenCalledWith('mock-jwt-token');
74
+ expect(LlmSessionModel.destroy).toHaveBeenCalledWith({ where: { id: 'test-user-id' } });
68
75
  expect(UserModel.findByPk).toHaveBeenCalledWith('test-user-id');
69
76
  expect(connectorRegistry.getConnector).toHaveBeenCalledWith('testCRM');
70
77
  expect(mockConnector.unAuthorize).toHaveBeenCalledWith({
@@ -170,6 +177,57 @@ describe('MCP Tool: logout', () => {
170
177
  expect(consoleSpy).toHaveBeenCalled();
171
178
  consoleSpy.mockRestore();
172
179
  });
180
+
181
+ test('should clear both CRM user and RC extension session rows when rcExtensionId is provided', async () => {
182
+ const mockUser = {
183
+ id: 'test-user-id',
184
+ platform: 'testCRM'
185
+ };
186
+
187
+ jwt.decodeJwt.mockReturnValue({
188
+ id: 'test-user-id',
189
+ platform: 'testCRM'
190
+ });
191
+ UserModel.findByPk.mockResolvedValue(mockUser);
192
+ connectorRegistry.getConnector.mockReturnValue({
193
+ unAuthorize: jest.fn().mockResolvedValue({})
194
+ });
195
+
196
+ const result = await logout.execute({
197
+ jwtToken: 'mock-jwt-token',
198
+ rcExtensionId: 'rc-ext-123'
199
+ });
200
+
201
+ expect(result.success).toBe(true);
202
+ expect(LlmSessionModel.destroy).toHaveBeenNthCalledWith(1, { where: { id: 'test-user-id' } });
203
+ expect(LlmSessionModel.destroy).toHaveBeenNthCalledWith(2, { where: { id: 'rc-ext-123' } });
204
+ });
205
+
206
+ test('should clear the resolved rcExtension cache for the current OpenAI session on logout', async () => {
207
+ const mockUser = {
208
+ id: 'test-user-id',
209
+ platform: 'testCRM'
210
+ };
211
+
212
+ jwt.decodeJwt.mockReturnValue({
213
+ id: 'test-user-id',
214
+ platform: 'testCRM'
215
+ });
216
+ UserModel.findByPk.mockResolvedValue(mockUser);
217
+ connectorRegistry.getConnector.mockReturnValue({
218
+ unAuthorize: jest.fn().mockResolvedValue({})
219
+ });
220
+
221
+ const result = await logout.execute({
222
+ jwtToken: 'mock-jwt-token',
223
+ openaiSessionId: 'oa-session-123'
224
+ });
225
+
226
+ expect(result.success).toBe(true);
227
+ expect(CacheModel.destroy).toHaveBeenCalledWith({
228
+ where: { id: 'oa-session-123-rcExtensionId' }
229
+ });
230
+ });
173
231
  });
174
232
  });
175
233