@app-connect/core 1.7.8 → 1.7.11

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 (69) 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 +44 -21
  5. package/handlers/auth.js +97 -69
  6. package/handlers/calldown.js +10 -4
  7. package/handlers/contact.js +45 -112
  8. package/handlers/disposition.js +4 -142
  9. package/handlers/log.js +174 -259
  10. package/handlers/user.js +19 -6
  11. package/index.js +310 -122
  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 -12
  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 +90 -0
  25. package/mcp/tools/collectAuthInfo.js +86 -0
  26. package/mcp/tools/createCallLog.js +299 -0
  27. package/mcp/tools/createMessageLog.js +283 -0
  28. package/mcp/tools/doAuth.js +185 -0
  29. package/mcp/tools/findContactByName.js +87 -0
  30. package/mcp/tools/findContactByPhone.js +96 -0
  31. package/mcp/tools/getCallLog.js +98 -0
  32. package/mcp/tools/getHelp.js +39 -0
  33. package/mcp/tools/getPublicConnectors.js +46 -0
  34. package/mcp/tools/index.js +58 -0
  35. package/mcp/tools/logout.js +63 -0
  36. package/mcp/tools/rcGetCallLogs.js +73 -0
  37. package/mcp/tools/setConnector.js +64 -0
  38. package/mcp/tools/updateCallLog.js +122 -0
  39. package/models/accountDataModel.js +34 -0
  40. package/models/cacheModel.js +3 -0
  41. package/package.json +6 -4
  42. package/releaseNotes.json +36 -0
  43. package/test/connector/registry.test.js +145 -0
  44. package/test/handlers/admin.test.js +583 -0
  45. package/test/handlers/auth.test.js +355 -0
  46. package/test/handlers/contact.test.js +852 -0
  47. package/test/handlers/log.test.js +872 -0
  48. package/test/lib/callLogComposer.test.js +1231 -0
  49. package/test/lib/debugTracer.test.js +328 -0
  50. package/test/lib/logger.test.js +206 -0
  51. package/test/lib/oauth.test.js +359 -0
  52. package/test/lib/ringcentral.test.js +473 -0
  53. package/test/lib/sharedSMSComposer.test.js +1084 -0
  54. package/test/lib/util.test.js +282 -0
  55. package/test/mcp/tools/collectAuthInfo.test.js +192 -0
  56. package/test/mcp/tools/createCallLog.test.js +412 -0
  57. package/test/mcp/tools/createMessageLog.test.js +580 -0
  58. package/test/mcp/tools/doAuth.test.js +363 -0
  59. package/test/mcp/tools/findContactByName.test.js +263 -0
  60. package/test/mcp/tools/findContactByPhone.test.js +284 -0
  61. package/test/mcp/tools/getCallLog.test.js +286 -0
  62. package/test/mcp/tools/getPublicConnectors.test.js +128 -0
  63. package/test/mcp/tools/logout.test.js +169 -0
  64. package/test/mcp/tools/setConnector.test.js +177 -0
  65. package/test/mcp/tools/updateCallLog.test.js +346 -0
  66. package/test/models/accountDataModel.test.js +98 -0
  67. package/test/models/dynamo/connectorSchema.test.js +189 -0
  68. package/test/models/models.test.js +539 -0
  69. package/test/setup.js +176 -176
