@app-connect/core 1.7.21 → 1.7.22

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 (42) hide show
  1. package/README.md +8 -1
  2. package/connector/developerPortal.js +4 -4
  3. package/docs/README.md +50 -0
  4. package/docs/architecture.md +93 -0
  5. package/docs/connectors.md +117 -0
  6. package/docs/handlers.md +125 -0
  7. package/docs/libraries.md +101 -0
  8. package/docs/models.md +144 -0
  9. package/docs/routes.md +115 -0
  10. package/docs/tests.md +73 -0
  11. package/handlers/admin.js +22 -2
  12. package/handlers/auth.js +51 -10
  13. package/handlers/log.js +4 -4
  14. package/handlers/managedAuth.js +446 -0
  15. package/index.js +264 -34
  16. package/lib/jwt.js +1 -1
  17. package/mcp/tools/createCallLog.js +5 -1
  18. package/mcp/tools/createContact.js +5 -1
  19. package/mcp/tools/createMessageLog.js +5 -1
  20. package/mcp/tools/findContactByName.js +5 -1
  21. package/mcp/tools/findContactByPhone.js +6 -2
  22. package/mcp/tools/getCallLog.js +5 -1
  23. package/mcp/tools/rcGetCallLogs.js +6 -2
  24. package/mcp/tools/updateCallLog.js +5 -1
  25. package/mcp/ui/App/lib/developerPortal.ts +1 -1
  26. package/package.json +72 -72
  27. package/releaseNotes.json +8 -0
  28. package/test/handlers/admin.test.js +34 -0
  29. package/test/handlers/auth.test.js +402 -6
  30. package/test/handlers/managedAuth.test.js +458 -0
  31. package/test/index.test.js +105 -0
  32. package/test/lib/jwt.test.js +15 -0
  33. package/test/mcp/tools/createCallLog.test.js +11 -0
  34. package/test/mcp/tools/createContact.test.js +58 -0
  35. package/test/mcp/tools/createMessageLog.test.js +15 -0
  36. package/test/mcp/tools/findContactByName.test.js +12 -0
  37. package/test/mcp/tools/findContactByPhone.test.js +12 -0
  38. package/test/mcp/tools/getCallLog.test.js +12 -0
  39. package/test/mcp/tools/rcGetCallLogs.test.js +56 -0
  40. package/test/mcp/tools/updateCallLog.test.js +14 -0
  41. package/test/routes/managedAuthRoutes.test.js +132 -0
  42. package/test/setup.js +2 -0
@@ -211,6 +211,18 @@ describe('MCP Tool: findContactByPhone', () => {
211
211
  expect(result.error).toContain('Invalid JWT token');
212
212
  });
213
213
 
