@app-connect/core 1.7.10 → 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 (55) 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 +89 -67
  6. package/handlers/calldown.js +10 -4
  7. package/handlers/contact.js +4 -104
  8. package/handlers/disposition.js +4 -142
  9. package/handlers/log.js +172 -257
  10. package/handlers/user.js +19 -6
  11. package/index.js +213 -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 +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/cacheModel.js +3 -0
  40. package/package.json +71 -70
  41. package/releaseNotes.json +12 -0
  42. package/test/handlers/log.test.js +6 -2
  43. package/test/lib/logger.test.js +206 -0
  44. package/test/lib/sharedSMSComposer.test.js +1084 -0
  45. package/test/mcp/tools/collectAuthInfo.test.js +192 -0
  46. package/test/mcp/tools/createCallLog.test.js +412 -0
  47. package/test/mcp/tools/createMessageLog.test.js +580 -0
  48. package/test/mcp/tools/doAuth.test.js +363 -0
  49. package/test/mcp/tools/findContactByName.test.js +263 -0
  50. package/test/mcp/tools/findContactByPhone.test.js +284 -0
  51. package/test/mcp/tools/getCallLog.test.js +286 -0
  52. package/test/mcp/tools/getPublicConnectors.test.js +128 -0
  53. package/test/mcp/tools/logout.test.js +169 -0
  54. package/test/mcp/tools/setConnector.test.js +177 -0
  55. package/test/mcp/tools/updateCallLog.test.js +346 -0
