@app-connect/core 1.7.20 → 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 (44) 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/contact.js +7 -2
  14. package/handlers/log.js +4 -4
  15. package/handlers/managedAuth.js +446 -0
  16. package/index.js +263 -38
  17. package/lib/jwt.js +1 -1
  18. package/mcp/tools/createCallLog.js +5 -1
  19. package/mcp/tools/createContact.js +5 -1
  20. package/mcp/tools/createMessageLog.js +5 -1
  21. package/mcp/tools/findContactByName.js +5 -1
  22. package/mcp/tools/findContactByPhone.js +6 -2
  23. package/mcp/tools/getCallLog.js +5 -1
  24. package/mcp/tools/rcGetCallLogs.js +6 -2
  25. package/mcp/tools/updateCallLog.js +5 -1
  26. package/mcp/ui/App/lib/developerPortal.ts +1 -1
  27. package/package.json +1 -1
  28. package/releaseNotes.json +20 -0
  29. package/test/handlers/admin.test.js +34 -0
  30. package/test/handlers/auth.test.js +402 -6
  31. package/test/handlers/contact.test.js +162 -0
  32. package/test/handlers/managedAuth.test.js +458 -0
  33. package/test/index.test.js +105 -0
  34. package/test/lib/jwt.test.js +15 -0
  35. package/test/mcp/tools/createCallLog.test.js +11 -0
  36. package/test/mcp/tools/createContact.test.js +58 -0
  37. package/test/mcp/tools/createMessageLog.test.js +15 -0
  38. package/test/mcp/tools/findContactByName.test.js +12 -0
  39. package/test/mcp/tools/findContactByPhone.test.js +12 -0
  40. package/test/mcp/tools/getCallLog.test.js +12 -0
  41. package/test/mcp/tools/rcGetCallLogs.test.js +56 -0
  42. package/test/mcp/tools/updateCallLog.test.js +14 -0
  43. package/test/routes/managedAuthRoutes.test.js +132 -0
  44. package/test/setup.js +2 -0
