@app-connect/core 1.7.26 → 1.7.27

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/handlers/auth.js CHANGED
@@ -7,6 +7,7 @@ const adminCore = require('./admin');
7
7
  const { Connector } = require('../models/dynamo/connectorSchema');
8
8
  const { handleDatabaseError } = require('../lib/errorHandler');
9
9
  const managedAuthCore = require('./managedAuth');
10
+ const managedOAuthCore = require('./managedOAuth');
10
11
  const { getHashValue } = require('../lib/util');
11
12
 
12
13
  async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcExtensionId, isFromMCP = false }) {
@@ -20,7 +21,19 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcEx
20
21
  if (proxyId) {
21
22
  proxyConfig = await Connector.getProxyConfig(proxyId);
22
23
  }
23
- const oauthInfo = await platformModule.getOauthInfo({ tokenUrl, hostname, rcAccountId: query.rcAccountId, proxyId, proxyConfig, userEmail, isFromMCP });
24
+ let managedOAuthSource = null;
25
+ let oauthInfo = null;
26
+ if (query.rcAccountId) {
27
+ const managedOAuthResult = await managedOAuthCore.resolveManagedOAuthInfo({
28
+ rcAccountId: query.rcAccountId,
29
+ platform
30
+ });
31
+ managedOAuthSource = managedOAuthResult.source;
32
+ oauthInfo = managedOAuthResult.oauthInfo;
33
+ }
34
+ if (!oauthInfo) {
35
+ oauthInfo = await platformModule.getOauthInfo({ tokenUrl, hostname, rcAccountId: query.rcAccountId, proxyId, proxyConfig, userEmail, isFromMCP });
36
+ }
24
37
  if (oauthInfo.failMessage) {
25
38
  return {
26
39
  userInfo: null,
@@ -66,6 +79,12 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcEx
66
79
  if (platformModule.postSaveUserInfo) {
67
80
  userInfo = await platformModule.postSaveUserInfo({ userInfo, oauthApp });
68
81
  }
82
+ if (managedOAuthSource === 'pending') {
83
+ await managedOAuthCore.migratePendingManagedOAuth({
84
+ rcAccountId: query.rcAccountId,
85
+ platform
86
+ });
87
+ }
69
88
  return {
70
89
  userInfo,
71
90
  returnMessage
@@ -249,7 +268,12 @@ async function authValidation({ platform, userId }) {
249
268
  if (existingUser) {
250
269
  const platformModule = connectorRegistry.getConnector(platform);
251
270
  const proxyId = existingUser?.platformAdditionalInfo?.proxyId;
252
- const oauthApp = oauth.getOAuthApp((await platformModule.getOauthInfo({ tokenUrl: existingUser?.platformAdditionalInfo?.tokenUrl, hostname: existingUser?.hostname, proxyId })));
271
+ const managedOAuthResult = await managedOAuthCore.resolveManagedOAuthInfo({
272
+ rcAccountId: existingUser.rcAccountId,
273
+ platform
274
+ });
275
+ const oauthInfo = managedOAuthResult.oauthInfo ?? await platformModule.getOauthInfo({ tokenUrl: existingUser?.platformAdditionalInfo?.tokenUrl, hostname: existingUser?.hostname, proxyId });
276
+ const oauthApp = oauth.getOAuthApp(oauthInfo);
253
277
  existingUser = await oauth.checkAndRefreshAccessToken(oauthApp, existingUser);
254
278
  const { successful, returnMessage, status } = await platformModule.authValidation({ user: existingUser });
255
279
  return {
package/handlers/log.js CHANGED
@@ -184,7 +184,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
184
184
  }
185
185
  }
186
186
  )
187
- const syncedPluginJwtToken = syncPluginTokenResponse?.data?.jwtToken ?? pluginJwtToken;
187
+ const syncedPluginJwtToken = pluginCore.getRefreshedJwtTokenFromHeaders({ headers: syncPluginTokenResponse.headers });
188
188
  axios.post(pluginEndpointUrl, {
189
189
  data: incomingData,
190
190
  config: userConfig,
@@ -304,6 +304,12 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
304
304
  pluginAsyncTaskIds
305
305
  };
306
306
  }
307
+ else {
308
+ return {
309
+ successful: false,
310
+ returnMessage
311
+ };
312
+ }
307
313
  } catch (e) {
308
314
  return handleApiError(e, platform, 'createCallLog', { userId });
309
315
  }
@@ -0,0 +1,247 @@
1
+ const { AccountDataModel } = require('../models/accountDataModel');
2
+ const { CacheModel } = require('../models/cacheModel');
3
+ const { encode, decoded } = require('../lib/encode');
4
+
5
+ const MANAGED_OAUTH_ACCOUNT_DATA_KEY = 'managed-oauth-account';
6
+ const MANAGED_OAUTH_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const MANAGED_OAUTH_FIELDS = [
8
+ 'clientId',
9
+ 'clientSecret',
10
+ 'accessTokenUri',
11
+ 'authorizationUri',
12
+ 'redirectUri',
13
+ 'scopes',
14
+ 'hostname'
15
+ ];
16
+
17
+ function getPendingManagedOAuthCacheId({ rcAccountId }) {
18
+ return `${rcAccountId}-${MANAGED_OAUTH_ACCOUNT_DATA_KEY}`;
19
+ }
20
+
21
+ function isFilled(value) {
22
+ return value !== undefined && value !== null && value !== '';
23
+ }
24
+
25
+ function encryptStoredValue(value) {
26
+ return {
27
+ version: 1,
28
+ encrypted: true,
29
+ value: encode(JSON.stringify(value))
30
+ };
31
+ }
32
+
33
+ function decryptStoredValue(value) {
34
+ if (!value) {
35
+ return undefined;
36
+ }
37
+ if (value?.encrypted && value?.value) {
38
+ return JSON.parse(decoded(value.value));
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function normalizeManagedOAuthValues(values = {}) {
44
+ const normalized = {};
45
+ MANAGED_OAUTH_FIELDS.forEach(field => {
46
+ if (isFilled(values[field])) {
47
+ normalized[field] = values[field];
48
+ }
49
+ });
50
+ return normalized;
51
+ }
52
+
53
+ function buildStoredData(values = {}) {
54
+ const normalized = normalizeManagedOAuthValues(values);
55
+ const fields = {};
56
+ Object.keys(normalized).forEach(key => {
57
+ fields[key] = encryptStoredValue(normalized[key]);
58
+ });
59
+ return { fields };
60
+ }
61
+
62
+ function decryptStoredData(data = {}) {
63
+ const fields = data?.fields ?? {};
64
+ const values = {};
65
+ Object.keys(fields).forEach(key => {
66
+ values[key] = decryptStoredValue(fields[key]);
67
+ });
68
+ return values;
69
+ }
70
+
71
+ function removeSecret(values = {}) {
72
+ const sanitized = { ...values };
73
+ delete sanitized.clientSecret;
74
+ return sanitized;
75
+ }
76
+
77
+ async function getAccountManagedOAuthRecord({ rcAccountId, platform }) {
78
+ if (!rcAccountId || !platform) {
79
+ return null;
80
+ }
81
+ return AccountDataModel.findOne({
82
+ where: {
83
+ rcAccountId,
84
+ platformName: platform,
85
+ dataKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY
86
+ }
87
+ });
88
+ }
89
+
90
+ async function getPendingManagedOAuthRecord({ rcAccountId }) {
91
+ if (!rcAccountId) {
92
+ return null;
93
+ }
94
+ const record = await CacheModel.findByPk(getPendingManagedOAuthCacheId({ rcAccountId }));
95
+ if (!record) {
96
+ return null;
97
+ }
98
+ if (record.expiry && new Date(record.expiry).getTime() <= Date.now()) {
99
+ await record.destroy();
100
+ return null;
101
+ }
102
+ return record;
103
+ }
104
+
105
+ async function getAccountManagedOAuthValues({ rcAccountId, platform }) {
106
+ const record = await getAccountManagedOAuthRecord({ rcAccountId, platform });
107
+ return decryptStoredData(record?.data);
108
+ }
109
+
110
+ async function getPendingManagedOAuthValues({ rcAccountId }) {
111
+ const record = await getPendingManagedOAuthRecord({ rcAccountId });
112
+ return decryptStoredData(record?.data);
113
+ }
114
+
115
+ async function getManagedOAuthState({ rcAccountId, platform, isAdmin }) {
116
+ const accountValues = await getAccountManagedOAuthValues({ rcAccountId, platform });
117
+ const hasAccountOAuth = Object.keys(accountValues).length > 0;
118
+ const pendingValues = isAdmin && !hasAccountOAuth
119
+ ? await getPendingManagedOAuthValues({ rcAccountId })
120
+ : {};
121
+ const hasPendingOAuth = Object.keys(pendingValues).length > 0;
122
+
123
+ return {
124
+ isAdmin: !!isAdmin,
125
+ hasAccountOAuth,
126
+ hasPendingOAuth,
127
+ ...(hasAccountOAuth ? { oauthValues: removeSecret(accountValues) } : {}),
128
+ ...(isAdmin && hasPendingOAuth ? { pendingValues } : {})
129
+ };
130
+ }
131
+
132
+ async function upsertPendingManagedOAuth({ rcAccountId, values = {} }) {
133
+ if (!rcAccountId) {
134
+ throw new Error('rcAccountId is required');
135
+ }
136
+ const id = getPendingManagedOAuthCacheId({ rcAccountId });
137
+ const data = buildStoredData(values);
138
+ const expiry = new Date(Date.now() + MANAGED_OAUTH_CACHE_TTL_MS);
139
+ const existing = await CacheModel.findByPk(id);
140
+ if (existing) {
141
+ await existing.update({
142
+ status: 'pending',
143
+ userId: rcAccountId,
144
+ cacheKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY,
145
+ data,
146
+ expiry
147
+ });
148
+ return existing;
149
+ }
150
+ return CacheModel.create({
151
+ id,
152
+ status: 'pending',
153
+ userId: rcAccountId,
154
+ cacheKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY,
155
+ data,
156
+ expiry
157
+ });
158
+ }
159
+
160
+ async function migratePendingManagedOAuth({ rcAccountId, platform }) {
161
+ const pending = await getPendingManagedOAuthRecord({ rcAccountId });
162
+ if (!pending) {
163
+ return false;
164
+ }
165
+ const existing = await getAccountManagedOAuthRecord({ rcAccountId, platform });
166
+ const data = pending.data ?? { fields: {} };
167
+ if (existing) {
168
+ await existing.update({ data });
169
+ }
170
+ else {
171
+ await AccountDataModel.create({
172
+ rcAccountId,
173
+ platformName: platform,
174
+ dataKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY,
175
+ data
176
+ });
177
+ }
178
+ await pending.destroy();
179
+ return true;
180
+ }
181
+
182
+ async function resolveManagedOAuthInfo({ rcAccountId, platform }) {
183
+ const accountValues = await getAccountManagedOAuthValues({ rcAccountId, platform });
184
+ if (Object.keys(accountValues).length > 0) {
185
+ return {
186
+ source: 'account',
187
+ oauthInfo: accountValues
188
+ };
189
+ }
190
+ const pendingValues = await getPendingManagedOAuthValues({ rcAccountId });
191
+ if (Object.keys(pendingValues).length > 0) {
192
+ return {
193
+ source: 'pending',
194
+ oauthInfo: pendingValues
195
+ };
196
+ }
197
+ return {
198
+ source: null,
199
+ oauthInfo: null
200
+ };
201
+ }
202
+
203
+ async function clearPendingManagedOAuth({ rcAccountId }) {
204
+ if (!rcAccountId) {
205
+ return 0;
206
+ }
207
+ return CacheModel.destroy({
208
+ where: {
209
+ id: getPendingManagedOAuthCacheId({ rcAccountId })
210
+ }
211
+ });
212
+ }
213
+
214
+ async function clearAccountManagedOAuth({ rcAccountId, platform }) {
215
+ if (!rcAccountId || !platform) {
216
+ return 0;
217
+ }
218
+ return AccountDataModel.destroy({
219
+ where: {
220
+ rcAccountId,
221
+ platformName: platform,
222
+ dataKey: MANAGED_OAUTH_ACCOUNT_DATA_KEY
223
+ }
224
+ });
225
+ }
226
+
227
+ async function resetManagedOAuth({ rcAccountId, platform }) {
228
+ const deletedAccountCount = await clearAccountManagedOAuth({ rcAccountId, platform });
229
+ const deletedPendingCount = await clearPendingManagedOAuth({ rcAccountId });
230
+ return {
231
+ deletedAccountCount,
232
+ deletedPendingCount
233
+ };
234
+ }
235
+
236
+ exports.MANAGED_OAUTH_ACCOUNT_DATA_KEY = MANAGED_OAUTH_ACCOUNT_DATA_KEY;
237
+ exports.MANAGED_OAUTH_FIELDS = MANAGED_OAUTH_FIELDS;
238
+ exports.getManagedOAuthState = getManagedOAuthState;
239
+ exports.upsertPendingManagedOAuth = upsertPendingManagedOAuth;
240
+ exports.getPendingManagedOAuthValues = getPendingManagedOAuthValues;
241
+ exports.getAccountManagedOAuthValues = getAccountManagedOAuthValues;
242
+ exports.migratePendingManagedOAuth = migratePendingManagedOAuth;
243
+ exports.resolveManagedOAuthInfo = resolveManagedOAuthInfo;
244
+ exports.clearPendingManagedOAuth = clearPendingManagedOAuth;
245
+ exports.clearAccountManagedOAuth = clearAccountManagedOAuth;
246
+ exports.resetManagedOAuth = resetManagedOAuth;
247
+ exports.removeSecret = removeSecret;
package/index.js CHANGED
@@ -41,6 +41,7 @@ const {
41
41
  sqliteCallLogsPkIncludesExtension,
42
42
  } = require('./lib/migrateCallLogsSchema');
43
43
  const managedAuthCore = require('./handlers/managedAuth');
44
+ const managedOAuthCore = require('./handlers/managedOAuth');
44
45
 
45
46
  let packageJson = null;
46
47
  try {
@@ -80,83 +81,6 @@ async function initDB() {
80
81
  await CacheModel.sync();
81
82
  await CallDownListModel.sync();
82
83
  await AccountDataModel.sync();
83
-
84
- const queryInterface = CallLogModel.sequelize.getQueryInterface();
85
- const sequelizeInstance = CallLogModel.sequelize;
86
- const dialect = sequelizeInstance.getDialect();
87
- const callLogsColumns = await queryInterface.describeTable('callLogs');
88
- const hasExtensionNumber = Object.keys(callLogsColumns).some(
89
- (col) => col.toLowerCase() === 'extensionnumber',
90
- );
91
- // Sequelize's SQLite addConstraint appends PRIMARY KEY instead of replacing it.
92
- const needsSqliteCallLogsRebuild =
93
- dialect === 'sqlite' &&
94
- (!hasExtensionNumber || !(await sqliteCallLogsPkIncludesExtension(sequelizeInstance)));
95
-
96
- if (needsSqliteCallLogsRebuild) {
97
- const transaction = await sequelizeInstance.transaction();
98
-
99
- try {
100
- await migrateCallLogsExtensionNumberSqlite(sequelizeInstance, transaction);
101
- await transaction.commit();
102
- } catch (err) {
103
- await transaction.rollback();
104
- throw err;
105
- }
106
- } else if (!hasExtensionNumber) {
107
- // sync() fills new Postgres DBs; legacy installs need alter + new PK constraint.
108
- const transaction = await sequelizeInstance.transaction();
109
-
110
- try {
111
- await queryInterface.addColumn('callLogs', 'extensionNumber', {
112
- type: Sequelize.STRING,
113
- defaultValue: '',
114
- allowNull: false,
115
- }, { transaction });
116
-
117
- // Postgres already has a PK on (id, sessionId); addConstraint would try to add a second PK and fail.
118
- if (dialect === 'postgres') {
119
- const relname = CallLogModel.tableName;
120
- const pkRows = await sequelizeInstance.query(
121
- `
122
- SELECT c.conname AS name
123
- FROM pg_constraint c
124
- INNER JOIN pg_class rel ON c.conrelid = rel.oid
125
- INNER JOIN pg_namespace nsp ON rel.relnamespace = nsp.oid
126
- WHERE c.contype = 'p'
127
- AND nsp.nspname = current_schema()
128
- AND rel.relname = :relname
129
- `,
130
- {
131
- replacements: { relname },
132
- transaction,
133
- type: Sequelize.QueryTypes.SELECT,
134
- },
135
- );
136
- const pkName = pkRows[0]?.name;
137
- if (pkName) {
138
- const qTable = queryInterface.queryGenerator.quoteTable(CallLogModel.tableName);
139
- const qName = queryInterface.queryGenerator.quoteIdentifier(pkName);
140
- await sequelizeInstance.query(
141
- `ALTER TABLE ${qTable} DROP CONSTRAINT ${qName}`,
142
- { transaction },
143
- );
144
- }
145
- }
146
-
147
- await queryInterface.addConstraint('callLogs', {
148
- fields: ['id', 'sessionId', 'extensionNumber'],
149
- type: 'primary key',
150
- name: 'callLogs_pkey',
151
- transaction,
152
- });
153
-
154
- await transaction.commit();
155
- } catch (err) {
156
- await transaction.rollback();
157
- throw err;
158
- }
159
- }
160
84
  }
161
85
  }
162
86
 
@@ -518,6 +442,35 @@ function createCoreRouter() {
518
442
  res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
519
443
  }
520
444
  });
445
+ router.get('/oauthManagedAuthState', async function (req, res) {
446
+ const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
447
+ tracer?.trace('oauthManagedAuthState:start', { query: req.query });
448
+ try {
449
+ const platform = req.query.platform;
450
+ const rcAccessToken = req.query.rcAccessToken;
451
+ if (!platform) {
452
+ res.status(400).send(tracer ? tracer.wrapResponse('Missing platform name') : 'Missing platform name');
453
+ return;
454
+ }
455
+ if (!rcAccessToken) {
456
+ res.status(400).send(tracer ? tracer.wrapResponse('Missing RingCentral access token') : 'Missing RingCentral access token');
457
+ return;
458
+ }
459
+ const { rcAccountId } = await adminCore.validateRcUserToken({ rcAccessToken });
460
+ const adminValidation = await adminCore.validateAdminRole({ rcAccessToken });
461
+ const state = await managedOAuthCore.getManagedOAuthState({
462
+ platform,
463
+ rcAccountId,
464
+ isAdmin: !!adminValidation.isValidated
465
+ });
466
+ res.status(200).send(tracer ? tracer.wrapResponse(state) : state);
467
+ }
468
+ catch (e) {
469
+ logger.error('Get managed OAuth state failed', { stack: e.stack });
470
+ tracer?.traceError('oauthManagedAuthState:error', e);
471
+ res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
472
+ }
473
+ });
521
474
  // Obsolete
522
475
  router.get('/serverVersionInfo', (req, res) => {
523
476
  const defaultCrmManifest = connectorRegistry.getManifest('default');
@@ -711,6 +664,75 @@ function createCoreRouter() {
711
664
  res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
712
665
  }
713
666
  });
667
+ router.post('/admin/managedOAuth/cache', async function (req, res) {
668
+ const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
669
+ tracer?.trace('setAdminManagedOAuthCache:start', {
670
+ body: {
671
+ hasValues: !!req.body?.values,
672
+ valueKeys: Object.keys(req.body?.values ?? {}).filter(key => key !== 'clientSecret')
673
+ }
674
+ });
675
+ try {
676
+ const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
677
+ if (!isValidated) {
678
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
679
+ return;
680
+ }
681
+ await managedOAuthCore.upsertPendingManagedOAuth({
682
+ rcAccountId: rcAccountId?.toString(),
683
+ values: req.body?.values ?? {}
684
+ });
685
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful: true }) : { successful: true });
686
+ }
687
+ catch (e) {
688
+ logger.error('Set managed OAuth pending cache failed', { stack: e.stack });
689
+ tracer?.traceError('setAdminManagedOAuthCache:error', e);
690
+ res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
691
+ }
692
+ });
693
+ router.delete('/admin/managedOAuth/cache', async function (req, res) {
694
+ const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
695
+ tracer?.trace('deleteAdminManagedOAuthCache:start', {});
696
+ try {
697
+ const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
698
+ if (!isValidated) {
699
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
700
+ return;
701
+ }
702
+ await managedOAuthCore.clearPendingManagedOAuth({ rcAccountId: rcAccountId?.toString() });
703
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful: true }) : { successful: true });
704
+ }
705
+ catch (e) {
706
+ logger.error('Delete managed OAuth pending cache failed', { stack: e.stack });
707
+ tracer?.traceError('deleteAdminManagedOAuthCache:error', e);
708
+ res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
709
+ }
710
+ });
711
+ router.delete('/admin/managedOAuth/account', async function (req, res) {
712
+ const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
713
+ tracer?.trace('deleteAdminManagedOAuthAccount:start', { query: req.query });
714
+ try {
715
+ const { isValidated, rcAccountId } = await adminCore.validateAdminRole({ rcAccessToken: req.query.rcAccessToken });
716
+ if (!isValidated) {
717
+ res.status(403).send(tracer ? tracer.wrapResponse('Admin validation failed') : 'Admin validation failed');
718
+ return;
719
+ }
720
+ if (!req.query.platform) {
721
+ res.status(400).send(tracer ? tracer.wrapResponse('Missing platform name') : 'Missing platform name');
722
+ return;
723
+ }
724
+ await managedOAuthCore.resetManagedOAuth({
725
+ rcAccountId: rcAccountId?.toString(),
726
+ platform: req.query.platform
727
+ });
728
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful: true }) : { successful: true });
729
+ }
730
+ catch (e) {
731
+ logger.error('Delete managed OAuth account failed', { stack: e.stack });
732
+ tracer?.traceError('deleteAdminManagedOAuthAccount:error', e);
733
+ res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
734
+ }
735
+ });
714
736
  router.post('/admin/userMapping', async function (req, res) {
715
737
  const requestStartTime = new Date().getTime();
716
738
  const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
@@ -71,7 +71,10 @@ async function execute(args) {
71
71
  }
72
72
  const platformModule = connectorRegistry.getConnector(platform);
73
73
  try {
74
- await platformModule.unAuthorize({ user: userToLogout });
74
+ const logoutResult = await platformModule.unAuthorize({ user: userToLogout });
75
+ if (!logoutResult.successful) {
76
+ throw new Error(logoutResult.returnMessage.message);
77
+ }
75
78
  }
76
79
  catch (error) {
77
80
  console.log(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.26",
3
+ "version": "1.7.27",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,12 @@
1
1
  {
2
+ "1.7.27": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Managed OAuth for platforms with dynamic OAuth configs for different accounts"
7
+ }
8
+ ]
9
+ },
2
10
  "1.7.26": {
3
11
  "global": [
4
12
  {
@@ -23,6 +23,7 @@ const { Connector } = require('../../models/dynamo/connectorSchema');
23
23
  const { RingCentral } = require('../../lib/ringcentral');
24
24
  const adminCore = require('../../handlers/admin');
25
25
  const { AccountDataModel } = require('../../models/accountDataModel');
26
+ const { CacheModel } = require('../../models/cacheModel');
26
27
  const { encode } = require('../../lib/encode');
27
28
  const { getHashValue } = require('../../lib/util');
28
29
 
@@ -664,6 +665,11 @@ describe('Auth Handler', () => {
664
665
  oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
665
666
  });
666
667
 
668
+ afterEach(async () => {
669
+ await AccountDataModel.destroy({ where: {} });
670
+ await CacheModel.destroy({ where: {} });
671
+ });
672
+
667
673
  test('should handle successful OAuth callback', async () => {
668
674
  // Arrange
669
675
  const mockUserInfo = {
@@ -892,6 +898,74 @@ describe('Auth Handler', () => {
892
898
  overridingOption
893
899
  );
894
900
  });
901
+
902
+ test('should use pending managed OAuth info and promote it after successful callback', async () => {
903
+ await CacheModel.create({
904
+ id: 'rc-managed-managed-oauth-account',
905
+ status: 'pending',
906
+ userId: 'rc-managed',
907
+ cacheKey: 'managed-oauth-account',
908
+ expiry: new Date(Date.now() + 60000),
909
+ data: {
910
+ fields: {
911
+ clientId: { version: 1, encrypted: true, value: encode(JSON.stringify('managed-client-id')) },
912
+ clientSecret: { version: 1, encrypted: true, value: encode(JSON.stringify('managed-client-secret')) },
913
+ accessTokenUri: { version: 1, encrypted: true, value: encode(JSON.stringify('https://managed.example.com/token')) },
914
+ authorizationUri: { version: 1, encrypted: true, value: encode(JSON.stringify('https://managed.example.com/authorize')) },
915
+ redirectUri: { version: 1, encrypted: true, value: encode(JSON.stringify('https://ringcentral.github.io/ringcentral-embeddable/redirect.html')) },
916
+ hostname: { version: 1, encrypted: true, value: encode(JSON.stringify('managed.example.com')) }
917
+ }
918
+ }
919
+ });
920
+
921
+ const mockConnector = global.testUtils.createMockConnector({
922
+ getOauthInfo: jest.fn(),
923
+ getUserInfo: jest.fn().mockResolvedValue({
924
+ successful: true,
925
+ platformUserInfo: { id: 'managed-oauth-user', name: 'Managed OAuth User' },
926
+ returnMessage: { messageType: 'success', message: 'OK' }
927
+ })
928
+ });
929
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
930
+ mockOAuthApp.code.getToken.mockResolvedValue({
931
+ accessToken: 'token',
932
+ refreshToken: 'refresh',
933
+ expires: new Date()
934
+ });
935
+
936
+ const result = await authHandler.onOAuthCallback({
937
+ platform: 'testCRM',
938
+ hostname: 'old.example.com',
939
+ tokenUrl: '',
940
+ query: {
941
+ callbackUri: 'https://app.example.com/callback?code=code123',
942
+ rcAccountId: 'rc-managed'
943
+ }
944
+ });
945
+
946
+ expect(result.userInfo.id).toBe('managed-oauth-user');
947
+ expect(mockConnector.getOauthInfo).not.toHaveBeenCalled();
948
+ expect(oauth.getOAuthApp).toHaveBeenCalledWith(expect.objectContaining({
949
+ clientId: 'managed-client-id',
950
+ clientSecret: 'managed-client-secret',
951
+ accessTokenUri: 'https://managed.example.com/token',
952
+ authorizationUri: 'https://managed.example.com/authorize'
953
+ }));
954
+ expect(mockConnector.getUserInfo).toHaveBeenCalledWith(expect.objectContaining({
955
+ hostname: 'managed.example.com',
956
+ tokenUrl: 'https://managed.example.com/token'
957
+ }));
958
+ const promoted = await AccountDataModel.findOne({
959
+ where: {
960
+ rcAccountId: 'rc-managed',
961
+ platformName: 'testCRM',
962
+ dataKey: 'managed-oauth-account'
963
+ }
964
+ });
965
+ expect(promoted).not.toBeNull();
966
+ const pending = await CacheModel.findByPk('rc-managed-managed-oauth-account');
967
+ expect(pending).toBeNull();
968
+ });
895
969
  });
896
970
 
897
971
  describe('getLicenseStatus', () => {
@@ -8,6 +8,12 @@ jest.mock('../../handlers/admin', () => ({
8
8
  jest.mock('../../handlers/managedAuth', () => ({
9
9
  getManagedAuthState: jest.fn(),
10
10
  }));
11
+ jest.mock('../../handlers/managedOAuth', () => ({
12
+ getManagedOAuthState: jest.fn(),
13
+ upsertPendingManagedOAuth: jest.fn(),
14
+ clearPendingManagedOAuth: jest.fn(),
15
+ resetManagedOAuth: jest.fn(),
16
+ }));
11
17
  jest.mock('../../handlers/auth', () => ({
12
18
  onApiKeyLogin: jest.fn(),
13
19
  }));
@@ -18,6 +24,7 @@ jest.mock('../../lib/jwt', () => ({
18
24
 
19
25
  const adminCore = require('../../handlers/admin');
20
26
  const managedAuthCore = require('../../handlers/managedAuth');
27
+ const managedOAuthCore = require('../../handlers/managedOAuth');
21
28
  const authCore = require('../../handlers/auth');
22
29
  const { createCoreRouter } = require('../../index');
23
30
 
@@ -63,6 +70,130 @@ describe('Managed Auth Routes', () => {
63
70
  });
64
71
  });
65
72
 
73
+ describe('GET /oauthManagedAuthState', () => {
74
+ test('should validate rcAccessToken and return managed OAuth state for validated account', async () => {
75
+ adminCore.validateRcUserToken.mockResolvedValue({
76
+ rcAccountId: 'validated-account-id',
77
+ rcExtensionId: 'validated-extension-id',
78
+ });
79
+ adminCore.validateAdminRole.mockResolvedValue({
80
+ isValidated: true,
81
+ rcAccountId: 'validated-account-id',
82
+ });
83
+ managedOAuthCore.getManagedOAuthState.mockResolvedValue({
84
+ isAdmin: true,
85
+ hasAccountOAuth: false,
86
+ hasPendingOAuth: true,
87
+ pendingValues: {
88
+ clientId: 'client-id',
89
+ clientSecret: 'client-secret',
90
+ },
91
+ });
92
+
93
+ const response = await request(app)
94
+ .get('/oauthManagedAuthState')
95
+ .query({
96
+ platform: 'testCRM',
97
+ rcAccessToken: 'valid-rc-token',
98
+ rcAccountId: 'spoofed-account-id',
99
+ });
100
+
101
+ expect(response.status).toBe(200);
102
+ expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
103
+ expect(adminCore.validateAdminRole).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
104
+ expect(managedOAuthCore.getManagedOAuthState).toHaveBeenCalledWith({
105
+ platform: 'testCRM',
106
+ rcAccountId: 'validated-account-id',
107
+ isAdmin: true,
108
+ });
109
+ expect(response.body.pendingValues.clientSecret).toBe('client-secret');
110
+ });
111
+ });
112
+
113
+ describe('POST /admin/managedOAuth/cache', () => {
114
+ test('should require admin role and write pending managed OAuth values for validated account', async () => {
115
+ adminCore.validateAdminRole.mockResolvedValue({
116
+ isValidated: true,
117
+ rcAccountId: 'validated-account-id',
118
+ });
119
+ managedOAuthCore.upsertPendingManagedOAuth.mockResolvedValue({});
120
+
121
+ const values = {
122
+ clientId: 'client-id',
123
+ clientSecret: 'client-secret',
124
+ };
125
+
126
+ const response = await request(app)
127
+ .post('/admin/managedOAuth/cache')
128
+ .query({ rcAccessToken: 'valid-rc-token' })
129
+ .send({ values });
130
+
131
+ expect(response.status).toBe(200);
132
+ expect(managedOAuthCore.upsertPendingManagedOAuth).toHaveBeenCalledWith({
133
+ rcAccountId: 'validated-account-id',
134
+ values,
135
+ });
136
+ });
137
+
138
+ test('should reject non-admin users', async () => {
139
+ adminCore.validateAdminRole.mockResolvedValue({
140
+ isValidated: false,
141
+ rcAccountId: 'validated-account-id',
142
+ });
143
+
144
+ const response = await request(app)
145
+ .post('/admin/managedOAuth/cache')
146
+ .query({ rcAccessToken: 'valid-rc-token' })
147
+ .send({ values: { clientId: 'client-id' } });
148
+
149
+ expect(response.status).toBe(403);
150
+ expect(managedOAuthCore.upsertPendingManagedOAuth).not.toHaveBeenCalled();
151
+ });
152
+ });
153
+
154
+ describe('DELETE /admin/managedOAuth/account', () => {
155
+ test('should require admin role and delete managed OAuth account for validated account', async () => {
156
+ adminCore.validateAdminRole.mockResolvedValue({
157
+ isValidated: true,
158
+ rcAccountId: 'validated-account-id',
159
+ });
160
+ managedOAuthCore.resetManagedOAuth.mockResolvedValue({
161
+ deletedAccountCount: 1,
162
+ deletedPendingCount: 1,
163
+ });
164
+
165
+ const response = await request(app)
166
+ .delete('/admin/managedOAuth/account')
167
+ .query({
168
+ rcAccessToken: 'valid-rc-token',
169
+ platform: 'testCRM',
170
+ });
171
+
172
+ expect(response.status).toBe(200);
173
+ expect(managedOAuthCore.resetManagedOAuth).toHaveBeenCalledWith({
174
+ rcAccountId: 'validated-account-id',
175
+ platform: 'testCRM',
176
+ });
177
+ });
178
+
179
+ test('should reject non-admin users', async () => {
180
+ adminCore.validateAdminRole.mockResolvedValue({
181
+ isValidated: false,
182
+ rcAccountId: 'validated-account-id',
183
+ });
184
+
185
+ const response = await request(app)
186
+ .delete('/admin/managedOAuth/account')
187
+ .query({
188
+ rcAccessToken: 'valid-rc-token',
189
+ platform: 'testCRM',
190
+ });
191
+
192
+ expect(response.status).toBe(403);
193
+ expect(managedOAuthCore.resetManagedOAuth).not.toHaveBeenCalled();
194
+ });
195
+ });
196
+
66
197
  describe('POST /apiKeyLogin', () => {
67
198
 
68
199
  test('should validate rcAccessToken and ignore spoofed rc ids in body', async () => {