@app-connect/core 0.0.3 → 1.6.4

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.
@@ -0,0 +1,9 @@
1
+ const LOG_DETAILS_FORMAT_TYPE = {
2
+ PLAIN_TEXT: 'text/plain',
3
+ HTML: 'text/html',
4
+ MARKDOWN: 'text/markdown'
5
+ };
6
+
7
+ module.exports = {
8
+ LOG_DETAILS_FORMAT_TYPE
9
+ };
package/lib/encode.js CHANGED
@@ -1,30 +1,30 @@
1
- const crypto = require('crypto');
2
-
3
- function getCipherKey() {
4
- if (!process.env.APP_SERVER_SECRET_KEY) {
5
- throw new Error('APP_SERVER_SECRET_KEY is not defined');
6
- }
7
- if (process.env.APP_SERVER_SECRET_KEY.length < 32) {
8
- // pad secret key with spaces if it is less than 32 bytes
9
- return process.env.APP_SERVER_SECRET_KEY.padEnd(32, ' ');
10
- }
11
- if (process.env.APP_SERVER_SECRET_KEY.length > 32) {
12
- // truncate secret key if it is more than 32 bytes
13
- return process.env.APP_SERVER_SECRET_KEY.slice(0, 32);
14
- }
15
- return process.env.APP_SERVER_SECRET_KEY;
16
- }
17
-
18
- function encode(data) {
19
- const cipher = crypto.createCipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0));
20
- return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
21
- }
22
-
23
- function decoded(encryptedData) {
24
- const decipher = crypto.createDecipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0));
25
- return decipher.update(encryptedData, 'hex', 'utf8') + decipher.final('utf8');
26
- }
27
-
28
-
29
- exports.encode = encode;
30
- exports.decoded = decoded;
1
+ const crypto = require('crypto');
2
+
3
+ function getCipherKey() {
4
+ if (!process.env.APP_SERVER_SECRET_KEY) {
5
+ throw new Error('APP_SERVER_SECRET_KEY is not defined');
6
+ }
7
+ if (process.env.APP_SERVER_SECRET_KEY.length < 32) {
8
+ // pad secret key with spaces if it is less than 32 bytes
9
+ return process.env.APP_SERVER_SECRET_KEY.padEnd(32, ' ');
10
+ }
11
+ if (process.env.APP_SERVER_SECRET_KEY.length > 32) {
12
+ // truncate secret key if it is more than 32 bytes
13
+ return process.env.APP_SERVER_SECRET_KEY.slice(0, 32);
14
+ }
15
+ return process.env.APP_SERVER_SECRET_KEY;
16
+ }
17
+
18
+ function encode(data) {
19
+ const cipher = crypto.createCipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0));
20
+ return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
21
+ }
22
+
23
+ function decoded(encryptedData) {
24
+ const decipher = crypto.createDecipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0));
25
+ return decipher.update(encryptedData, 'hex', 'utf8') + decipher.final('utf8');
26
+ }
27
+
28
+
29
+ exports.encode = encode;
30
+ exports.decoded = decoded;
@@ -1,42 +1,42 @@
1
- function rateLimitErrorMessage({ platform }) {
2
- return {
3
- message: `Rate limit exceeded`,
4
- messageType: 'warning',
5
- details: [
6
- {
7
- title: 'Details',
8
- items: [
9
- {
10
- id: '1',
11
- type: 'text',
12
- text: `You have exceeded the maximum number of requests allowed by ${platform}. Please try again in the next minute. If the problem persists please contact support.`
13
- }
14
- ]
15
- }
16
- ],
17
- ttl: 5000
18
- }
19
- }
20
-
21
- function authorizationErrorMessage({ platform }) {
22
- return {
23
- message: `Authorization error`,
24
- messageType: 'warning',
25
- details: [
26
- {
27
- title: 'Details',
28
- items: [
29
- {
30
- id: '1',
31
- type: 'text',
32
- text: `It seems like there's something wrong with your authorization of ${platform}. Please Logout and then Connect your ${platform} account within this extension.`
33
- }
34
- ]
35
- }
36
- ],
37
- ttl: 5000
38
- }
39
- }
40
-
41
- exports.rateLimitErrorMessage = rateLimitErrorMessage;
1
+ function rateLimitErrorMessage({ platform }) {
2
+ return {
3
+ message: `Rate limit exceeded`,
4
+ messageType: 'warning',
5
+ details: [
6
+ {
7
+ title: 'Details',
8
+ items: [
9
+ {
10
+ id: '1',
11
+ type: 'text',
12
+ text: `You have exceeded the maximum number of requests allowed by ${platform}. Please try again in the next minute. If the problem persists please contact support.`
13
+ }
14
+ ]
15
+ }
16
+ ],
17
+ ttl: 5000
18
+ }
19
+ }
20
+
21
+ function authorizationErrorMessage({ platform }) {
22
+ return {
23
+ message: `Authorization error`,
24
+ messageType: 'warning',
25
+ details: [
26
+ {
27
+ title: 'Details',
28
+ items: [
29
+ {
30
+ id: '1',
31
+ type: 'text',
32
+ text: `It seems like there's something wrong with your authorization of ${platform}. Please Logout and then Connect your ${platform} account within this extension.`
33
+ }
34
+ ]
35
+ }
36
+ ],
37
+ ttl: 5000
38
+ }
39
+ }
40
+
41
+ exports.rateLimitErrorMessage = rateLimitErrorMessage;
42
42
  exports.authorizationErrorMessage = authorizationErrorMessage;