@@ -59,7 +59,11 @@ async function execute(args) {
59
59
  const { jwtToken, incomingData } = args;
60
60
 
61
61
  // Decode JWT to get userId and platform
62
- const { id: userId, platform } = jwt.decodeJwt(jwtToken);
62
+ const decodedToken = jwt.decodeJwt(jwtToken);
63
+ if (!decodedToken) {
64
+ throw new Error('Invalid JWT token');
65
+ }
66
+ const { id: userId, platform } = decodedToken;
63
67
 
64
68
  if (!userId) {
65
69
  throw new Error('Invalid JWT token: userId not found');
@@ -90,7 +90,7 @@ export async function fetchManifest(
90
90
  dbg.info('fetchManifest: connectorId=', connectorId, 'isPrivate=', isPrivate)
91
91
 
92
92
  const url = isPrivate && rcAccountId
93
- ? `${PORTAL_BASE}/connectors/${connectorId}/manifest?type=internal&accountId=${encodeURIComponent(rcAccountId)}`
93
+ ? `${PORTAL_BASE}/connectors/${connectorId}/manifest?access=internal&type=connector&accountId=${encodeURIComponent(rcAccountId)}`
94
94
  : `${PORTAL_BASE}/connectors/${connectorId}/manifest`
95
95
 
96
96
  dbg.info('fetchManifest: GET', url)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.20",
3
+ "version": "1.7.22",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,24 @@
1
1
  {
2
+ "1.7.22": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Managed auth fields for non-OAuth platforms"
7
+ }
8
+ ]
9
+ },
10
+ "1.7.21": {
11
+ "global": [
12
+ {
13
+ "type": "Fix",
14
+ "description": "Refresh contact for deleted contact issue"
15
+ },
16
+ {
17
+ "type": "Fix",
18
+ "description": "Click-to-sms button issue"
19
+ }
20
+ ]
21
+ },
2
22
  "1.7.20": {
3
23
  "global": [
4
24
  {
@@ -123,6 +123,40 @@ describe('Admin Handler', () => {
123
123
  });
124
124
  });
125
125
 
126
+ describe('validateRcUserToken', () => {
127
+ test('should return rc account and extension identity from valid token', async () => {
128
+ axios.get.mockResolvedValue({
129
+ data: {
130
+ account: { id: 'rc-account-789' },
131
+ id: 'extension-789',
132
+ contact: {
133
+ firstName: 'Alex',
134
+ lastName: 'Johnson'
135
+ }
136
+ }
137
+ });
138
+
139
+ const result = await adminHandler.validateRcUserToken({
140
+ rcAccessToken: 'valid-user-token'
141
+ });
142
+
143
+ expect(result).toEqual({
144
+ rcAccountId: 'rc-account-789',
145
+ rcExtensionId: 'extension-789',
146
+ rcUserName: 'Alex Johnson'
147
+ });
148
+ expect(axios.get).toHaveBeenCalledWith(
149
+ 'https://platform.ringcentral.com/restapi/v1.0/account/~/extension/~',
150
+ { headers: { Authorization: 'Bearer valid-user-token' } }
151
+ );
152
+ });
153
+
154
+ test('should throw when rcAccessToken is missing', async () => {
155
+ await expect(adminHandler.validateRcUserToken({})).rejects.toThrow('rcAccessToken is required');
156
+ expect(axios.get).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+
126
160
  describe('upsertAdminSettings', () => {
127
161
  test('should create new admin config when none exists', async () => {
128
162
  // Act
@@ -22,6 +22,8 @@ const oauth = require('../../lib/oauth');
22
22
  const { Connector } = require('../../models/dynamo/connectorSchema');
23
23
  const { RingCentral } = require('../../lib/ringcentral');
24
24
  const adminCore = require('../../handlers/admin');
25
+ const { AccountDataModel } = require('../../models/accountDataModel');
26
+ const { encode } = require('../../lib/encode');
25
27
 
26
28
  describe('Auth Handler', () => {
27
29
  const originalEnv = process.env;
@@ -31,6 +33,7 @@ describe('Auth Handler', () => {
31
33
  jest.clearAllMocks();
32
34
  global.testUtils.resetConnectorRegistry();
33
35
  process.env = { ...originalEnv };
36
+ process.env.APP_SERVER_SECRET_KEY = 'test-app-server-secret-key-123456';
34
37
  });
35
38
 
36
39
  afterEach(() => {
@@ -38,6 +41,10 @@ describe('Auth Handler', () => {
38
41
  });
39
42
 
40
43
  describe('onApiKeyLogin', () => {
44
+ afterEach(async () => {
45
+ await AccountDataModel.destroy({ where: {} });
46
+ });
47
+
41
48
  test('should handle successful API key login', async () => {
42
49
  // Arrange
43
50
  const mockUserInfo = {
@@ -60,7 +67,7 @@ describe('Auth Handler', () => {
60
67
  getBasicAuth: jest.fn().mockReturnValue('dGVzdC1hcGkta2V5Og=='),
61
68
  getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
62
69
  });
63
-
70
+
64
71
  connectorRegistry.getConnector.mockReturnValue(mockConnector);
65
72
 
66
73
  const requestData = {
@@ -82,7 +89,7 @@ describe('Auth Handler', () => {
82
89
  expect(mockConnector.getUserInfo).toHaveBeenCalledWith({
83
90
  authHeader: 'Basic dGVzdC1hcGkta2V5Og==',
84
91
  hostname: 'test.example.com',
85
- additionalInfo: {},
92
+ additionalInfo: { apiKey: 'test-api-key' },
86
93
  apiKey: 'test-api-key',
87
94
  platform: 'testCRM',
88
95
  proxyId: undefined
@@ -105,7 +112,7 @@ describe('Auth Handler', () => {
105
112
  getBasicAuth: jest.fn().mockReturnValue('dGVzdC1hcGkta2V5Og=='),
106
113
  getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
107
114
  });
108
-
115
+
109
116
  connectorRegistry.getConnector.mockReturnValue(mockConnector);
110
117
 
111
118
  const requestData = {
@@ -123,6 +130,394 @@ describe('Auth Handler', () => {
123
130
  expect(result.returnMessage).toEqual(mockUserInfo.returnMessage);
124
131
  });
125
132
 
133
+ test('should mark managed auth auto-login failure so the next attempt can fall back to manual auth', async () => {
134
+ connectorRegistry.getManifest.mockReturnValue({
135
+ platforms: {
136
+ testCRM: {
137
+ auth: {
138
+ type: 'apiKey',
139
+ apiKey: {
140
+ page: {
141
+ content: [
142
+ { const: 'tenantId', required: true, managed: true, managedScope: 'account' },
143
+ { const: 'apiKey', required: true, managed: true, managedScope: 'user' }
144
+ ]
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ });
151
+
152
+ await AccountDataModel.create({
153
+ rcAccountId: 'rc-account-fail',
154
+ platformName: 'testCRM',
155
+ dataKey: 'managed-auth-org',
156
+ data: {
157
+ fields: {
158
+ tenantId: { version: 1, encrypted: true, value: encode(JSON.stringify('tenant-1')) }
159
+ }
160
+ }
161
+ });
162
+ await AccountDataModel.create({
163
+ rcAccountId: 'rc-account-fail',
164
+ platformName: 'testCRM',
165
+ dataKey: 'managed-auth-user:101',
166
+ data: {
167
+ rcExtensionId: '101',
168
+ rcUserName: 'Agent 101',
169
+ fields: {
170
+ apiKey: { version: 1, encrypted: true, value: encode(JSON.stringify('bad-stored-key')) }
171
+ }
172
+ }
173
+ });
174
+
175
+ const mockConnector = global.testUtils.createMockConnector({
176
+ getBasicAuth: jest.fn().mockReturnValue('encoded-bad-key'),
177
+ getUserInfo: jest.fn().mockResolvedValue({
178
+ successful: false,
179
+ platformUserInfo: null,
180
+ returnMessage: {
181
+ messageType: 'error',
182
+ message: 'Invalid API key',
183
+ ttl: 3000
184
+ }
185
+ })
186
+ });
187
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
188
+
189
+ const result = await authHandler.onApiKeyLogin({
190
+ platform: 'testCRM',
191
+ hostname: 'test.example.com',
192
+ rcAccountId: 'rc-account-fail',
193
+ rcExtensionId: '101',
194
+ additionalInfo: {}
195
+ });
196
+
197
+ expect(result.userInfo).toBeNull();
198
+ const failureRecord = await AccountDataModel.findOne({
199
+ where: {
200
+ rcAccountId: 'rc-account-fail',
201
+ platformName: 'testCRM',
202
+ dataKey: 'managed-auth-login-failure:101'
203
+ }
204
+ });
205
+ expect(failureRecord).not.toBeNull();
206
+ });
207
+
208
+ test('should merge stored org managed auth values into additionalInfo', async () => {
209
+ connectorRegistry.getManifest.mockReturnValue({
210
+ platforms: {
211
+ testCRM: {
212
+ auth: {
213
+ type: 'apiKey',
214
+ apiKey: {
215
+ page: {
216
+ content: [
217
+ { const: 'apiKey', required: true, managed: true, managedScope: 'account' },
218
+ { const: 'tenantId', required: true, managed: true, managedScope: 'account' },
219
+ { const: 'userToken', required: true }
220
+ ]
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ });
227
+ await AccountDataModel.create({
228
+ rcAccountId: 'rc-account-1',
229
+ platformName: 'testCRM',
230
+ dataKey: 'managed-auth-org',
231
+ data: {
232
+ fields: {
233
+ apiKey: { version: 1, encrypted: true, value: encode(JSON.stringify('stored-api-key')) },
234
+ tenantId: { version: 1, encrypted: true, value: encode(JSON.stringify('tenant-1')) }
235
+ }
236
+ }
237
+ });
238
+
239
+ const mockUserInfo = {
240
+ successful: true,
241
+ platformUserInfo: {
242
+ id: 'test-user-id',
243
+ name: 'Test User',
244
+ platformAdditionalInfo: {}
245
+ },
246
+ returnMessage: { messageType: 'success', message: 'ok' }
247
+ };
248
+ const mockConnector = global.testUtils.createMockConnector({
249
+ getBasicAuth: jest.fn().mockReturnValue('encoded-shared'),
250
+ getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
251
+ });
252
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
253
+
254
+ await authHandler.onApiKeyLogin({
255
+ platform: 'testCRM',
256
+ hostname: 'test.example.com',
257
+ rcAccountId: 'rc-account-1',
258
+ additionalInfo: { userToken: 'user-token-1' }
259
+ });
260
+
261
+ expect(mockConnector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'stored-api-key' });
262
+ expect(mockConnector.getUserInfo).toHaveBeenCalledWith(expect.objectContaining({
263
+ additionalInfo: expect.objectContaining({
264
+ apiKey: 'stored-api-key',
265
+ tenantId: 'tenant-1',
266
+ userToken: 'user-token-1'
267
+ })
268
+ }));
269
+ });
270
+
271
+ test('should allow submitted shared fields to satisfy missing required managed auth values', async () => {
272
+ connectorRegistry.getManifest.mockReturnValue({
273
+ platforms: {
274
+ testCRM: {
275
+ auth: {
276
+ type: 'apiKey',
277
+ apiKey: {
278
+ page: {
279
+ content: [
280
+ { const: 'companyId', required: true, managed: true, managedScope: 'account' },
281
+ { const: 'userToken', required: true }
282
+ ]
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ });
289
+
290
+ const mockConnector = global.testUtils.createMockConnector({
291
+ getBasicAuth: jest.fn(),
292
+ getUserInfo: jest.fn().mockResolvedValue({
293
+ successful: true,
294
+ platformUserInfo: {
295
+ id: 'test-user-id',
296
+ name: 'Test User',
297
+ platformAdditionalInfo: {}
298
+ },
299
+ returnMessage: { messageType: 'success', message: 'ok' }
300
+ })
301
+ });
302
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
303
+
304
+ const result = await authHandler.onApiKeyLogin({
305
+ platform: 'testCRM',
306
+ hostname: 'test.example.com',
307
+ rcAccountId: 'rc-account-2',
308
+ additionalInfo: {
309
+ companyId: 'company-123',
310
+ userToken: 'user-token-1'
311
+ }
312
+ });
313
+
314
+ expect(result.userInfo).not.toBeNull();
315
+ expect(mockConnector.getUserInfo).toHaveBeenCalledWith(expect.objectContaining({
316
+ additionalInfo: expect.objectContaining({
317
+ companyId: 'company-123',
318
+ userToken: 'user-token-1'
319
+ })
320
+ }));
321
+ });
322
+
323
+ test('should not persist submitted managed auth values from end users', async () => {
324
+ connectorRegistry.getManifest.mockReturnValue({
325
+ platforms: {
326
+ testCRM: {
327
+ auth: {
328
+ type: 'apiKey',
329
+ apiKey: {
330
+ page: {
331
+ content: [
332
+ { const: 'companyId', required: false, managed: true, managedScope: 'account' },
333
+ { const: 'userToken', required: true }
334
+ ]
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ });
341
+
342
+ const mockUserInfo = {
343
+ successful: true,
344
+ platformUserInfo: {
345
+ id: 'test-user-id',
346
+ name: 'Test User',
347
+ platformAdditionalInfo: {}
348
+ },
349
+ returnMessage: { messageType: 'success', message: 'ok' }
350
+ };
351
+ const mockConnector = global.testUtils.createMockConnector({
352
+ getBasicAuth: jest.fn().mockReturnValue('encoded'),
353
+ getUserInfo: jest.fn().mockResolvedValue(mockUserInfo)
354
+ });
355
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
356
+
357
+ await authHandler.onApiKeyLogin({
358
+ platform: 'testCRM',
359
+ hostname: 'test.example.com',
360
+ rcAccountId: 'rc-account-2',
361
+ additionalInfo: {
362
+ companyId: 'company-123',
363
+ userToken: 'user-token-1'
364
+ }
365
+ });
366
+
367
+ expect(mockConnector.getUserInfo).toHaveBeenCalledWith(expect.objectContaining({
368
+ additionalInfo: expect.objectContaining({
369
+ companyId: 'company-123',
370
+ userToken: 'user-token-1'
371
+ })
372
+ }));
373
+
374
+ const stored = await AccountDataModel.findOne({
375
+ where: {
376
+ rcAccountId: 'rc-account-2',
377
+ platformName: 'testCRM',
378
+ dataKey: 'managed-auth-org'
379
+ }
380
+ });
381
+ expect(stored).toBeNull();
382
+ });
383
+
384
+ test('should allow manual fallback values to override stored managed credentials and clear failure state after success', async () => {
385
+ connectorRegistry.getManifest.mockReturnValue({
386
+ platforms: {
387
+ testCRM: {
388
+ auth: {
389
+ type: 'apiKey',
390
+ apiKey: {
391
+ page: {
392
+ content: [
393
+ { const: 'apiKey', required: true, managed: true, managedScope: 'user' },
394
+ { const: 'tenantId', required: true, managed: true, managedScope: 'account' }
395
+ ]
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+ });
402
+
403
+ await AccountDataModel.create({
404
+ rcAccountId: 'rc-account-recover',
405
+ platformName: 'testCRM',
406
+ dataKey: 'managed-auth-org',
407
+ data: {
408
+ fields: {
409
+ tenantId: { version: 1, encrypted: true, value: encode(JSON.stringify('stored-tenant')) }
410
+ }
411
+ }
412
+ });
413
+ await AccountDataModel.create({
414
+ rcAccountId: 'rc-account-recover',
415
+ platformName: 'testCRM',
416
+ dataKey: 'managed-auth-user:202',
417
+ data: {
418
+ rcExtensionId: '202',
419
+ rcUserName: 'Agent 202',
420
+ fields: {
421
+ apiKey: { version: 1, encrypted: true, value: encode(JSON.stringify('stored-bad-key')) }
422
+ }
423
+ }
424
+ });
425
+ await AccountDataModel.create({
426
+ rcAccountId: 'rc-account-recover',
427
+ platformName: 'testCRM',
428
+ dataKey: 'managed-auth-login-failure:202',
429
+ data: {
430
+ failedAt: '2026-04-07T00:00:00.000Z'
431
+ }
432
+ });
433
+
434
+ const mockConnector = global.testUtils.createMockConnector({
435
+ getBasicAuth: jest.fn().mockReturnValue('encoded-manual-key'),
436
+ getUserInfo: jest.fn().mockResolvedValue({
437
+ successful: true,
438
+ platformUserInfo: {
439
+ id: 'test-user-id',
440
+ name: 'Recovered User',
441
+ platformAdditionalInfo: {}
442
+ },
443
+ returnMessage: { messageType: 'success', message: 'ok' }
444
+ })
445
+ });
446
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
447
+
448
+ const result = await authHandler.onApiKeyLogin({
449
+ platform: 'testCRM',
450
+ hostname: 'test.example.com',
451
+ rcAccountId: 'rc-account-recover',
452
+ rcExtensionId: '202',
453
+ additionalInfo: {
454
+ apiKey: 'manual-good-key',
455
+ tenantId: 'manual-tenant'
456
+ }
457
+ });
458
+
459
+ expect(result.userInfo).not.toBeNull();
460
+ expect(mockConnector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'manual-good-key' });
461
+ expect(mockConnector.getUserInfo).toHaveBeenCalledWith(expect.objectContaining({
462
+ additionalInfo: {
463
+ apiKey: 'manual-good-key',
464
+ tenantId: 'manual-tenant'
465
+ }
466
+ }));
467
+
468
+ const failureRecord = await AccountDataModel.findOne({
469
+ where: {
470
+ rcAccountId: 'rc-account-recover',
471
+ platformName: 'testCRM',
472
+ dataKey: 'managed-auth-login-failure:202'
473
+ }
474
+ });
475
+ expect(failureRecord).toBeNull();
476
+ });
477
+
478
+ test('should return warning when required auth fields are missing', async () => {
479
+ connectorRegistry.getManifest.mockReturnValue({
480
+ platforms: {
481
+ testCRM: {
482
+ auth: {
483
+ type: 'apiKey',
484
+ apiKey: {
485
+ page: {
486
+ content: [
487
+ { const: 'tenantId', required: true, managed: true, managedScope: 'account' },
488
+ { const: 'userToken', required: true }
489
+ ]
490
+ }
491
+ }
492
+ }
493
+ }
494
+ }
495
+ });
496
+
497
+ const mockConnector = global.testUtils.createMockConnector({
498
+ getBasicAuth: jest.fn(),
499
+ getUserInfo: jest.fn()
500
+ });
501
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
502
+
503
+ const result = await authHandler.onApiKeyLogin({
504
+ platform: 'testCRM',
505
+ hostname: 'test.example.com',
506
+ rcAccountId: 'rc-account-4',
507
+ additionalInfo: {}
508
+ });
509
+
510
+ expect(result.userInfo).toBeNull();
511
+ expect(result.returnMessage).toEqual({
512
+ messageType: 'warning',
513
+ message: 'Missing required authentication fields.',
514
+ ttl: 3000,
515
+ missingRequiredFieldConsts: ['tenantId', 'userToken']
516
+ });
517
+ expect(mockConnector.getBasicAuth).not.toHaveBeenCalled();
518
+ expect(mockConnector.getUserInfo).not.toHaveBeenCalled();
519
+ });
520
+
126
521
  test('should throw error when connector not found', async () => {
127
522
  // Arrange
128
523
  connectorRegistry.getConnector.mockImplementation(() => {
@@ -160,7 +555,7 @@ describe('Auth Handler', () => {
160
555
  getOauthInfo: jest.fn().mockResolvedValue({}),
161
556
  authValidation: jest.fn().mockResolvedValue(mockValidationResponse)
162
557
  });
163
-
558
+
164
559
  connectorRegistry.getConnector.mockReturnValue(mockConnector);
165
560
 
166
561
  // Mock UserModel.findOne to return a user
@@ -229,7 +624,7 @@ describe('Auth Handler', () => {
229
624
  getOauthInfo: jest.fn().mockResolvedValue({}),
230
625
  authValidation: jest.fn().mockResolvedValue(mockValidationResponse)
231
626
  });
232
-
627
+
233
628
  connectorRegistry.getConnector.mockReturnValue(mockConnector);
234
629
 
235
630
  // Mock UserModel.findOne to return a user
@@ -616,4 +1011,5 @@ describe('Auth Handler', () => {
616
1011
  expect(RingCentral).not.toHaveBeenCalled();
617
1012
  });
618
1013
  });
619
- });
1014
+ });
1015
+