@@ -0,0 +1,363 @@
1
+ const doAuth = require('../../../mcp/tools/doAuth');
2
+ const authCore = require('../../../handlers/auth');
3
+ const jwt = require('../../../lib/jwt');
4
+
5
+ // Mock dependencies
6
+ jest.mock('../../../handlers/auth');
7
+ jest.mock('../../../lib/jwt');
8
+
9
+ describe('MCP Tool: doAuth', () => {
10
+ beforeEach(() => {
11
+ jest.clearAllMocks();
12
+ process.env.APP_SERVER_SECRET_KEY = 'test-secret-key';
13
+ });
14
+
15
+ describe('tool definition', () => {
16
+ test('should have correct tool definition', () => {
17
+ expect(doAuth.definition).toBeDefined();
18
+ expect(doAuth.definition.name).toBe('doAuth');
19
+ expect(doAuth.definition.description).toContain('Auth flow step.4');
20
+ expect(doAuth.definition.inputSchema).toBeDefined();
21
+ });
22
+
23
+ test('should have optional parameters', () => {
24
+ expect(doAuth.definition.inputSchema.properties).toHaveProperty('connectorManifest');
25
+ expect(doAuth.definition.inputSchema.properties).toHaveProperty('connectorName');
26
+ expect(doAuth.definition.inputSchema.properties).toHaveProperty('hostname');
27
+ expect(doAuth.definition.inputSchema.properties).toHaveProperty('apiKey');
28
+ expect(doAuth.definition.inputSchema.properties).toHaveProperty('additionalInfo');
29
+ expect(doAuth.definition.inputSchema.properties).toHaveProperty('callbackUri');
30
+ });
31
+ });
32
+
33
+ describe('execute - apiKey authentication', () => {
34
+ test('should authenticate with API key successfully', async () => {
35
+ // Arrange
36
+ const mockManifest = {
37
+ platforms: {
38
+ testCRM: {
39
+ name: 'testCRM',
40
+ auth: { type: 'apiKey' },
41
+ environment: { type: 'fixed' }
42
+ }
43
+ }
44
+ };
45
+
46
+ const mockUserInfo = {
47
+ id: 'test-user-123',
48
+ name: 'Test User'
49
+ };
50
+
51
+ authCore.onApiKeyLogin.mockResolvedValue({
52
+ userInfo: mockUserInfo
53
+ });
54
+
55
+ jwt.generateJwt.mockReturnValue('mock-jwt-token');
56
+
57
+ // Act
58
+ const result = await doAuth.execute({
59
+ connectorManifest: mockManifest,
60
+ connectorName: 'testCRM',
61
+ hostname: 'test.crm.com',
62
+ apiKey: 'test-api-key'
63
+ });
64
+
65
+ // Assert
66
+ expect(result).toEqual({
67
+ success: true,
68
+ data: {
69
+ jwtToken: 'mock-jwt-token',
70
+ message: expect.stringContaining('IMPORTANT')
71
+ }
72
+ });
73
+ expect(authCore.onApiKeyLogin).toHaveBeenCalledWith({
74
+ platform: 'testCRM',
75
+ hostname: 'test.crm.com',
76
+ apiKey: 'test-api-key',
77
+ additionalInfo: undefined
78
+ });
79
+ expect(jwt.generateJwt).toHaveBeenCalledWith({
80
+ id: 'test-user-123',
81
+ platform: 'testCRM'
82
+ });
83
+ });
84
+
85
+ test('should handle apiKey authentication with additional info', async () => {
86
+ // Arrange
87
+ const mockManifest = {
88
+ platforms: {
89
+ testCRM: {
90
+ name: 'testCRM',
91
+ auth: { type: 'apiKey' }
92
+ }
93
+ }
94
+ };
95
+
96
+ const additionalInfo = {
97
+ username: 'testuser',
98
+ password: 'testpass',
99
+ apiUrl: 'https://api.test.com'
100
+ };
101
+
102
+ const mockUserInfo = {
103
+ id: 'test-user-456',
104
+ name: 'Test User'
105
+ };
106
+
107
+ authCore.onApiKeyLogin.mockResolvedValue({
108
+ userInfo: mockUserInfo
109
+ });
110
+
111
+ jwt.generateJwt.mockReturnValue('mock-jwt-token-2');
112
+
113
+ // Act
114
+ const result = await doAuth.execute({
115
+ connectorManifest: mockManifest,
116
+ connectorName: 'testCRM',
117
+ hostname: 'test.crm.com',
118
+ apiKey: 'test-api-key',
119
+ additionalInfo
120
+ });
121
+
122
+ // Assert
123
+ expect(result.success).toBe(true);
124
+ expect(authCore.onApiKeyLogin).toHaveBeenCalledWith({
125
+ platform: 'testCRM',
126
+ hostname: 'test.crm.com',
127
+ apiKey: 'test-api-key',
128
+ additionalInfo
129
+ });
130
+ });
131
+
132
+ test('should return error when user info not found', async () => {
133
+ // Arrange
134
+ const mockManifest = {
135
+ platforms: {
136
+ testCRM: {
137
+ name: 'testCRM',
138
+ auth: { type: 'apiKey' }
139
+ }
140
+ }
141
+ };
142
+
143
+ authCore.onApiKeyLogin.mockResolvedValue({
144
+ userInfo: null
145
+ });
146
+
147
+ // Act
148
+ const result = await doAuth.execute({
149
+ connectorManifest: mockManifest,
150
+ connectorName: 'testCRM',
151
+ hostname: 'test.crm.com',
152
+ apiKey: 'invalid-api-key'
153
+ });
154
+
155
+ // Assert
156
+ expect(result).toEqual({
157
+ success: false,
158
+ error: 'Authentication failed',
159
+ errorDetails: 'User info not found'
160
+ });
161
+ });
162
+ });
163
+
164
+ describe('execute - OAuth authentication', () => {
165
+ test('should return auth URI when callback not provided', async () => {
166
+ // Arrange
167
+ const mockManifest = {
168
+ platforms: {
169
+ salesforce: {
170
+ name: 'salesforce',
171
+ auth: {
172
+ type: 'oauth',
173
+ oauth: {
174
+ authUrl: 'https://login.salesforce.com/services/oauth2/authorize',
175
+ clientId: 'test-client-id',
176
+ scope: 'api refresh_token',
177
+ customState: ''
178
+ }
179
+ }
180
+ }
181
+ }
182
+ };
183
+
184
+ // Act
185
+ const result = await doAuth.execute({
186
+ connectorManifest: mockManifest,
187
+ connectorName: 'salesforce'
188
+ });
189
+
190
+ // Assert
191
+ expect(result.success).toBe(true);
192
+ expect(result.data.authUri).toContain('https://login.salesforce.com');
193
+ expect(result.data.authUri).toContain('client_id=test-client-id');
194
+ expect(result.data.authUri).toContain('response_type=code');
195
+ expect(result.data.message).toContain('IMPORTANT');
196
+ });
197
+
198
+ test('should handle OAuth callback successfully', async () => {
199
+ // Arrange
200
+ const mockManifest = {
201
+ platforms: {
202
+ salesforce: {
203
+ name: 'salesforce',
204
+ auth: {
205
+ type: 'oauth',
206
+ oauth: {}
207
+ }
208
+ }
209
+ }
210
+ };
211
+
212
+ const mockUserInfo = {
213
+ id: 'sf-user-123',
214
+ name: 'SF User'
215
+ };
216
+
217
+ authCore.onOAuthCallback.mockResolvedValue({
218
+ userInfo: mockUserInfo
219
+ });
220
+
221
+ jwt.generateJwt.mockReturnValue('mock-jwt-token-oauth');
222
+
223
+ // Act
224
+ const result = await doAuth.execute({
225
+ connectorManifest: mockManifest,
226
+ connectorName: 'salesforce',
227
+ hostname: 'login.salesforce.com',
228
+ callbackUri: 'https://redirect.com?code=test-code&state=test-state'
229
+ });
230
+
231
+ // Assert
232
+ expect(result).toEqual({
233
+ success: true,
234
+ data: {
235
+ jwtToken: 'mock-jwt-token-oauth',
236
+ message: expect.stringContaining('IMPORTANT')
237
+ }
238
+ });
239
+ expect(authCore.onOAuthCallback).toHaveBeenCalledWith({
240
+ platform: 'salesforce',
241
+ hostname: 'login.salesforce.com',
242
+ callbackUri: 'https://redirect.com?code=test-code&state=test-state',
243
+ query: expect.objectContaining({
244
+ hostname: 'login.salesforce.com'
245
+ })
246
+ });
247
+ });
248
+
249
+ test('should return error when OAuth callback fails', async () => {
250
+ // Arrange
251
+ const mockManifest = {
252
+ platforms: {
253
+ salesforce: {
254
+ name: 'salesforce',
255
+ auth: {
256
+ type: 'oauth',
257
+ oauth: {}
258
+ }
259
+ }
260
+ }
261
+ };
262
+
263
+ authCore.onOAuthCallback.mockResolvedValue({
264
+ userInfo: null
265
+ });
266
+
267
+ // Act
268
+ const result = await doAuth.execute({
269
+ connectorManifest: mockManifest,
270
+ connectorName: 'salesforce',
271
+ hostname: 'login.salesforce.com',
272
+ callbackUri: 'https://redirect.com?error=access_denied'
273
+ });
274
+
275
+ // Assert
276
+ expect(result).toEqual({
277
+ success: false,
278
+ error: 'Authentication failed',
279
+ errorDetails: 'User info not found'
280
+ });
281
+ });
282
+
283
+ test('should include custom state in auth URI', async () => {
284
+ // Arrange
285
+ const mockManifest = {
286
+ platforms: {
287
+ customCRM: {
288
+ name: 'customCRM',
289
+ auth: {
290
+ type: 'oauth',
291
+ oauth: {
292
+ authUrl: 'https://custom.com/oauth',
293
+ clientId: 'custom-client-id',
294
+ scope: '',
295
+ customState: 'custom=state&other=value'
296
+ }
297
+ }
298
+ }
299
+ }
300
+ };
301
+
302
+ // Act
303
+ const result = await doAuth.execute({
304
+ connectorManifest: mockManifest,
305
+ connectorName: 'customCRM'
306
+ });
307
+
308
+ // Assert
309
+ expect(result.success).toBe(true);
310
+ expect(result.data.authUri).toContain('state=custom=state&other=value');
311
+ });
312
+ });
313
+
314
+ describe('error handling', () => {
315
+ test('should handle authentication errors gracefully', async () => {
316
+ // Arrange
317
+ const mockManifest = {
318
+ platforms: {
319
+ testCRM: {
320
+ name: 'testCRM',
321
+ auth: { type: 'apiKey' }
322
+ }
323
+ }
324
+ };
325
+
326
+ authCore.onApiKeyLogin.mockRejectedValue(
327
+ new Error('Invalid credentials')
328
+ );
329
+
330
+ // Act
331
+ const result = await doAuth.execute({
332
+ connectorManifest: mockManifest,
333
+ connectorName: 'testCRM',
334
+ hostname: 'test.crm.com',
335
+ apiKey: 'bad-key'
336
+ });
337
+
338
+ // Assert
339
+ expect(result.success).toBe(false);
340
+ expect(result.error).toBe('Invalid credentials');
341
+ expect(result.errorDetails).toBeDefined();
342
+ });
343
+
344
+ test('should handle missing platform in manifest', async () => {
345
+ // Arrange
346
+ const mockManifest = {
347
+ platforms: {}
348
+ };
349
+
350
+ // Act
351
+ const result = await doAuth.execute({
352
+ connectorManifest: mockManifest,
353
+ connectorName: 'nonExistent',
354
+ apiKey: 'test-key'
355
+ });
356
+
357
+ // Assert
358
+ expect(result.success).toBe(false);
359
+ expect(result.error).toBeDefined();
360
+ });
361
+ });
362
+ });
363
+
@@ -0,0 +1,263 @@
1
+ const findContactByName = require('../../../mcp/tools/findContactByName');
2
+ const jwt = require('../../../lib/jwt');
3
+ const connectorRegistry = require('../../../connector/registry');
4
+ const contactCore = require('../../../handlers/contact');
5
+
6
+ // Mock dependencies
7
+ jest.mock('../../../lib/jwt');
8
+ jest.mock('../../../connector/registry');
9
+ jest.mock('../../../handlers/contact');
10
+
11
+ describe('MCP Tool: findContactByName', () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ describe('tool definition', () => {
17
+ test('should have correct tool definition', () => {
18
+ expect(findContactByName.definition).toBeDefined();
19
+ expect(findContactByName.definition.name).toBe('findContactByName');
20
+ expect(findContactByName.definition.description).toContain('REQUIRES AUTHENTICATION');
21
+ expect(findContactByName.definition.inputSchema).toBeDefined();
22
+ });
23
+
24
+ test('should require jwtToken and name parameters', () => {
25
+ expect(findContactByName.definition.inputSchema.required).toContain('jwtToken');
26
+ expect(findContactByName.definition.inputSchema.required).toContain('name');
27
+ });
28
+ });
29
+
30
+ describe('execute', () => {
31
+ test('should find contact by name successfully', async () => {
32
+ // Arrange
33
+ const mockContact = {
34
+ id: 'contact-123',
35
+ name: 'John Doe',
36
+ phone: '+1234567890',
37
+ type: 'Contact'
38
+ };
39
+
40
+ jwt.decodeJwt.mockReturnValue({
41
+ id: 'user-123',
42
+ platform: 'testCRM'
43
+ });
44
+
45
+ const mockConnector = {
46
+ findContactWithName: jest.fn()
47
+ };
48
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
49
+
50
+ contactCore.findContactWithName.mockResolvedValue({
51
+ successful: true,
52
+ contact: mockContact,
53
+ returnMessage: { message: 'Contact found' }
54
+ });
55
+
56
+ // Act
57
+ const result = await findContactByName.execute({
58
+ jwtToken: 'mock-jwt-token',
59
+ name: 'John Doe'
60
+ });
61
+
62
+ // Assert
63
+ expect(result).toEqual({
64
+ success: true,
65
+ data: mockContact
66
+ });
67
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('mock-jwt-token');
68
+ expect(connectorRegistry.getConnector).toHaveBeenCalledWith('testCRM');
69
+ expect(contactCore.findContactWithName).toHaveBeenCalledWith({
70
+ platform: 'testCRM',
71
+ userId: 'user-123',
72
+ name: 'John Doe'
73
+ });
74
+ });
75
+
76
+ test('should find contact with partial name', async () => {
77
+ // Arrange
78
+ const mockContact = {
79
+ id: 'contact-456',
80
+ name: 'Jane Smith',
81
+ phone: '+9876543210',
82
+ type: 'Contact'
83
+ };
84
+
85
+ jwt.decodeJwt.mockReturnValue({
86
+ id: 'user-123',
87
+ platform: 'testCRM'
88
+ });
89
+
90
+ const mockConnector = {
91
+ findContactWithName: jest.fn()
92
+ };
93
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
94
+
95
+ contactCore.findContactWithName.mockResolvedValue({
96
+ successful: true,
97
+ contact: mockContact
98
+ });
99
+
100
+ // Act
101
+ const result = await findContactByName.execute({
102
+ jwtToken: 'mock-jwt-token',
103
+ name: 'Jane'
104
+ });
105
+
106
+ // Assert
107
+ expect(result.success).toBe(true);
108
+ expect(result.data).toEqual(mockContact);
109
+ expect(contactCore.findContactWithName).toHaveBeenCalledWith({
110
+ platform: 'testCRM',
111
+ userId: 'user-123',
112
+ name: 'Jane'
113
+ });
114
+ });
115
+
116
+ test('should return error when contact not found', async () => {
117
+ // Arrange
118
+ jwt.decodeJwt.mockReturnValue({
119
+ id: 'user-123',
120
+ platform: 'testCRM'
121
+ });
122
+
123
+ const mockConnector = {
124
+ findContactWithName: jest.fn()
125
+ };
126
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
127
+
128
+ contactCore.findContactWithName.mockResolvedValue({
129
+ successful: false,
130
+ contact: null,
131
+ returnMessage: { message: 'Contact not found' }
132
+ });
133
+
134
+ // Act
135
+ const result = await findContactByName.execute({
136
+ jwtToken: 'mock-jwt-token',
137
+ name: 'NonExistent Person'
138
+ });
139
+
140
+ // Assert
141
+ expect(result).toEqual({
142
+ success: false,
143
+ error: 'Contact not found'
144
+ });
145
+ });
146
+
147
+ test('should return error when JWT is invalid', async () => {
148
+ // Arrange
149
+ jwt.decodeJwt.mockReturnValue({
150
+ platform: 'testCRM'
151
+ // id is missing
152
+ });
153
+
154
+ // Act
155
+ const result = await findContactByName.execute({
156
+ jwtToken: 'invalid-token',
157
+ name: 'John Doe'
158
+ });
159
+
160
+ // Assert
161
+ expect(result.success).toBe(false);
162
+ expect(result.error).toContain('Invalid JWT token');
163
+ });
164
+
165
+ test('should return error when platform connector not found', async () => {
166
+ // Arrange
167
+ jwt.decodeJwt.mockReturnValue({
168
+ id: 'user-123',
169
+ platform: 'unknownCRM'
170
+ });
171
+
172
+ connectorRegistry.getConnector.mockReturnValue(null);
173
+
174
+ // Act
175
+ const result = await findContactByName.execute({
176
+ jwtToken: 'mock-jwt-token',
177
+ name: 'John Doe'
178
+ });
179
+
180
+ // Assert
181
+ expect(result.success).toBe(false);
182
+ expect(result.error).toContain('Platform connector not found');
183
+ });
184
+
185
+ test('should return error when findContactWithName is not implemented', async () => {
186
+ // Arrange
187
+ jwt.decodeJwt.mockReturnValue({
188
+ id: 'user-123',
189
+ platform: 'testCRM'
190
+ });
191
+
192
+ const mockConnector = {}; // No findContactWithName method
193
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
194
+
195
+ // Act
196
+ const result = await findContactByName.execute({
197
+ jwtToken: 'mock-jwt-token',
198
+ name: 'John Doe'
199
+ });
200
+
201
+ // Assert
202
+ expect(result.success).toBe(false);
203
+ expect(result.error).toContain('not implemented');
204
+ });
205
+
206
+ test('should handle unexpected errors gracefully', async () => {
207
+ // Arrange
208
+ jwt.decodeJwt.mockReturnValue({
209
+ id: 'user-123',
210
+ platform: 'testCRM'
211
+ });
212
+
213
+ const mockConnector = {
214
+ findContactWithName: jest.fn()
215
+ };
216
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
217
+
218
+ contactCore.findContactWithName.mockRejectedValue(
219
+ new Error('API rate limit exceeded')
220
+ );
221
+
222
+ // Act
223
+ const result = await findContactByName.execute({
224
+ jwtToken: 'mock-jwt-token',
225
+ name: 'John Doe'
226
+ });
227
+
228
+ // Assert
229
+ expect(result.success).toBe(false);
230
+ expect(result.error).toBe('API rate limit exceeded');
231
+ expect(result.errorDetails).toBeDefined();
232
+ });
233
+
234
+ test('should handle empty name parameter', async () => {
235
+ // Arrange
236
+ jwt.decodeJwt.mockReturnValue({
237
+ id: 'user-123',
238
+ platform: 'testCRM'
239
+ });
240
+
241
+ const mockConnector = {
242
+ findContactWithName: jest.fn()
243
+ };
244
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
245
+
246
+ contactCore.findContactWithName.mockResolvedValue({
247
+ successful: false,
248
+ returnMessage: { message: 'Name parameter is required' }
249
+ });
250
+
251
+ // Act
252
+ const result = await findContactByName.execute({
253
+ jwtToken: 'mock-jwt-token',
254
+ name: ''
255
+ });
256
+
257
+ // Assert
258
+ expect(result.success).toBe(false);
259
+ expect(result.error).toBe('Name parameter is required');
260
+ });
261
+ });
262
+ });
263
+