@app-connect/core 1.7.33 → 1.7.35

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
@@ -50,7 +50,7 @@ async function onOAuthCallback({ platform, hostname, tokenUrl, query, hashedRcEx
50
50
  let overridingOAuthOption = null;
51
51
  if (platformModule.getOverridingOAuthOption != null) {
52
52
  const code = new URL(callbackUri).searchParams.get('code');
53
- overridingOAuthOption = platformModule.getOverridingOAuthOption({ code });
53
+ overridingOAuthOption = platformModule.getOverridingOAuthOption({ code, oauthInfo });
54
54
  }
55
55
  const oauthApp = oauth.getOAuthApp(oauthInfo);
56
56
  const { accessToken, refreshToken, expires, data } = await oauthApp.code.getToken(callbackUri, overridingOAuthOption);
package/handlers/log.js CHANGED
@@ -103,6 +103,7 @@ async function runSyncCallPlugins({ syncCallPlugins, incomingData, user, platfor
103
103
  const processedResultResponse = await axios.post(pluginEndpointUrl, {
104
104
  data: processedIncomingData,
105
105
  config: userConfig,
106
+ hashedExtensionId: user.hashedRcExtensionId,
106
107
  }, {
107
108
  headers: {
108
109
  Authorization: `Bearer ${pluginJwtToken}`,
@@ -182,6 +183,7 @@ async function dispatchAsyncCallPlugin({ plugin, incomingData, user, platform, o
182
183
  config: pluginCore.getPluginConfigFromUserSettings({ userSettings: user.userSettings, pluginId }),
183
184
  asyncTaskId: taskId,
184
185
  callbackUrl,
186
+ hashedExtensionId: user.hashedRcExtensionId,
185
187
  }, {
186
188
  headers: {
187
189
  Authorization: `Bearer ${syncedPluginJwtToken ?? pluginJwtToken}`,
package/index.js CHANGED
@@ -118,13 +118,12 @@ function normalizeJwtFromRequest(req, res, next) {
118
118
  const originalBearerToken = getBearerTokenFromRequest(req);
119
119
  const queryToken = req.query?.jwtToken;
120
120
  let bearerToken = originalBearerToken;
121
+ const isQueryAuth = !originalBearerToken && !!queryToken;
121
122
 
122
123
  // Backward compatibility: promote query jwtToken to Authorization Bearer.
123
- if (!bearerToken && queryToken) {
124
+ if (isQueryAuth) {
124
125
  req.headers.authorization = `Bearer ${queryToken}`;
125
126
  bearerToken = queryToken;
126
- // Don't refresh JWT because old version cannot update its local token storage to support refreshed token.
127
- return next();
128
127
  }
129
128
 
130
129
  const token = bearerToken;
@@ -136,7 +135,7 @@ function normalizeJwtFromRequest(req, res, next) {
136
135
  const decodedToken = jwt.decodeJwt(token);
137
136
  if (!decodedToken?.id) {
138
137
  req.invalidJwtToken = true;
139
- if (req.query?.jwtToken) {
138
+ if (originalBearerToken && req.query?.jwtToken) {
140
139
  delete req.query.jwtToken;
141
140
  }
142
141
  return next();
@@ -148,16 +147,17 @@ function normalizeJwtFromRequest(req, res, next) {
148
147
  if (typeof decodedToken.exp === 'number') {
149
148
  const now = Math.floor(Date.now() / 1000);
150
149
  const timeLeft = decodedToken.exp - now;
151
- const isBearerAuth = !!originalBearerToken;
152
150
  const shouldRefreshNearExpiry = timeLeft <= JWT_REFRESH_THRESHOLD_SECONDS;
153
- // Rotate legacy 120y tokens only for Bearer auth clients (they can persist refreshed headers).
154
- const shouldRefreshLegacyLongLivedBearer = isBearerAuth && timeLeft > JWT_LEGACY_LONG_LIVED_THRESHOLD_SECONDS;
155
- if (shouldRefreshNearExpiry || shouldRefreshLegacyLongLivedBearer) {
151
+ if (shouldRefreshNearExpiry) {
156
152
  const refreshedToken = jwt.generateJwt({
157
153
  id: decodedToken.id.toString(),
158
154
  platform: decodedToken.platform
159
155
  });
160
156
  res.setHeader('x-refreshed-jwt-token', refreshedToken);
157
+ req.headers.authorization = `Bearer ${refreshedToken}`;
158
+ if (isQueryAuth && req.query?.jwtToken) {
159
+ req.query.jwtToken = refreshedToken;
160
+ }
161
161
  req.jwtToken = refreshedToken;
162
162
  req.jwtAuth = jwt.decodeJwt(refreshedToken) || decodedToken;
163
163
  }
@@ -1040,6 +1040,11 @@ function createCoreRouter() {
1040
1040
  if (jwtToken) {
1041
1041
  const unAuthData = jwt.decodeJwt(jwtToken);
1042
1042
  platformName = unAuthData?.platform ?? 'Unknown';
1043
+ if(!unAuthData || !unAuthData?.id) {
1044
+ tracer?.trace('getUserSettings:noToken', {});
1045
+ res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
1046
+ return;
1047
+ }
1043
1048
  const user = await UserModel.findByPk(unAuthData?.id);
1044
1049
  if (!user) {
1045
1050
  tracer?.trace('getUserSettings:userNotFound', {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.33",
3
+ "version": "1.7.35",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,52 @@
1
1
  {
2
+ "1.7.35": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Can show RingCentral Account Id on support page"
7
+ },
8
+ {
9
+ "type": "New",
10
+ "description": "Add hashedExtensionId in plugin calls"
11
+ },
12
+ {
13
+ "type": "New",
14
+ "description": "Show notification when having incoming call while minized"
15
+ },
16
+ {
17
+ "type": "Fix",
18
+ "description": "User session refresh and validtion issue"
19
+ },
20
+ {
21
+ "type": "Fix",
22
+ "description": "Warm-transfer call pop"
23
+ },
24
+ {
25
+ "type": "Fix",
26
+ "description": "Refresh manifest for override properties"
27
+ }
28
+ ]
29
+ },
30
+ "1.7.34": {
31
+ "global": [
32
+ {
33
+ "type": "New",
34
+ "description": "Call pop fallback url when no matched contact"
35
+ },
36
+ {
37
+ "type": "Fix",
38
+ "description": "Hostname page input debounce"
39
+ },
40
+ {
41
+ "type": "Fix",
42
+ "description": "RingCentral additional info submission during auth"
43
+ },
44
+ {
45
+ "type": "Fix",
46
+ "description": "Managed auth, admins can now see hidden fields"
47
+ }
48
+ ]
49
+ },
2
50
  "1.7.33": {
3
51
  "global": [
4
52
  {
@@ -863,8 +863,9 @@ describe('Auth Handler', () => {
863
863
  test('should use overriding OAuth option if provided', async () => {
864
864
  // Arrange
865
865
  const overridingOption = { redirect_uri: 'custom-redirect' };
866
+ const oauthInfo = { clientId: 'id', clientSecret: 'secret' };
866
867
  const mockConnector = global.testUtils.createMockConnector({
867
- getOauthInfo: jest.fn().mockResolvedValue({ clientId: 'id', clientSecret: 'secret' }),
868
+ getOauthInfo: jest.fn().mockResolvedValue(oauthInfo),
868
869
  getOverridingOAuthOption: jest.fn().mockReturnValue(overridingOption),
869
870
  getUserInfo: jest.fn().mockResolvedValue({
870
871
  successful: true,
@@ -892,7 +893,10 @@ describe('Auth Handler', () => {
892
893
  await authHandler.onOAuthCallback(requestData);
893
894
 
894
895
  // Assert
895
- expect(mockConnector.getOverridingOAuthOption).toHaveBeenCalledWith({ code: 'code123' });
896
+ expect(mockConnector.getOverridingOAuthOption).toHaveBeenCalledWith({
897
+ code: 'code123',
898
+ oauthInfo: { clientId: 'id', clientSecret: 'secret' }
899
+ });
896
900
  expect(mockOAuthApp.code.getToken).toHaveBeenCalledWith(
897
901
  expect.any(String),
898
902
  overridingOption
@@ -324,7 +324,7 @@ describe('Log Handler', () => {
324
324
  expect(result.successful).toBe(true);
325
325
  expect(axios.post).toHaveBeenCalledWith(
326
326
  'https://plugins.example.com/plugin/testPlugin',
327
- { data: mockIncomingData, config: null },
327
+ { data: mockIncomingData, config: null, hashedExtensionId: null },
328
328
  {
329
329
  headers: {
330
330
  Authorization: 'Bearer plugin-jwt-token'
@@ -402,7 +402,8 @@ describe('Log Handler', () => {
402
402
  data: mockIncomingData,
403
403
  config: null,
404
404
  asyncTaskId: cache.id,
405
- callbackUrl: `https://app.example.com/plugin/async-callback/${cache.id}`
405
+ callbackUrl: `https://app.example.com/plugin/async-callback/${cache.id}`,
406
+ hashedExtensionId: null
406
407
  });
407
408
  expect(pluginCall[2]).toEqual({
408
409
  headers: {
@@ -33,7 +33,7 @@ describe('Core Router JWT normalization', () => {
33
33
  jest.clearAllMocks();
34
34
  });
35
35
 
36
- test('should accept query jwtToken without refreshing it', async () => {
36
+ test('should accept query jwtToken without exp without refreshing it', async () => {
37
37
  jwt.decodeJwt.mockReturnValue({ id: 'user-1', platform: 'testCRM' });
38
38
  authCore.authValidation.mockResolvedValue({
39
39
  successful: true,
@@ -54,6 +54,69 @@ describe('Core Router JWT normalization', () => {
54
54
  expect(jwt.generateJwt).not.toHaveBeenCalled();
55
55
  });
56
56
 
57
+ test('should refresh near-expiry query jwtToken and expose header', async () => {
58
+ const nowMs = 1700000000000;
59
+ const nowSeconds = Math.floor(nowMs / 1000);
60
+ const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
61
+ jwt.decodeJwt.mockImplementation((token) => {
62
+ if (token === 'query-token') {
63
+ return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + 60 };
64
+ }
65
+ if (token === 'new-token') {
66
+ return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + (14 * 24 * 60 * 60) };
67
+ }
68
+ return null;
69
+ });
70
+ jwt.generateJwt.mockReturnValue('new-token');
71
+ authCore.authValidation.mockResolvedValue({
72
+ successful: true,
73
+ returnMessage: { message: 'ok' },
74
+ failReason: null,
75
+ status: 200,
76
+ });
77
+ const app = buildApp();
78
+
79
+ const response = await request(app)
80
+ .get('/authValidation?jwtToken=query-token')
81
+ .set('Origin', 'https://example.com');
82
+
83
+ expect(response.status).toBe(200);
84
+ expect(response.headers['x-refreshed-jwt-token']).toBe('new-token');
85
+ expect(response.headers['access-control-expose-headers']).toContain('x-refreshed-jwt-token');
86
+ expect(jwt.generateJwt).toHaveBeenCalledWith({ id: 'user-1', platform: 'testCRM' });
87
+ expect(authCore.authValidation).toHaveBeenCalledWith({
88
+ platform: 'testCRM',
89
+ userId: 'user-1',
90
+ });
91
+ expect(jwt.decodeJwt).toHaveBeenCalledWith('new-token');
92
+ nowSpy.mockRestore();
93
+ });
94
+
95
+ test('should not rotate long-lived query jwtToken', async () => {
96
+ const nowMs = 1700000000000;
97
+ const nowSeconds = Math.floor(nowMs / 1000);
98
+ const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
99
+ jwt.decodeJwt.mockReturnValue({
100
+ id: 'user-1',
101
+ platform: 'testCRM',
102
+ exp: nowSeconds + (120 * 365 * 24 * 60 * 60),
103
+ });
104
+ authCore.authValidation.mockResolvedValue({
105
+ successful: true,
106
+ returnMessage: { message: 'ok' },
107
+ failReason: null,
108
+ status: 200,
109
+ });
110
+ const app = buildApp();
111
+
112
+ const response = await request(app).get('/authValidation?jwtToken=query-token');
113
+
114
+ expect(response.status).toBe(200);
115
+ expect(response.headers['x-refreshed-jwt-token']).toBeUndefined();
116
+ expect(jwt.generateJwt).not.toHaveBeenCalled();
117
+ nowSpy.mockRestore();
118
+ });
119
+
57
120
  test('should refresh near-expiry bearer token and expose header', async () => {
58
121
  const nowMs = 1700000000000;
59
122
  const nowSeconds = Math.floor(nowMs / 1000);