214
+ test('should return error when decodeJwt returns null', async () => {
215
+ jwt.decodeJwt.mockReturnValue(null);
216
+
217
+ const result = await findContactByPhone.execute({
218
+ jwtToken: 'invalid-token',
219
+ phoneNumber: '+1234567890'
220
+ });
221
+
222
+ expect(result.success).toBe(false);
223
+ expect(result.error).toContain('Invalid JWT token');
224
+ });
225
+
214
226
  test('should return error when platform connector not found', async () => {
215
227
  // Arrange
216
228
  jwt.decodeJwt.mockReturnValue({
@@ -213,6 +213,18 @@ describe('MCP Tool: getCallLog', () => {
213
213
  expect(result.error).toContain('Invalid JWT token');
214
214
  });
215
215
 
216
+ test('should return error when decodeJwt returns null', async () => {
217
+ jwt.decodeJwt.mockReturnValue(null);
218
+
219
+ const result = await getCallLog.execute({
220
+ jwtToken: 'invalid-token',
221
+ sessionIds: ['session-123']
222
+ });
223
+
224
+ expect(result.success).toBe(false);
225
+ expect(result.error).toContain('Invalid JWT token');
226
+ });
227
+
216
228
  test('should return error when platform connector not found', async () => {
217
229
  // Arrange
218
230
  jwt.decodeJwt.mockReturnValue({
@@ -0,0 +1,56 @@
1
+ const rcGetCallLogs = require('../../../mcp/tools/rcGetCallLogs');
2
+ const jwt = require('../../../lib/jwt');
3
+ const { RingCentral } = require('../../../lib/ringcentral');
4
+
5
+ jest.mock('../../../lib/jwt');
6
+ jest.mock('../../../lib/ringcentral', () => ({
7
+ RingCentral: jest.fn()
8
+ }));
9
+
10
+ describe('MCP Tool: rcGetCallLogs', () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
14
+ process.env.RINGCENTRAL_CLIENT_ID = 'client-id';
15
+ process.env.RINGCENTRAL_CLIENT_SECRET = 'client-secret';
16
+ process.env.APP_SERVER = 'https://app.example.com';
17
+ });
18
+
19
+ test('should have correct tool definition', () => {
20
+ expect(rcGetCallLogs.definition).toBeDefined();
21
+ expect(rcGetCallLogs.definition.name).toBe('rcGetCallLogs');
22
+ });
23
+
24
+ test('should return call logs successfully', async () => {
25
+ jwt.decodeJwt.mockReturnValue({ id: 'user-123' });
26
+ const getCallLogData = jest.fn().mockResolvedValue({ records: [{ id: '1' }] });
27
+ RingCentral.mockImplementation(() => ({ getCallLogData }));
28
+
29
+ const result = await rcGetCallLogs.execute({
30
+ jwtToken: 'mock-jwt',
31
+ rcAccessToken: 'rc-token',
32
+ timeFrom: '2026-04-01T00:00:00.000Z',
33
+ timeTo: '2026-04-02T00:00:00.000Z'
34
+ });
35
+
36
+ expect(result).toEqual({ records: [{ id: '1' }] });
37
+ expect(getCallLogData).toHaveBeenCalledWith({
38
+ token: { access_token: 'rc-token', token_type: 'Bearer' },
39
+ timeFrom: '2026-04-01T00:00:00.000Z',
40
+ timeTo: '2026-04-02T00:00:00.000Z'
41
+ });
42
+ });
43
+
44
+ test('should return error when decodeJwt returns null', async () => {
45
+ jwt.decodeJwt.mockReturnValue(null);
46
+
47
+ const result = await rcGetCallLogs.execute({
48
+ jwtToken: 'bad-jwt',
49
+ rcAccessToken: 'rc-token'
50
+ });
51
+
52
+ expect(result.success).toBe(false);
53
+ expect(result.error).toContain('Invalid JWT token');
54
+ });
55
+ });
56
+
@@ -191,6 +191,20 @@ describe('MCP Tool: updateCallLog', () => {
191
191
  expect(result.error).toContain('Invalid JWT token');
192
192
  });
193
193
 
194
+ test('should return error when decodeJwt returns null', async () => {
195
+ jwt.decodeJwt.mockReturnValue(null);
196
+
197
+ const result = await updateCallLog.execute({
198
+ jwtToken: 'invalid-token',
199
+ incomingData: {
200
+ logData: { sessionId: 'session-123' }
201
+ }
202
+ });
203
+
204
+ expect(result.success).toBe(false);
205
+ expect(result.error).toContain('Invalid JWT token');
206
+ });
207
+
194
208
  test('should return error when platform connector not found', async () => {
195
209
  // Arrange
196
210
  const mockIncomingData = {
@@ -0,0 +1,132 @@
1
+ const express = require('express');
2
+ const request = require('supertest');
3
+
4
+ jest.mock('../../handlers/admin', () => ({
5
+ validateRcUserToken: jest.fn(),
6
+ validateAdminRole: jest.fn(),
7
+ }));
8
+ jest.mock('../../handlers/managedAuth', () => ({
9
+ getManagedAuthState: jest.fn(),
10
+ }));
11
+ jest.mock('../../handlers/auth', () => ({
12
+ onApiKeyLogin: jest.fn(),
13
+ }));
14
+ jest.mock('../../lib/jwt', () => ({
15
+ generateJwt: jest.fn().mockReturnValue('jwt-token'),
16
+ decodeJwt: jest.fn(),
17
+ }));
18
+
19
+ const adminCore = require('../../handlers/admin');
20
+ const managedAuthCore = require('../../handlers/managedAuth');
21
+ const authCore = require('../../handlers/auth');
22
+ const { createCoreRouter } = require('../../index');
23
+
24
+ describe('Managed Auth Routes', () => {
25
+ let app;
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ app = express();
30
+ app.use(express.json());
31
+ app.use('/', createCoreRouter());
32
+ });
33
+
34
+ describe('GET /apiKeyManagedAuthState', () => {
35
+ test('should require rcAccessToken', async () => {
36
+ const response = await request(app)
37
+ .get('/apiKeyManagedAuthState')
38
+ .query({ platform: 'testCRM' });
39
+
40
+ expect(response.status).toBe(400);
41
+ expect(response.text).toContain('Missing RingCentral access token');
42
+ expect(adminCore.validateRcUserToken).not.toHaveBeenCalled();
43
+ expect(managedAuthCore.getManagedAuthState).not.toHaveBeenCalled();
44
+ });
45
+
46
+ test('should validate rcAccessToken and use validated identity', async () => {
47
+ adminCore.validateRcUserToken.mockResolvedValue({
48
+ rcAccountId: 'validated-account-id',
49
+ rcExtensionId: 'validated-extension-id',
50
+ rcUserName: 'Validated User',
51
+ });
52
+ managedAuthCore.getManagedAuthState.mockResolvedValue({
53
+ hasManagedAuth: true,
54
+ allRequiredFieldsSatisfied: true,
55
+ visibleFieldConsts: [],
56
+ missingRequiredFieldConsts: [],
57
+ });
58
+
59
+ const response = await request(app)
60
+ .get('/apiKeyManagedAuthState')
61
+ .query({
62
+ platform: 'testCRM',
63
+ rcAccessToken: 'valid-rc-token',
64
+ rcAccountId: 'spoofed-account-id',
65
+ rcExtensionId: 'spoofed-extension-id',
66
+ });
67
+
68
+ expect(response.status).toBe(200);
69
+ expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
70
+ expect(managedAuthCore.getManagedAuthState).toHaveBeenCalledWith(expect.objectContaining({
71
+ platform: 'testCRM',
72
+ rcAccountId: 'validated-account-id',
73
+ rcExtensionId: 'validated-extension-id',
74
+ }));
75
+ });
76
+ });
77
+
78
+ describe('POST /apiKeyLogin', () => {
79
+ test('should require rcAccessToken', async () => {
80
+ const response = await request(app)
81
+ .post('/apiKeyLogin')
82
+ .send({
83
+ platform: 'testCRM',
84
+ apiKey: 'api-key',
85
+ hostname: 'test.example.com',
86
+ });
87
+
88
+ expect(response.status).toBe(400);
89
+ expect(response.text).toContain('Missing RingCentral access token');
90
+ expect(adminCore.validateRcUserToken).not.toHaveBeenCalled();
91
+ expect(authCore.onApiKeyLogin).not.toHaveBeenCalled();
92
+ });
93
+
94
+ test('should validate rcAccessToken and ignore spoofed rc ids in body', async () => {
95
+ adminCore.validateRcUserToken.mockResolvedValue({
96
+ rcAccountId: 'validated-account-id',
97
+ rcExtensionId: 'validated-extension-id',
98
+ rcUserName: 'Validated User',
99
+ });
100
+ authCore.onApiKeyLogin.mockResolvedValue({
101
+ userInfo: {
102
+ id: 'crm-user-id',
103
+ name: 'CRM User',
104
+ },
105
+ returnMessage: {
106
+ messageType: 'success',
107
+ message: 'ok',
108
+ },
109
+ });
110
+
111
+ const response = await request(app)
112
+ .post('/apiKeyLogin')
113
+ .send({
114
+ platform: 'testCRM',
115
+ apiKey: 'api-key',
116
+ hostname: 'test.example.com',
117
+ rcAccessToken: 'valid-rc-token',
118
+ rcAccountId: 'spoofed-account-id',
119
+ rcExtensionId: 'spoofed-extension-id',
120
+ });
121
+
122
+ expect(response.status).toBe(200);
123
+ expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
124
+ expect(authCore.onApiKeyLogin).toHaveBeenCalledWith(expect.objectContaining({
125
+ platform: 'testCRM',
126
+ rcAccountId: 'validated-account-id',
127
+ rcExtensionId: 'validated-extension-id',
128
+ rcUserName: 'Validated User',
129
+ }));
130
+ });
131
+ });
132
+ });
package/test/setup.js CHANGED
@@ -29,6 +29,7 @@ beforeAll(async () => {
29
29
  const { UserModel } = require('../models/userModel');
30
30
  const { CacheModel } = require('../models/cacheModel');
31
31
  const { AdminConfigModel } = require('../models/adminConfigModel');
32
+ const { AccountDataModel } = require('../models/accountDataModel');
32
33
 
33
34
  // Sync database models
34
35
  await CallLogModel.sync({ force: true });
@@ -36,6 +37,7 @@ beforeAll(async () => {
36
37
  await UserModel.sync({ force: true });
37
38
  await CacheModel.sync({ force: true });
38
39
  await AdminConfigModel.sync({ force: true });
40
+ await AccountDataModel.sync({ force: true });
39
41
 
40
42
  console.log('Database models synced for testing');
41
43
  } catch (error) {