@app-connect/core 1.7.10 → 1.7.12

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 (59) hide show
  1. package/connector/developerPortal.js +43 -0
  2. package/connector/proxy/index.js +10 -3
  3. package/connector/registry.js +8 -6
  4. package/handlers/admin.js +135 -22
  5. package/handlers/auth.js +89 -67
  6. package/handlers/calldown.js +10 -4
  7. package/handlers/contact.js +4 -104
  8. package/handlers/disposition.js +7 -145
  9. package/handlers/log.js +174 -258
  10. package/handlers/user.js +19 -6
  11. package/index.js +280 -47
  12. package/lib/analytics.js +3 -1
  13. package/lib/authSession.js +68 -0
  14. package/lib/callLogComposer.js +498 -420
  15. package/lib/errorHandler.js +206 -0
  16. package/lib/jwt.js +2 -0
  17. package/lib/logger.js +190 -0
  18. package/lib/oauth.js +21 -10
  19. package/lib/ringcentral.js +2 -10
  20. package/lib/sharedSMSComposer.js +471 -0
  21. package/mcp/SupportedPlatforms.md +12 -0
  22. package/mcp/lib/validator.js +91 -0
  23. package/mcp/mcpHandler.js +166 -0
  24. package/mcp/tools/checkAuthStatus.js +110 -0
  25. package/mcp/tools/collectAuthInfo.js +91 -0
  26. package/mcp/tools/createCallLog.js +308 -0
  27. package/mcp/tools/createContact.js +117 -0
  28. package/mcp/tools/createMessageLog.js +283 -0
  29. package/mcp/tools/doAuth.js +190 -0
  30. package/mcp/tools/findContactByName.js +92 -0
  31. package/mcp/tools/findContactByPhone.js +101 -0
  32. package/mcp/tools/getCallLog.js +98 -0
  33. package/mcp/tools/getGoogleFilePicker.js +103 -0
  34. package/mcp/tools/getHelp.js +44 -0
  35. package/mcp/tools/getPublicConnectors.js +53 -0
  36. package/mcp/tools/index.js +64 -0
  37. package/mcp/tools/logout.js +68 -0
  38. package/mcp/tools/rcGetCallLogs.js +78 -0
  39. package/mcp/tools/setConnector.js +69 -0
  40. package/mcp/tools/updateCallLog.js +122 -0
  41. package/models/cacheModel.js +3 -0
  42. package/package.json +71 -70
  43. package/releaseNotes.json +24 -0
  44. package/test/handlers/log.test.js +11 -4
  45. package/test/lib/logger.test.js +206 -0
  46. package/test/lib/ringcentral.test.js +0 -6
  47. package/test/lib/sharedSMSComposer.test.js +1084 -0
  48. package/test/mcp/tools/collectAuthInfo.test.js +234 -0
  49. package/test/mcp/tools/createCallLog.test.js +425 -0
  50. package/test/mcp/tools/createMessageLog.test.js +580 -0
  51. package/test/mcp/tools/doAuth.test.js +376 -0
  52. package/test/mcp/tools/findContactByName.test.js +263 -0
  53. package/test/mcp/tools/findContactByPhone.test.js +284 -0
  54. package/test/mcp/tools/getCallLog.test.js +286 -0
  55. package/test/mcp/tools/getGoogleFilePicker.test.js +281 -0
  56. package/test/mcp/tools/getPublicConnectors.test.js +128 -0
  57. package/test/mcp/tools/logout.test.js +169 -0
  58. package/test/mcp/tools/setConnector.test.js +177 -0
  59. package/test/mcp/tools/updateCallLog.test.js +346 -0