package/lib/jwt.js CHANGED
@@ -1,16 +1,16 @@
1
- const { sign, verify } = require('jsonwebtoken');
2
-
3
- function generateJwt(data) {
4
- return sign(data, process.env.APP_SERVER_SECRET_KEY, { expiresIn: '120y' })
5
- }
6
-
7
- function decodeJwt(token) {
8
- try {
9
- return verify(token, process.env.APP_SERVER_SECRET_KEY);
10
- } catch (e) {
11
- return null;
12
- }
13
- }
14
-
15
- exports.generateJwt = generateJwt;
16
- exports.decodeJwt = decodeJwt;
1
+ const { sign, verify } = require('jsonwebtoken');
2
+
3
+ function generateJwt(data) {
4
+ return sign(data, process.env.APP_SERVER_SECRET_KEY, { expiresIn: '120y' })
5
+ }
6
+
7
+ function decodeJwt(token) {
8
+ try {
9
+ return verify(token, process.env.APP_SERVER_SECRET_KEY);
10
+ } catch (e) {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ exports.generateJwt = generateJwt;
16
+ exports.decodeJwt = decodeJwt;
package/lib/oauth.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
  const ClientOAuth2 = require('client-oauth2');
3
- const { Lock } = require('../models/dynamo/lockSchema');
3
+ const moment = require('moment');
4
4
  const { UserModel } = require('../models/userModel');
5
5
  const adapterRegistry = require('../adapter/registry');
6
+ const dynamoose = require('dynamoose');
6
7
 
7
8
  // oauthApp strategy is default to 'code' which use credentials to get accessCode, then exchange for accessToken and refreshToken.
8
9
  // To change to other strategies, please refer to: https://github.com/mulesoft-labs/js-client-oauth2
@@ -18,10 +19,10 @@ function getOAuthApp({ clientId, clientSecret, accessTokenUri, authorizationUri,
18
19
  }
19
20
 
20
21
 
21
- async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10) {
22
- const dateNow = new Date();
23
- const tokenExpiry = new Date(user.tokenExpiry);
24
- const expiryBuffer = 1000 * 60 * 2; // 2 minutes => 120000ms
22
+ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20) {
23
+ const now = moment();
24
+ const tokenExpiry = moment(user.tokenExpiry);
25
+ const expiryBuffer = 2; // 2 minutes
25
26
  // Special case: Bullhorn
26
27
  if (user.platform) {
27
28
  const platformModule = adapterRegistry.getAdapter(user.platform);
@@ -29,52 +30,101 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10)
29
30
  return platformModule.checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout);
30
31
  }
31
32
  }
