@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.
- package/README.md +8 -1
- package/connector/developerPortal.js +4 -4
- package/docs/README.md +50 -0
- package/docs/architecture.md +93 -0
- package/docs/connectors.md +117 -0
- package/docs/handlers.md +125 -0
- package/docs/libraries.md +101 -0
- package/docs/models.md +144 -0
- package/docs/routes.md +115 -0
- package/docs/tests.md +73 -0
- package/handlers/admin.js +22 -2
- package/handlers/auth.js +51 -10
- package/handlers/log.js +4 -4
- package/handlers/managedAuth.js +446 -0
- package/index.js +264 -34
- package/lib/jwt.js +1 -1
- package/mcp/tools/createCallLog.js +5 -1
- package/mcp/tools/createContact.js +5 -1
- package/mcp/tools/createMessageLog.js +5 -1
- package/mcp/tools/findContactByName.js +5 -1
- package/mcp/tools/findContactByPhone.js +6 -2
- package/mcp/tools/getCallLog.js +5 -1
- package/mcp/tools/rcGetCallLogs.js +6 -2
- package/mcp/tools/updateCallLog.js +5 -1
- package/mcp/ui/App/lib/developerPortal.ts +1 -1
- package/package.json +72 -72
- package/releaseNotes.json +8 -0
- package/test/handlers/admin.test.js +34 -0
- package/test/handlers/auth.test.js +402 -6
- package/test/handlers/managedAuth.test.js +458 -0
- package/test/index.test.js +105 -0
- package/test/lib/jwt.test.js +15 -0
- package/test/mcp/tools/createCallLog.test.js +11 -0
- package/test/mcp/tools/createContact.test.js +58 -0
- package/test/mcp/tools/createMessageLog.test.js +15 -0
- package/test/mcp/tools/findContactByName.test.js +12 -0
- package/test/mcp/tools/findContactByPhone.test.js +12 -0
- package/test/mcp/tools/getCallLog.test.js +12 -0
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -0
- package/test/mcp/tools/updateCallLog.test.js +14 -0
- package/test/routes/managedAuthRoutes.test.js +132 -0
- 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) {
|