@@ -0,0 +1,206 @@
1
+ const logger = require('./logger');
2
+ const errorMessage = require('./generalErrorMessage');
3
+
4
+ /**
5
+ * Centralized error handler for API operations
6
+ * Handles common error patterns (rate limits, auth errors, etc.)
7
+ *
8
+ * @param {Error} error - The error object
9
+ * @param {string} platform - Platform name (clio, bullhorn, etc.)
10
+ * @param {string} operation - Operation name (createCallLog, findContact, etc.)
11
+ * @param {Object} additionalContext - Additional logging context
12
+ * @returns {Object} Standardized error response
13
+ */
14
+ function handleApiError(error, platform, operation, additionalContext = {}) {
15
+ const statusCode = error.response?.status ?? 'unknown';
16
+
17
+ // Log the error with full context
18
+ logger.error(`${operation} failed for platform ${platform}`, {
19
+ platform,
20
+ operation,
21
+ statusCode,
22
+ errorMessage: error.message,
23
+ errorStack: error.stack,
24
+ errorResponse: error.response?.data,
25
+ ...additionalContext,
26
+ });
27
+
28
+ // Rate limit error (429)
29
+ if (statusCode === 429) {
30
+ return {
31
+ successful: false,
32
+ returnMessage: errorMessage.rateLimitErrorMessage({ platform }),
33
+ extraDataTracking: {
34
+ statusCode,
35
+ },
36
+ };
37
+ }
38
+
39
+ // Authorization/Authentication errors (400-409)
40
+ if (statusCode >= 400 && statusCode < 410) {
41
+ return {
42
+ successful: false,
43
+ returnMessage: errorMessage.authorizationErrorMessage({ platform }),
44
+ extraDataTracking: {
45
+ statusCode,
46
+ },
47
+ };
48
+ }
49
+
50
+ // Get operation-specific error message
51
+ const defaultErrorMessage = getOperationErrorMessage(operation, platform);
52
+
53
+ return {
54
+ successful: false,
55
+ returnMessage: defaultErrorMessage,
56
+ extraDataTracking: {
57
+ statusCode,
58
+ },
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Get operation-specific error message
64
+ * @param {string} operation - Operation name
65
+ * @param {string} platform - Platform name
66
+ * @returns {Object} Error message object
67
+ */
68
+ function getOperationErrorMessage(operation, platform) {
69
+ const operationMessages = {
70
+ createCallLog: {
71
+ message: 'Error creating call log',
72
+ details: ['Please check if your account has permission to CREATE logs.'],
73
+ },
74
+ updateCallLog: {
75
+ message: 'Error updating call log',
76
+ details: [`Please check if the log entity still exists on ${platform} and your account has permission to EDIT logs.`],
77
+ },
78
+ getCallLog: {
79
+ message: 'Error getting call log',
80
+ details: ['Please check if your account has permission to READ logs.'],
81
+ },
82
+ createMessageLog: {
83
+ message: 'Error creating message log',
84
+ details: ['Please check if your account has permission to CREATE logs.'],
85
+ },
86
+ updateMessageLog: {
87
+ message: 'Error updating message log',
88
+ details: [`Please check if the log entity still exists on ${platform} and your account has permission to EDIT logs.`],
89
+ },
90
+ findContact: {
91
+ message: 'Error finding contact',
92
+ details: ['Please check if your account has permission to GET contacts.'],
93
+ },
94
+ createContact: {
95
+ message: 'Error creating contact',
96
+ details: ['Please check if your account has permission to CREATE contacts.'],
97
+ },
98
+ findContactWithName: {
99
+ message: 'Error searching contacts',
100
+ details: ['Please check if your account has permission to GET contacts.'],
101
+ },
102
+ };
103
+
104
+ const operationInfo = operationMessages[operation] || {
105
+ message: `Error performing ${operation}`,
106
+ details: ['Please check if your account has the necessary permissions.'],
107
+ };
108
+
109
+ return {
110
+ message: operationInfo.message,
111
+ messageType: 'warning',
112
+ details: [
113
+ {
114
+ title: 'Details',
115
+ items: operationInfo.details.map((detail, index) => ({
116
+ id: index + 1,
117
+ type: 'text',
118
+ text: detail,
119
+ })),
120
+ },
121
+ ],
122
+ ttl: 5000,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Handle database errors
128
+ * @param {Error} error - The error object
129
+ * @param {string} operation - Database operation name
130
+ * @param {Object} context - Additional context
131
+ */
132
+ function handleDatabaseError(error, operation, context = {}) {
133
+ logger.error(`Database operation failed: ${operation}`, {
134
+ operation,
135
+ errorMessage: error.message,
136
+ errorStack: error.stack,
137
+ ...context,
138
+ });
139
+
140
+ return {
141
+ successful: false,
142
+ returnMessage: {
143
+ message: 'Database operation failed',
144
+ messageType: 'warning',
145
+ ttl: 5000,
146
+ },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Wrap async route handlers to catch errors
152
+ * Prevents unhandled promise rejections
153
+ * @param {Function} fn - Async route handler
154
+ * @returns {Function} Wrapped handler
155
+ */
156
+ function asyncHandler(fn) {
157
+ return (req, res, next) => {
158
+ Promise.resolve(fn(req, res, next)).catch(next);
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Express error handling middleware
164
+ * Should be added after all routes
165
+ * @param {Error} err - Error object
166
+ * @param {Object} req - Express request
167
+ * @param {Object} res - Express response
168
+ * @param {Function} next - Express next function (required by Express signature)
169
+ */
170
+ function errorMiddleware(err, req, res, next) { // eslint-disable-line no-unused-vars
171
+ const platform = req.platform || req.query?.platform || 'unknown';
172
+ const operation = req.route?.path || 'unknown';
173
+
174
+ logger.error('Request failed', {
175
+ platform,
176
+ operation,
177
+ method: req.method,
178
+ path: req.path,
179
+ statusCode: err.statusCode || 500,
180
+ error: err,
181
+ correlationId: req.correlationId,
182
+ });
183
+
184
+ // Don't expose internal errors in production
185
+ const message = process.env.NODE_ENV === 'production'
186
+ ? 'An internal error occurred'
187
+ : err.message;
188
+
189
+ res.status(err.statusCode || 500).json({
190
+ successful: false,
191
+ returnMessage: {
192
+ message,
193
+ messageType: 'error',
194
+ ttl: 5000,
195
+ },
196
+ });
197
+ }
198
+
199
+ module.exports = {
200
+ handleApiError,
201
+ handleDatabaseError,
202
+ asyncHandler,
203
+ errorMiddleware,
204
+ getOperationErrorMessage,
205
+ };
206
+
package/lib/jwt.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { sign, verify } = require('jsonwebtoken');
2
+ const logger = require('./logger');
2
3
 
3
4
  function generateJwt(data) {
4
5
  return sign(data, process.env.APP_SERVER_SECRET_KEY, { expiresIn: '120y' })
@@ -8,6 +9,7 @@ function decodeJwt(token) {
8
9
  try {
9
10
  return verify(token, process.env.APP_SERVER_SECRET_KEY);
10
11
  } catch (e) {
12
+ logger.error('Error decoding JWT', { stack: e.stack });
11
13
  return null;
12
14
  }
13
15
  }
package/lib/logger.js ADDED
@@ -0,0 +1,190 @@
1
+ const util = require('util');
2
+
3
+ /**
4
+ * Logger levels
5
+ */
6
+ const LOG_LEVELS = {
7
+ ERROR: 0,
8
+ WARN: 1,
9
+ INFO: 2,
10
+ DEBUG: 3,
11
+ };
12
+
13
+ /**
14
+ * Color codes for terminal output
15
+ */
16
+ const COLORS = {
17
+ ERROR: '\x1b[31m', // Red
18
+ WARN: '\x1b[33m', // Yellow
19
+ INFO: '\x1b[36m', // Cyan
20
+ DEBUG: '\x1b[90m', // Gray
21
+ RESET: '\x1b[0m',
22
+ };
23
+
24
+ class Logger {
25
+ constructor(options = {}) {
26
+ this.level = this._getLogLevel(options.level || process.env.LOG_LEVEL || 'INFO');
27
+ this.isProd = process.env.NODE_ENV === 'production';
28
+ this.enableColors = !this.isProd && process.stdout.isTTY;
29
+ }
30
+
31
+ _getLogLevel(levelName) {
32
+ const upperLevel = levelName.toUpperCase();
33
+ return LOG_LEVELS[upperLevel] !== undefined ? LOG_LEVELS[upperLevel] : LOG_LEVELS.INFO;
34
+ }
35
+
36
+ _shouldLog(level) {
37
+ return LOG_LEVELS[level] <= this.level;
38
+ }
39
+
40
+ _formatMessage(level, message, context = {}) {
41
+ const timestamp = new Date().toISOString();
42
+
43
+ if (this.isProd) {
44
+ // Production: JSON format for log aggregation tools
45
+ return JSON.stringify({
46
+ timestamp,
47
+ level,
48
+ message,
49
+ ...context,
50
+ });
51
+ } else {
52
+ // Development: Human-readable format with colors
53
+ const color = this.enableColors ? COLORS[level] : '';
54
+ const reset = this.enableColors ? COLORS.RESET : '';
55
+ const contextStr = Object.keys(context).length > 0
56
+ ? '\n' + util.inspect(context, { depth: 4, colors: this.enableColors })
57
+ : '';
58
+
59
+ return `${color}[${timestamp}] [${level}]${reset} ${message}${contextStr}`;
60
+ }
61
+ }
62
+
63
+ _log(level, message, context = {}) {
64
+ if (!this._shouldLog(level)) {
65
+ return;
66
+ }
67
+
68
+ const formattedMessage = this._formatMessage(level, message, context);
69
+
70
+ if (level === 'ERROR' || level === 'WARN') {
71
+ console.error(formattedMessage);
72
+ } else {
73
+ console.log(formattedMessage);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Log error level messages
79
+ * @param {string} message - Log message
80
+ * @param {Object} context - Additional context (error, stack, platform, etc.)
81
+ */
82
+ error(message, context = {}) {
83
+ // If context has an error object, extract useful information
84
+ let enrichedContext = context;
85
+ if (context.error instanceof Error) {
86
+ const { error, ...rest } = context;
87
+ enrichedContext = {
88
+ ...rest,
89
+ errorMessage: error.message,
90
+ errorStack: error.stack,
91
+ errorResponse: error.response?.data,
92
+ errorStatus: error.response?.status,
93
+ };
94
+ }
95
+
96
+ this._log('ERROR', message, enrichedContext);
97
+ }
98
+
99
+ /**
100
+ * Log warning level messages
101
+ * @param {string} message - Log message
102
+ * @param {Object} context - Additional context
103
+ */
104
+ warn(message, context = {}) {
105
+ this._log('WARN', message, context);
106
+ }
107
+
108
+ /**
109
+ * Log info level messages
110
+ * @param {string} message - Log message
111
+ * @param {Object} context - Additional context
112
+ */
113
+ info(message, context = {}) {
114
+ this._log('INFO', message, context);
115
+ }
116
+
117
+ /**
118
+ * Log debug level messages
119
+ * @param {string} message - Log message
120
+ * @param {Object} context - Additional context
121
+ */
122
+ debug(message, context = {}) {
123
+ this._log('DEBUG', message, context);
124
+ }
125
+
126
+ /**
127
+ * Create a child logger with default context
128
+ * Useful for adding request-specific or module-specific context
129
+ * @param {Object} defaultContext - Context to include in all logs
130
+ * @returns {Object} Child logger with bound context
131
+ */
132
+ child(defaultContext = {}) {
133
+ return {
134
+ error: (message, context = {}) => this.error(message, { ...defaultContext, ...context }),
135
+ warn: (message, context = {}) => this.warn(message, { ...defaultContext, ...context }),
136
+ info: (message, context = {}) => this.info(message, { ...defaultContext, ...context }),
137
+ debug: (message, context = {}) => this.debug(message, { ...defaultContext, ...context }),
138
+ child: (additionalContext) => this.child({ ...defaultContext, ...additionalContext }),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Log API request/response for debugging
144
+ * @param {Object} options - Request details
145
+ */
146
+ logApiRequest({ method, url, status, duration, platform, error }) {
147
+ const context = {
148
+ method,
149
+ url,
150
+ status,
151
+ duration: duration ? `${duration}ms` : undefined,
152
+ platform,
153
+ };
154
+
155
+ if (error) {
156
+ this.error('API request failed', { ...context, error });
157
+ } else if (status >= 400) {
158
+ this.warn('API request returned error status', context);
159
+ } else {
160
+ this.debug('API request completed', context);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Log database query for debugging
166
+ * @param {Object} options - Query details
167
+ */
168
+ logDatabaseQuery({ operation, table, duration, error }) {
169
+ const context = {
170
+ operation,
171
+ table,
172
+ duration: duration ? `${duration}ms` : undefined,
173
+ };
174
+
175
+ if (error) {
176
+ this.error('Database query failed', { ...context, error });
177
+ } else {
178
+ this.debug('Database query completed', context);
179
+ }
180
+ }
181
+ }
182
+
183
+ // Create singleton instance
184
+ const logger = new Logger();
185
+
186
+ // Export both the class and instance
187
+ module.exports = logger;
188
+ module.exports.Logger = Logger;
189
+ module.exports.LOG_LEVELS = LOG_LEVELS;
190
+
package/lib/oauth.js CHANGED
@@ -3,7 +3,8 @@ const ClientOAuth2 = require('client-oauth2');
3
3
  const moment = require('moment');
4
4
  const { UserModel } = require('../models/userModel');
5
5
  const connectorRegistry = require('../connector/registry');
6
-
6
+ const logger = require('./logger');
7
+ const { handleDatabaseError } = require('./errorHandler');
7
8
  // oauthApp strategy is default to 'code' which use credentials to get accessCode, then exchange for accessToken and refreshToken.
8
9
  // To change to other strategies, please refer to: https://github.com/mulesoft-labs/js-client-oauth2
9
10
  function getOAuthApp({ clientId, clientSecret, accessTokenUri, authorizationUri, redirectUri, scopes }) {
@@ -46,7 +47,7 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
46
47
  overwrite: false
47
48
  }
48
49
  );
49
- console.log('lock created')
50
+ logger.info('lock created')
50
51
  } catch (e) {
51
52
  // If creation failed due to condition, a lock exists
52
53
  if (e.name === 'ConditionalCheckFailedException' || e.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
@@ -54,7 +55,7 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
54
55
  if (!!lock?.ttl && moment(lock.ttl).unix() < now.unix()) {
55
56
  // Try to delete expired lock and create a new one atomically
56
57
  try {
57
- console.log('lock expired.')
58
+ logger.info('lock expired.')
58
59
  await lock.delete();
59
60
  newLock = await Lock.create(
60
61
  {
@@ -90,7 +91,7 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
90
91
  throw new Error('Token lock timeout');
91
92
  }
92
93
  user = await UserModel.findByPk(user.id);
93
- console.log('locked. bypass')
94
+ logger.info('locked. bypass')
94
95
  return user;
95
96
  }
96
97
  } else {
@@ -100,20 +101,25 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
100
101
  try {
101
102
  const startRefreshTime = moment();
102
103
  const token = oauthApp.createToken(user.accessToken, user.refreshToken);
103
- console.log('token refreshing...')
104
+ logger.info('token refreshing...')
104
105
  const { accessToken, refreshToken, expires } = await token.refresh();
105
106
  user.accessToken = accessToken;
106
107
  user.refreshToken = refreshToken;
107
108
  user.tokenExpiry = expires;
109
+ try {
108
110
  await user.save();
111
+ }
112
+ catch (error) {
113
+ return handleDatabaseError(error, 'Error saving user');
114
+ }
109
115
  if (newLock) {
110
116
  const deletionStartTime = moment();
111
117
  await newLock.delete();
112
118
  const deletionEndTime = moment();
113
- console.log(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
119
+ logger.info(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
114
120
  }
115
121
  const endRefreshTime = moment();
116
- console.log(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
122
+ logger.info(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
117
123
  }
118
124
  catch (e) {
119
125
  console.log('token refreshing failed', e.stack)
@@ -124,14 +130,19 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
124
130
  }
125
131
  // case: run withou token refresh lock
126
132
  else {
127
- console.log('token refreshing...')
133
+ logger.info('token refreshing...')
128
134
  const token = oauthApp.createToken(user.accessToken, user.refreshToken);
129
135
  const { accessToken, refreshToken, expires } = await token.refresh();
130
136
  user.accessToken = accessToken;
131
137
  user.refreshToken = refreshToken;
132
138
  user.tokenExpiry = expires;
133
- await user.save();
134
- console.log('token refreshing finished')
139
+ try {
140
+ await user.save();
141
+ }
142
+ catch (error) {
143
+ return handleDatabaseError(error, 'Error saving user');
144
+ }
145
+ logger.info('token refreshing finished')
135
146
  }
136
147
 
137
148
  }
@@ -52,8 +52,6 @@ class RingCentral {
52
52
  const {
53
53
  expires_in,
54
54
  refresh_token_expires_in,
55
- scope,
56
- endpoint_id, // do no save this field into db to reduce db size
57
55
  ...token
58
56
  } = await response.json();
59
57
  return {
@@ -79,8 +77,6 @@ class RingCentral {
79
77
  const {
80
78
  expires_in,
81
79
  refresh_token_expires_in,
82
- scope,
83
- endpoint_id, // do no save this field into db to reduce db size
84
80
  ...newToken
85
81
  } = await response.json();
86
82
  return {
@@ -164,10 +160,6 @@ class RingCentral {
164
160
  },
165
161
  }, token);
166
162
  const {
167
- uri,
168
- creationTime,
169
- deliveryMode,
170
- status, // do no save those field into db to reduce db size
171
163
  ...subscription
172
164
  } = await response.json();
173
165
  return subscription;
@@ -226,7 +218,7 @@ class RingCentral {
226
218
  return response.json();
227
219
  }
228
220
 
229
- async getCallLogData({ extensionId = '~', token, timezone, timeFrom, timeTo }) {
221
+ async getCallLogData({ extensionId = '~', token, timeFrom, timeTo }) {
230
222
  let pageStart = 1;
231
223
  let isFinalPage = false;
232
224
  let callLogResponse = null;
@@ -247,7 +239,7 @@ class RingCentral {
247
239
  }
248
240
  return result;
249
241
  }
250
- async getSMSData({ extensionId = '~', token, timezone, timeFrom, timeTo }) {
242
+ async getSMSData({ extensionId = '~', token, timeFrom, timeTo }) {
251
243
  let pageStart = 1;
252
244
  let isFinalPage = false;
253
245
  let smsLogResponse = null;