32
- // Other CRMs
33
- if (user && user.accessToken && user.refreshToken && tokenExpiry.getTime() < (dateNow.getTime() + expiryBuffer)) {
33
+ // Other CRMs - check if token will expire within the buffer time
34
+ if (user && user.accessToken && user.refreshToken && tokenExpiry.isBefore(now.clone().add(expiryBuffer, 'minutes'))) {
34
35
  // case: use dynamoDB to manage token refresh lock
35
- if (process.env.USE_TOKEN_REFRESH_LOCK === 'true') {
36
- let lock = await Lock.get({ userId: user.id });
36
+ if (adapterRegistry.getManifest('default')?.platforms?.[user.platform]?.auth?.useTokenRefreshLock) {
37
37
  let newLock;
38
- if (!!lock?.ttl && lock.ttl < dateNow.getTime()) {
39
- await lock.delete();
40
- lock = null;
41
- }
42
- if (lock) {
43
- let processTime = 0;
44
- while (!!lock && processTime < tokenLockTimeout) {
45
- await new Promise(resolve => setTimeout(resolve, 2000)); // wait for 2 seconds
46
- processTime += 2;
47
- lock = await Lock.get({ userId: user.id });
48
- }
49
- // Timeout -> let users try another time
50
- if (processTime >= tokenLockTimeout) {
51
- throw new Error('Token lock timeout');
38
+ const { Lock } = require('../models/dynamo/lockSchema');
39
+ // Try to atomically create lock only if it doesn't exist
40
+ try {
41
+ newLock = await Lock.create(
42
+ {
43
+ userId: user.id,
44
+ ttl: now.unix() + 30
45
+ },
46
+ {
47
+ overwrite: false
48
+ }
49
+ );
50
+ console.log('lock created')
51
+ } catch (e) {
52
+ // If creation failed due to condition, a lock exists
53
+ if (e.name === 'ConditionalCheckFailedException' || e.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
54
+ let lock = await Lock.get({ userId: user.id });
55
+ if (!!lock?.ttl && moment(lock.ttl).unix() < now.unix()) {
56
+ // Try to delete expired lock and create a new one atomically
57
+ try {
58
+ console.log('lock expired.')
59
+ await lock.delete();
60
+ newLock = await Lock.create(
61
+ {
62
+ userId: user.id,
63
+ ttl: now.unix() + 30
64
+ },
65
+ {
66
+ overwrite: false
67
+ }
68
+ );
69
+ } catch (e2) {
70
+ if (e2.name === 'ConditionalCheckFailedException' || e2.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
71
+ // Another process created a lock between our delete and create
72
+ lock = await Lock.get({ userId: user.id });
73
+ } else {
74
+ throw e2;
75
+ }
76
+ }
77
+ }
78
+
79
+ if (lock && !newLock) {
80
+ let processTime = 0;
81
+ let delay = 500; // Start with 500ms
82
+ const maxDelay = 8000; // Cap at 8 seconds
83
+ while (!!lock && processTime < tokenLockTimeout) {
84
+ await new Promise(resolve => setTimeout(resolve, delay));
85
+ processTime += delay / 1000; // Convert to seconds for comparison
86
+ delay = Math.min(delay * 2, maxDelay); // Exponential backoff with cap
87
+ lock = await Lock.get({ userId: user.id });
88
+ }
89
+ // Timeout -> let users try another time
90
+ if (processTime >= tokenLockTimeout) {
91
+ throw new Error('Token lock timeout');
92
+ }
93
+ user = await UserModel.findByPk(user.id);
94
+ console.log('locked. bypass')
95
+ return user;
96
+ }
97
+ } else {
98
+ throw e;
52
99
  }
53
- user = await UserModel.findByPk(user.id);
54
- }
55
- else {
56
- newLock = await Lock.create({
57
- userId: user.id
58
- });
59
100
  }
101
+ const startRefreshTime = moment();
60
102
  const token = oauthApp.createToken(user.accessToken, user.refreshToken);
103
+ console.log('token refreshing...')
61
104
  const { accessToken, refreshToken, expires } = await token.refresh();
62
105
  user.accessToken = accessToken;
63
106
  user.refreshToken = refreshToken;
64
107
  user.tokenExpiry = expires;
65
108
  await user.save();
66
109
  if (newLock) {
110
+ const deletionStartTime = moment();
67
111
  await newLock.delete();
112
+ const deletionEndTime = moment();
113
+ console.log(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
68
114
  }
115
+ const endRefreshTime = moment();
116
+ console.log(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
69
117
  }
70
118
  // case: run withou token refresh lock
71
119
  else {
120
+ console.log('token refreshing...')
72
121
  const token = oauthApp.createToken(user.accessToken, user.refreshToken);
73
122
  const { accessToken, refreshToken, expires } = await token.refresh();
74
123
  user.accessToken = accessToken;
75
124
  user.refreshToken = refreshToken;
76
125
  user.tokenExpiry = expires;
77
126
  await user.save();
127
+ console.log('token refreshing finished')
78
128
  }
79
129
 
80
130
  }
package/lib/util.js CHANGED
@@ -1,40 +1,43 @@
1
-
2
- const tzlookup = require('tz-lookup');
3
- const { State } = require('country-state-city');
4
- const crypto = require('crypto');
5
-
6
- function getTimeZone(countryCode, stateCode) {
7
- const state = State.getStateByCodeAndCountry(stateCode, countryCode);
8
- if (!state) {
9
- return 'Unknown timezone';
10
- }
11
- const timezone = tzlookup(state.latitude, state.longitude);
12
- return timezone;
13
- }
14
-
15
-
16
- function getHashValue(string, secretKey) {
17
- return crypto.createHash('sha256').update(
18
- `${string}:${secretKey}`
19
- ).digest('hex');
20
- }
21
-
22
- function secondsToHoursMinutesSeconds(seconds) {
23
- // If not a number, return the input directly
24
- if (isNaN(seconds)) {
25
- return seconds;
26
- }
27
- const hours = Math.floor(seconds / 3600);
28
- const hoursString = hours > 0 ? `${hours} ${hours > 1 ? 'hours' : 'hour'}` : '';
29
- const minutes = Math.floor((seconds % 3600) / 60);
30
- const minutesString = minutes > 0 ? `${minutes} ${minutes > 1 ? 'minutes' : 'minute'}` : '';
31
- const remainingSeconds = seconds % 60;
32
- const secondsString = remainingSeconds > 0 ? `${remainingSeconds} ${remainingSeconds > 1 ? 'seconds' : 'second'}` : '';
33
- const resultString = [hoursString, minutesString, secondsString].filter(Boolean).join(', ');
34
- return resultString;
35
- }
36
-
37
- exports.getTimeZone = getTimeZone;
38
- exports.getHashValue = getHashValue;
39
- exports.secondsToHoursMinutesSeconds = secondsToHoursMinutesSeconds;
40
-
1
+
2
+ const tzlookup = require('tz-lookup');
3
+ const { State } = require('country-state-city');
4
+ const crypto = require('crypto');
5
+
6
+ function getTimeZone(countryCode, stateCode) {
7
+ const state = State.getStateByCodeAndCountry(stateCode, countryCode);
8
+ if (!state) {
9
+ return 'Unknown timezone';
10
+ }
11
+ const timezone = tzlookup(state.latitude, state.longitude);
12
+ return timezone;
13
+ }
14
+
15
+
16
+ function getHashValue(string, secretKey) {
17
+ return crypto.createHash('sha256').update(
18
+ `${string}:${secretKey}`
19
+ ).digest('hex');
20
+ }
21
+
22
+ function secondsToHoursMinutesSeconds(seconds) {
23
+ // If not a number, return the input directly
24
+ if (isNaN(seconds)) {
25
+ return seconds;
26
+ }
27
+ const hours = Math.floor(seconds / 3600);
28
+ const hoursString = hours > 0 ? `${hours} ${hours > 1 ? 'hours' : 'hour'}` : '';
29
+ const minutes = Math.floor((seconds % 3600) / 60);
30
+ const minutesString = minutes > 0 ? `${minutes} ${minutes > 1 ? 'minutes' : 'minute'}` : '';
31
+ const remainingSeconds = seconds % 60;
32
+ const secondsString = remainingSeconds > 0 ? `${remainingSeconds} ${remainingSeconds > 1 ? 'seconds' : 'second'}` : '';
33
+ if (!hoursString && !minutesString && !secondsString) {
34
+ return '0 seconds';
35
+ }
36
+ const resultString = [hoursString, minutesString, secondsString].filter(Boolean).join(', ');
37
+ return resultString;
38
+ }
39
+
40
+ exports.getTimeZone = getTimeZone;
41
+ exports.getHashValue = getHashValue;
42
+ exports.secondsToHoursMinutesSeconds = secondsToHoursMinutesSeconds;
43
+
@@ -1,17 +1,17 @@
1
- const Sequelize = require('sequelize');
2
- const { sequelize } = require('./sequelize');
3
-
4
- // Model for User data
5
- exports.AdminConfigModel = sequelize.define('adminConfigs', {
6
- // hashed rc account ID
7
- id: {
8
- type: Sequelize.STRING,
9
- primaryKey: true,
10
- },
11
- userSettings: {
12
- type: Sequelize.JSON
13
- },
14
- customAdapter: {
15
- type: Sequelize.JSON
16
- }
17
- });
1
+ const Sequelize = require('sequelize');
2
+ const { sequelize } = require('./sequelize');
3
+
4
+ // Model for User data
5
+ exports.AdminConfigModel = sequelize.define('adminConfigs', {
6
+ // hashed rc account ID
7
+ id: {
8
+ type: Sequelize.STRING,
9
+ primaryKey: true,
10
+ },
11
+ userSettings: {
12
+ type: Sequelize.JSON
13
+ },
14
+ customAdapter: {
15
+ type: Sequelize.JSON
16
+ }
17
+ });
@@ -1,23 +1,23 @@
1
- const Sequelize = require('sequelize');
2
- const { sequelize } = require('./sequelize');
3
-
4
- // Model for cache data
5
- exports.CacheModel = sequelize.define('cache', {
6
- // id = {userId}-{cacheKey}
7
- id: {
8
- type: Sequelize.STRING,
9
- primaryKey: true,
10
- },
11
- status: {
12
- type: Sequelize.STRING,
13
- },
14
- userId: {
15
- type: Sequelize.STRING,
16
- },
17
- cacheKey: {
18
- type: Sequelize.STRING,
19
- },
20
- expiry: {
21
- type: Sequelize.DATE
22
- }
23
- });
1
+ const Sequelize = require('sequelize');
2
+ const { sequelize } = require('./sequelize');
3
+
4
+ // Model for cache data
5
+ exports.CacheModel = sequelize.define('cache', {
6
+ // id = {userId}-{cacheKey}
7
+ id: {
8
+ type: Sequelize.STRING,
9
+ primaryKey: true,
10
+ },
11
+ status: {
12
+ type: Sequelize.STRING,
13
+ },
14
+ userId: {
15
+ type: Sequelize.STRING,
16
+ },
17
+ cacheKey: {
18
+ type: Sequelize.STRING,
19
+ },
20
+ expiry: {
21
+ type: Sequelize.DATE
22
+ }
23
+ });
@@ -1,27 +1,27 @@
1
- const Sequelize = require('sequelize');
2
- const { sequelize } = require('./sequelize');
3
-
4
- // Model for User data
5
- exports.CallLogModel = sequelize.define('callLogs', {
6
- // callId
7
- id: {
8
- type: Sequelize.STRING,
9
- primaryKey: true,
10
- },
11
- sessionId: {
12
- type: Sequelize.STRING,
13
- primaryKey: true,
14
- },
15
- platform: {
16
- type: Sequelize.STRING,
17
- },
18
- thirdPartyLogId: {
19
- type: Sequelize.STRING,
20
- },
21
- userId: {
22
- type: Sequelize.STRING,
23
- },
24
- contactId: {
25
- type: Sequelize.STRING,
26
- }
27
- });
1
+ const Sequelize = require('sequelize');
2
+ const { sequelize } = require('./sequelize');
3
+
4
+ // Model for User data
5
+ exports.CallLogModel = sequelize.define('callLogs', {
6
+ // callId
7
+ id: {
8
+ type: Sequelize.STRING,
9
+ primaryKey: true,
10
+ },
11
+ sessionId: {
12
+ type: Sequelize.STRING,
13
+ primaryKey: true,
14
+ },
15
+ platform: {
16
+ type: Sequelize.STRING,
17
+ },
18
+ thirdPartyLogId: {
19
+ type: Sequelize.STRING,
20
+ },
21
+ userId: {
22
+ type: Sequelize.STRING,
23
+ },
24
+ contactId: {
25
+ type: Sequelize.STRING,
26
+ }
27
+ });
@@ -1,25 +1,25 @@
1
- const dynamoose = require('dynamoose');
2
-
3
- const lockSchema = new dynamoose.Schema({
4
- userId: {
5
- type: String,
6
- hashKey: true
7
- },
8
- ttl: {
9
- type: Number
10
- }
11
- });
12
-
13
- const tableOptions = {
14
- prefix: process.env.DYNAMODB_TABLE_PREFIX,
15
- expires: 60 // 60 seconds
16
- };
17
-
18
- if (process.env.NODE_ENV === 'production') {
19
- tableOptions.create = false;
20
- tableOptions.waitForActive = false;
21
- }
22
-
23
- const Lock = dynamoose.model('-token-refresh-lock', lockSchema, tableOptions);
24
-
1
+ const dynamoose = require('dynamoose');
2
+
3
+ const lockSchema = new dynamoose.Schema({
4
+ userId: {
5
+ type: String,
6
+ hashKey: true
7
+ },
8
+ ttl: {
9
+ type: Number
10
+ }
11
+ });
12
+
13
+ const tableOptions = {
14
+ prefix: process.env.DYNAMODB_TABLE_PREFIX,
15
+ expires: 60 // 60 seconds
16
+ };
17
+
18
+ if (process.env.NODE_ENV === 'production') {
19
+ tableOptions.create = false;
20
+ tableOptions.waitForActive = false;
21
+ }
22
+
23
+ const Lock = dynamoose.model('-token-refresh-lock', lockSchema, tableOptions);
24
+
25
25
  exports.Lock = Lock;
@@ -0,0 +1,30 @@
1
+ const dynamoose = require('dynamoose');
2
+
3
+ const noteCacheSchema = new dynamoose.Schema({
4
+ sessionId: {
5
+ type: String,
6
+ hashKey: true,
7
+ },
8
+ note: {
9
+ type: String,
10
+ required: true,
11
+ },
12
+ ttl: {
13
+ type: Number,
14
+ required: true,
15
+ },
16
+ });
17
+
18
+ const tableOptions = {
19
+ prefix: process.env.DYNAMODB_TABLE_PREFIX,
20
+ expires: 60 // 60 seconds
21
+ };
22
+
23
+ if (process.env.NODE_ENV === 'production') {
24
+ tableOptions.create = false;
25
+ tableOptions.waitForActive = false;
26
+ }
27
+
28
+ const NoteCache = dynamoose.model('-note-cache', noteCacheSchema, tableOptions);
29
+
30
+ exports.NoteCache = NoteCache;