@app-connect/core 1.7.21 → 1.7.23

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 (50) 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 +57 -10
  13. package/handlers/log.js +217 -109
  14. package/handlers/managedAuth.js +446 -0
  15. package/handlers/plugin.js +183 -1
  16. package/handlers/user.js +1 -1
  17. package/index.js +410 -35
  18. package/lib/callLogComposer.js +36 -36
  19. package/lib/jwt.js +1 -1
  20. package/lib/util.js +0 -18
  21. package/mcp/tools/createCallLog.js +5 -1
  22. package/mcp/tools/createContact.js +5 -1
  23. package/mcp/tools/createMessageLog.js +5 -1
  24. package/mcp/tools/findContactByName.js +5 -1
  25. package/mcp/tools/findContactByPhone.js +6 -2
  26. package/mcp/tools/getCallLog.js +5 -1
  27. package/mcp/tools/rcGetCallLogs.js +6 -2
  28. package/mcp/tools/updateCallLog.js +5 -1
  29. package/mcp/ui/App/lib/developerPortal.ts +1 -1
  30. package/package.json +72 -72
  31. package/releaseNotes.json +16 -0
  32. package/test/handlers/admin.test.js +33 -0
  33. package/test/handlers/auth.test.js +402 -6
  34. package/test/handlers/log.test.js +60 -0
  35. package/test/handlers/managedAuth.test.js +458 -0
  36. package/test/handlers/plugin.test.js +93 -0
  37. package/test/index.test.js +105 -0
  38. package/test/lib/callLogComposer.test.js +21 -21
  39. package/test/lib/jwt.test.js +15 -0
  40. package/test/lib/util.test.js +1 -332
  41. package/test/mcp/tools/createCallLog.test.js +11 -0
  42. package/test/mcp/tools/createContact.test.js +58 -0
  43. package/test/mcp/tools/createMessageLog.test.js +15 -0
  44. package/test/mcp/tools/findContactByName.test.js +12 -0
  45. package/test/mcp/tools/findContactByPhone.test.js +12 -0
  46. package/test/mcp/tools/getCallLog.test.js +12 -0
  47. package/test/mcp/tools/rcGetCallLogs.test.js +56 -0
  48. package/test/mcp/tools/updateCallLog.test.js +14 -0
  49. package/test/routes/managedAuthRoutes.test.js +129 -0
  50. package/test/setup.js +2 -0
@@ -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,129 @@
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
+ });
51
+ managedAuthCore.getManagedAuthState.mockResolvedValue({
52
+ hasManagedAuth: true,
53
+ allRequiredFieldsSatisfied: true,
54
+ visibleFieldConsts: [],
55
+ missingRequiredFieldConsts: [],
56
+ });
57
+
58
+ const response = await request(app)
59
+ .get('/apiKeyManagedAuthState')
60
+ .query({
61
+ platform: 'testCRM',
62
+ rcAccessToken: 'valid-rc-token',
63
+ rcAccountId: 'spoofed-account-id',
64
+ rcExtensionId: 'spoofed-extension-id',
65
+ });
66
+
67
+ expect(response.status).toBe(200);
68
+ expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
69
+ expect(managedAuthCore.getManagedAuthState).toHaveBeenCalledWith(expect.objectContaining({
70
+ platform: 'testCRM',
71
+ rcAccountId: 'validated-account-id',
72
+ rcExtensionId: 'validated-extension-id',
73
+ }));
74
+ });
75
+ });
76
+
77
+ describe('POST /apiKeyLogin', () => {
78
+ test('should require rcAccessToken', async () => {
79
+ const response = await request(app)
80
+ .post('/apiKeyLogin')
81
+ .send({
82
+ platform: 'testCRM',
83
+ apiKey: 'api-key',
84
+ hostname: 'test.example.com',
85
+ });
86
+
87
+ expect(response.status).toBe(400);
88
+ expect(response.text).toContain('Missing RingCentral access token');
89
+ expect(adminCore.validateRcUserToken).not.toHaveBeenCalled();
90
+ expect(authCore.onApiKeyLogin).not.toHaveBeenCalled();
91
+ });
92
+
93
+ test('should validate rcAccessToken and ignore spoofed rc ids in body', async () => {
94
+ adminCore.validateRcUserToken.mockResolvedValue({
95
+ rcAccountId: 'validated-account-id',
96
+ rcExtensionId: 'validated-extension-id',
97
+ });
98
+ authCore.onApiKeyLogin.mockResolvedValue({
99
+ userInfo: {
100
+ id: 'crm-user-id',
101
+ name: 'CRM User',
102
+ },
103
+ returnMessage: {
104
+ messageType: 'success',
105
+ message: 'ok',
106
+ },
107
+ });
108
+
109
+ const response = await request(app)
110
+ .post('/apiKeyLogin')
111
+ .send({
112
+ platform: 'testCRM',
113
+ apiKey: 'api-key',
114
+ hostname: 'test.example.com',
115
+ rcAccessToken: 'valid-rc-token',
116
+ rcAccountId: 'spoofed-account-id',
117
+ rcExtensionId: 'spoofed-extension-id',
118
+ });
119
+
120
+ expect(response.status).toBe(200);
121
+ expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
122
+ expect(authCore.onApiKeyLogin).toHaveBeenCalledWith(expect.objectContaining({
123
+ platform: 'testCRM',
124
+ rcAccountId: 'validated-account-id',
125
+ rcExtensionId: 'validated-extension-id',
126
+ }));
127
+ });
128
+ });
129
+ });
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) {