@@ -0,0 +1,192 @@
1
+ const collectAuthInfo = require('../../../mcp/tools/collectAuthInfo');
2
+
3
+ describe('MCP Tool: collectAuthInfo', () => {
4
+ describe('tool definition', () => {
5
+ test('should have correct tool definition', () => {
6
+ expect(collectAuthInfo.definition).toBeDefined();
7
+ expect(collectAuthInfo.definition.name).toBe('collectAuthInfo');
8
+ expect(collectAuthInfo.definition.description).toContain('Auth flow step.3');
9
+ expect(collectAuthInfo.definition.inputSchema).toBeDefined();
10
+ });
11
+
12
+ test('should require connectorManifest and connectorName parameters', () => {
13
+ expect(collectAuthInfo.definition.inputSchema.required).toContain('connectorManifest');
14
+ expect(collectAuthInfo.definition.inputSchema.required).toContain('connectorName');
15
+ });
16
+ });
17
+
18
+ describe('execute', () => {
19
+ test('should handle selectable environment type', async () => {
20
+ // Arrange
21
+ const mockManifest = {
22
+ platforms: {
23
+ salesforce: {
24
+ name: 'salesforce',
25
+ environment: {
26
+ type: 'selectable',
27
+ selections: [
28
+ { name: 'Production', const: 'https://login.salesforce.com' },
29
+ { name: 'Sandbox', const: 'https://test.salesforce.com' }
30
+ ]
31
+ }
32
+ }
33
+ }
34
+ };
35
+
36
+ // Act
37
+ const result = await collectAuthInfo.execute({
38
+ connectorManifest: mockManifest,
39
+ connectorName: 'salesforce',
40
+ selection: 'Production'
41
+ });
42
+
43
+ // Assert
44
+ expect(result).toEqual({
45
+ success: true,
46
+ data: {
47
+ hostname: 'login.salesforce.com',
48
+ message: expect.stringContaining('IMPORTANT')
49
+ }
50
+ });
51
+ });
52
+
53
+ test('should handle dynamic environment type', async () => {
54
+ // Arrange
55
+ const mockManifest = {
56
+ platforms: {
57
+ netsuite: {
58
+ name: 'netsuite',
59
+ environment: {
60
+ type: 'dynamic'
61
+ }
62
+ }
63
+ }
64
+ };
65
+
66
+ // Act
67
+ const result = await collectAuthInfo.execute({
68
+ connectorManifest: mockManifest,
69
+ connectorName: 'netsuite',
70
+ hostname: 'https://1234567.app.netsuite.com'
71
+ });
72
+
73
+ // Assert
74
+ expect(result).toEqual({
75
+ success: true,
76
+ data: {
77
+ hostname: '1234567.app.netsuite.com',
78
+ message: expect.stringContaining('IMPORTANT')
79
+ }
80
+ });
81
+ });
82
+
83
+ test('should handle sandbox selection', async () => {
84
+ // Arrange
85
+ const mockManifest = {
86
+ platforms: {
87
+ salesforce: {
88
+ name: 'salesforce',
89
+ environment: {
90
+ type: 'selectable',
91
+ selections: [
92
+ { name: 'Production', const: 'https://login.salesforce.com' },
93
+ { name: 'Sandbox', const: 'https://test.salesforce.com' }
94
+ ]
95
+ }
96
+ }
97
+ }
98
+ };
99
+
100
+ // Act
101
+ const result = await collectAuthInfo.execute({
102
+ connectorManifest: mockManifest,
103
+ connectorName: 'salesforce',
104
+ selection: 'Sandbox'
105
+ });
106
+
107
+ // Assert
108
+ expect(result.success).toBe(true);
109
+ expect(result.data.hostname).toBe('test.salesforce.com');
110
+ });
111
+
112
+ test('should handle invalid hostname URL', async () => {
113
+ // Arrange
114
+ const mockManifest = {
115
+ platforms: {
116
+ netsuite: {
117
+ name: 'netsuite',
118
+ environment: {
119
+ type: 'dynamic'
120
+ }
121
+ }
122
+ }
123
+ };
124
+
125
+ // Act
126
+ const result = await collectAuthInfo.execute({
127
+ connectorManifest: mockManifest,
128
+ connectorName: 'netsuite',
129
+ hostname: 'invalid-url'
130
+ });
131
+
132
+ // Assert
133
+ expect(result.success).toBe(false);
134
+ expect(result.error).toBeDefined();
135
+ expect(result.errorDetails).toBeDefined();
136
+ });
137
+
138
+ test('should handle missing selection for selectable type', async () => {
139
+ // Arrange
140
+ const mockManifest = {
141
+ platforms: {
142
+ salesforce: {
143
+ name: 'salesforce',
144
+ environment: {
145
+ type: 'selectable',
146
+ selections: [
147
+ { name: 'Production', const: 'https://login.salesforce.com' }
148
+ ]
149
+ }
150
+ }
151
+ }
152
+ };
153
+
154
+ // Act
155
+ const result = await collectAuthInfo.execute({
156
+ connectorManifest: mockManifest,
157
+ connectorName: 'salesforce',
158
+ selection: 'NonExistent'
159
+ });
160
+
161
+ // Assert
162
+ expect(result.success).toBe(false);
163
+ expect(result.error).toBeDefined();
164
+ });
165
+
166
+ test('should handle missing hostname for dynamic type', async () => {
167
+ // Arrange
168
+ const mockManifest = {
169
+ platforms: {
170
+ netsuite: {
171
+ name: 'netsuite',
172
+ environment: {
173
+ type: 'dynamic'
174
+ }
175
+ }
176
+ }
177
+ };
178
+
179
+ // Act
180
+ const result = await collectAuthInfo.execute({
181
+ connectorManifest: mockManifest,
182
+ connectorName: 'netsuite'
183
+ // hostname is missing
184
+ });
185
+
186
+ // Assert
187
+ expect(result.success).toBe(false);
188
+ expect(result.error).toBeDefined();
189
+ });
190
+ });
191
+ });
192
+
@@ -0,0 +1,412 @@
1
+ const createCallLog = require('../../../mcp/tools/createCallLog');
2
+ const jwt = require('../../../lib/jwt');
3
+ const connectorRegistry = require('../../../connector/registry');
4
+ const logCore = require('../../../handlers/log');
5
+ const util = require('../../../lib/util');
6
+ const { CallLogModel } = require('../../../models/callLogModel');
7
+
8
+ // Mock dependencies
9
+ jest.mock('../../../lib/jwt');
10
+ jest.mock('../../../connector/registry');
11
+ jest.mock('../../../handlers/log');
12
+ jest.mock('../../../lib/util');
13
+ jest.mock('../../../models/callLogModel');
14
+
15
+ describe('MCP Tool: createCallLog', () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ process.env.HASH_KEY = 'test-hash-key';
19
+ });
20
+
21
+ describe('tool definition', () => {
22
+ test('should have correct tool definition', () => {
23
+ expect(createCallLog.definition).toBeDefined();
24
+ expect(createCallLog.definition.name).toBe('createCallLog');
25
+ expect(createCallLog.definition.description).toContain('REQUIRES AUTHENTICATION');
26
+ expect(createCallLog.definition.inputSchema).toBeDefined();
27
+ });
28
+
29
+ test('should require jwtToken and incomingData parameters', () => {
30
+ expect(createCallLog.definition.inputSchema.required).toContain('jwtToken');
31
+ expect(createCallLog.definition.inputSchema.required).toContain('incomingData');
32
+ });
33
+
34
+ test('should have detailed inputSchema for incomingData', () => {
35
+ const incomingDataSchema = createCallLog.definition.inputSchema.properties.incomingData;
36
+ expect(incomingDataSchema.properties).toHaveProperty('logInfo');
37
+ expect(incomingDataSchema.properties).toHaveProperty('contactId');
38
+ expect(incomingDataSchema.properties).toHaveProperty('note');
39
+ expect(incomingDataSchema.required).toContain('logInfo');
40
+ expect(incomingDataSchema.required).toContain('contactId');
41
+ });
42
+ });
43
+
44
+ describe('execute', () => {
45
+ test('should create call log successfully', async () => {
46
+ // Arrange
47
+ const mockIncomingData = {
48
+ logInfo: {
49
+ id: 'rc-call-123',
50
+ sessionId: 'session-123',
51
+ direction: 'Inbound',
52
+ startTime: '2024-01-01T10:00:00Z',
53
+ duration: 120,
54
+ from: { phoneNumber: '+1234567890', name: 'John Doe' },
55
+ to: { phoneNumber: '+0987654321', name: 'Company' },
56
+ accountId: 'rc-account-123'
57
+ },
58
+ contactId: 'contact-123',
59
+ contactName: 'John Doe',
60
+ contactType: 'Contact',
61
+ note: 'Test call note'
62
+ };
63
+
64
+ jwt.decodeJwt.mockReturnValue({
65
+ id: 'user-123',
66
+ platform: 'testCRM'
67
+ });
68
+
69
+ const mockConnector = {
70
+ createCallLog: jest.fn()
71
+ };
72
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
73
+
74
+ CallLogModel.findOne.mockResolvedValue(null); // No existing log
75
+
76
+ util.getHashValue.mockReturnValue('hashed-account-id');
77
+
78
+ logCore.createCallLog.mockResolvedValue({
79
+ successful: true,
80
+ logId: 'crm-log-123',
81
+ returnMessage: { message: 'Call logged successfully' }
82
+ });
83
+
84
+ // Act
85
+ const result = await createCallLog.execute({
86
+ jwtToken: 'mock-jwt-token',
87
+ incomingData: mockIncomingData
88
+ });
89
+
90
+ // Assert
91
+ expect(result).toEqual({
92
+ success: true,
93
+ data: {
94
+ logId: 'crm-log-123',
95
+ message: 'Call logged successfully'
96
+ }
97
+ });
98
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('mock-jwt-token');
99
+ expect(CallLogModel.findOne).toHaveBeenCalledWith({
100
+ where: { sessionId: 'session-123' }
101
+ });
102
+ expect(logCore.createCallLog).toHaveBeenCalledWith({
103
+ platform: 'testCRM',
104
+ userId: 'user-123',
105
+ incomingData: mockIncomingData,
106
+ hashedAccountId: 'hashed-account-id',
107
+ isFromSSCL: false
108
+ });
109
+ });
110
+
111
+ test('should create call log with AI note and transcript', async () => {
112
+ // Arrange
113
+ const mockIncomingData = {
114
+ logInfo: {
115
+ id: 'rc-call-456',
116
+ sessionId: 'session-456',
117
+ direction: 'Outbound',
118
+ startTime: '2024-01-01T11:00:00Z',
119
+ duration: 300,
120
+ from: { phoneNumber: '+0987654321' },
121
+ to: { phoneNumber: '+1234567890' }
122
+ },
123
+ contactId: 'contact-456',
124
+ aiNote: 'AI generated summary of the call',
125
+ transcript: 'Full call transcript text'
126
+ };
127
+
128
+ jwt.decodeJwt.mockReturnValue({
129
+ id: 'user-123',
130
+ platform: 'testCRM'
131
+ });
132
+
133
+ const mockConnector = {
134
+ createCallLog: jest.fn()
135
+ };
136
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
137
+
138
+ CallLogModel.findOne.mockResolvedValue(null);
139
+
140
+ logCore.createCallLog.mockResolvedValue({
141
+ successful: true,
142
+ logId: 'crm-log-456'
143
+ });
144
+
145
+ // Act
146
+ const result = await createCallLog.execute({
147
+ jwtToken: 'mock-jwt-token',
148
+ incomingData: mockIncomingData
149
+ });
150
+
151
+ // Assert
152
+ expect(result.success).toBe(true);
153
+ expect(result.data.logId).toBe('crm-log-456');
154
+ });
155
+
156
+ test('should create call log with additional submission', async () => {
157
+ // Arrange
158
+ const mockIncomingData = {
159
+ logInfo: {
160
+ id: 'rc-call-789',
161
+ sessionId: 'session-789',
162
+ direction: 'Inbound',
163
+ startTime: '2024-01-01T12:00:00Z',
164
+ duration: 60,
165
+ from: { phoneNumber: '+1234567890' },
166
+ to: { phoneNumber: '+0987654321' }
167
+ },
168
+ contactId: 'contact-789',
169
+ additionalSubmission: {
170
+ isAssignedToUser: true,
171
+ adminAssignedUserToken: 'admin-jwt-token',
172
+ adminAssignedUserRcId: 'rc-ext-101'
173
+ }
174
+ };
175
+
176
+ jwt.decodeJwt.mockReturnValue({
177
+ id: 'user-123',
178
+ platform: 'testCRM'
179
+ });
180
+
181
+ const mockConnector = {
182
+ createCallLog: jest.fn()
183
+ };
184
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
185
+
186
+ CallLogModel.findOne.mockResolvedValue(null);
187
+
188
+ logCore.createCallLog.mockResolvedValue({
189
+ successful: true,
190
+ logId: 'crm-log-789'
191
+ });
192
+
193
+ // Act
194
+ const result = await createCallLog.execute({
195
+ jwtToken: 'mock-jwt-token',
196
+ incomingData: mockIncomingData
197
+ });
198
+
199
+ // Assert
200
+ expect(result.success).toBe(true);
201
+ });
202
+
203
+ test('should return error when call log already exists', async () => {
204
+ // Arrange
205
+ const mockIncomingData = {
206
+ logInfo: {
207
+ sessionId: 'existing-session'
208
+ }
209
+ };
210
+
211
+ CallLogModel.findOne.mockResolvedValue({
212
+ id: 'existing-log',
213
+ sessionId: 'existing-session'
214
+ });
215
+
216
+ // Act
217
+ const result = await createCallLog.execute({
218
+ jwtToken: 'mock-jwt-token',
219
+ incomingData: mockIncomingData
220
+ });
221
+
222
+ // Assert
223
+ expect(result.success).toBe(false);
224
+ expect(result.error).toContain('already exists');
225
+ });
226
+
227
+ test('should return error when jwtToken is missing', async () => {
228
+ // Act
229
+ const result = await createCallLog.execute({
230
+ incomingData: { logInfo: {} }
231
+ });
232
+
233
+ // Assert
234
+ expect(result.success).toBe(false);
235
+ expect(result.error).toContain('authorize CRM platform');
236
+ });
237
+
238
+ test('should return error when incomingData is missing', async () => {
239
+ // Act
240
+ const result = await createCallLog.execute({
241
+ jwtToken: 'mock-jwt-token'
242
+ });
243
+
244
+ // Assert
245
+ expect(result.success).toBe(false);
246
+ expect(result.error).toContain('Incoming data must be provided');
247
+ });
248
+
249
+ test('should return error when logInfo is missing', async () => {
250
+ // Act
251
+ const result = await createCallLog.execute({
252
+ jwtToken: 'mock-jwt-token',
253
+ incomingData: { contactId: 'contact-123' }
254
+ });
255
+
256
+ // Assert
257
+ expect(result.success).toBe(false);
258
+ expect(result.error).toContain('logInfo is required');
259
+ });
260
+
261
+ test('should return error when JWT is invalid', async () => {
262
+ // Arrange
263
+ const mockIncomingData = {
264
+ logInfo: {
265
+ sessionId: 'session-123'
266
+ }
267
+ };
268
+
269
+ CallLogModel.findOne.mockResolvedValue(null);
270
+
271
+ jwt.decodeJwt.mockReturnValue({
272
+ platform: 'testCRM'
273
+ // id is missing
274
+ });
275
+
276
+ // Act
277
+ const result = await createCallLog.execute({
278
+ jwtToken: 'invalid-token',
279
+ incomingData: mockIncomingData
280
+ });
281
+
282
+ // Assert
283
+ expect(result.success).toBe(false);
284
+ expect(result.error).toContain('Invalid JWT token');
285
+ });
286
+
287
+ test('should return error when platform connector not found', async () => {
288
+ // Arrange
289
+ const mockIncomingData = {
290
+ logInfo: {
291
+ sessionId: 'session-123'
292
+ }
293
+ };
294
+
295
+ CallLogModel.findOne.mockResolvedValue(null);
296
+
297
+ jwt.decodeJwt.mockReturnValue({
298
+ id: 'user-123',
299
+ platform: 'unknownCRM'
300
+ });
301
+
302
+ connectorRegistry.getConnector.mockReturnValue(null);
303
+
304
+ // Act
305
+ const result = await createCallLog.execute({
306
+ jwtToken: 'mock-jwt-token',
307
+ incomingData: mockIncomingData
308
+ });
309
+
310
+ // Assert
311
+ expect(result.success).toBe(false);
312
+ expect(result.error).toContain('Platform connector not found');
313
+ });
314
+
315
+ test('should return error when createCallLog is not implemented', async () => {
316
+ // Arrange
317
+ const mockIncomingData = {
318
+ logInfo: {
319
+ sessionId: 'session-123'
320
+ }
321
+ };
322
+
323
+ CallLogModel.findOne.mockResolvedValue(null);
324
+
325
+ jwt.decodeJwt.mockReturnValue({
326
+ id: 'user-123',
327
+ platform: 'testCRM'
328
+ });
329
+
330
+ const mockConnector = {}; // No createCallLog method
331
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
332
+
333
+ // Act
334
+ const result = await createCallLog.execute({
335
+ jwtToken: 'mock-jwt-token',
336
+ incomingData: mockIncomingData
337
+ });
338
+
339
+ // Assert
340
+ expect(result.success).toBe(false);
341
+ expect(result.error).toContain('not implemented');
342
+ });
343
+
344
+ test('should return error when creation fails', async () => {
345
+ // Arrange
346
+ const mockIncomingData = {
347
+ logInfo: {
348
+ id: 'rc-call-999',
349
+ sessionId: 'session-999',
350
+ direction: 'Inbound',
351
+ startTime: '2024-01-01T13:00:00Z',
352
+ duration: 45,
353
+ from: { phoneNumber: '+1234567890' },
354
+ to: { phoneNumber: '+0987654321' }
355
+ },
356
+ contactId: 'contact-999'
357
+ };
358
+
359
+ jwt.decodeJwt.mockReturnValue({
360
+ id: 'user-123',
361
+ platform: 'testCRM'
362
+ });
363
+
364
+ const mockConnector = {
365
+ createCallLog: jest.fn()
366
+ };
367
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
368
+
369
+ CallLogModel.findOne.mockResolvedValue(null);
370
+
371
+ logCore.createCallLog.mockResolvedValue({
372
+ successful: false,
373
+ returnMessage: { message: 'Failed to create log in CRM' }
374
+ });
375
+
376
+ // Act
377
+ const result = await createCallLog.execute({
378
+ jwtToken: 'mock-jwt-token',
379
+ incomingData: mockIncomingData
380
+ });
381
+
382
+ // Assert
383
+ expect(result.success).toBe(false);
384
+ expect(result.error).toBe('Failed to create log in CRM');
385
+ });
386
+
387
+ test('should handle unexpected errors gracefully', async () => {
388
+ // Arrange
389
+ const mockIncomingData = {
390
+ logInfo: {
391
+ sessionId: 'session-error'
392
+ }
393
+ };
394
+
395
+ CallLogModel.findOne.mockRejectedValue(
396
+ new Error('Database connection failed')
397
+ );
398
+
399
+ // Act
400
+ const result = await createCallLog.execute({
401
+ jwtToken: 'mock-jwt-token',
402
+ incomingData: mockIncomingData
403
+ });
404
+
405
+ // Assert
406
+ expect(result.success).toBe(false);
407
+ expect(result.error).toBe('Database connection failed');
408
+ expect(result.errorDetails).toBeDefined();
409
+ });
410
+ });
411
+ });
412
+