@app-connect/core 1.7.4 → 1.7.8

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,159 @@
1
+ class DebugTracer {
2
+ /**
3
+ * Creates a new DebugTracer instance
4
+ * @param {Object} headers - Request headers object
5
+ */
6
+ constructor(headers = {}) {
7
+ this.traces = [];
8
+ this.startTime = Date.now();
9
+ this.requestId = this._generateRequestId();
10
+ }
11
+
12
+ /**
13
+ * Generates a unique request ID for tracking
14
+ * @returns {string} Unique request ID
15
+ */
16
+ _generateRequestId() {
17
+ return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
18
+ }
19
+
20
+ /**
21
+ * Captures the current stack trace
22
+ * @param {number} skipFrames - Number of frames to skip from the top (default: 2)
23
+ * @returns {string[]} Array of stack trace lines
24
+ */
25
+ _captureStackTrace(skipFrames = 2) {
26
+ const err = new Error();
27
+ const stack = err.stack || '';
28
+ const lines = stack.split('\n').slice(skipFrames);
29
+ return lines.map(line => line.trim()).filter(line => line.startsWith('at '));
30
+ }
31
+
32
+ /**
33
+ * Records a trace entry with method name, data, and stack trace
34
+ * @param {string} methodName - Name of the method/function being traced
35
+ * @param {Object} data - Data to record (will be sanitized to remove sensitive info)
36
+ * @param {Object} options - Additional options
37
+ * @param {boolean} options.includeStack - Whether to include stack trace (default: true)
38
+ * @param {string} options.level - Log level: 'info', 'warn', 'error' (default: 'info')
39
+ * @returns {DebugTracer} Returns this for chaining
40
+ */
41
+ trace(methodName, data = {}, options = {}) {
42
+ const { includeStack = true, level = 'info' } = options;
43
+
44
+ const traceEntry = {
45
+ timestamp: new Date().toISOString(),
46
+ elapsed: Date.now() - this.startTime,
47
+ methodName,
48
+ level,
49
+ data: this._sanitizeData(data)
50
+ };
51
+
52
+ if (includeStack) {
53
+ traceEntry.stackTrace = this._captureStackTrace(3);
54
+ }
55
+
56
+ this.traces.push(traceEntry);
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Records an error trace with full stack information
62
+ * @param {string} methodName - Name of the method where error occurred
63
+ * @param {Error|string} error - Error object or message
64
+ * @param {Object} additionalData - Additional context data
65
+ * @returns {DebugTracer} Returns this for chaining
66
+ */
67
+ traceError(methodName, error, additionalData = {}) {
68
+ const errorData = {
69
+ message: error instanceof Error ? error.message : String(error),
70
+ errorStack: error instanceof Error ? error.stack : null,
71
+ ...additionalData
72
+ };
73
+
74
+ return this.trace(methodName, errorData, { level: 'error' });
75
+ }
76
+
77
+ /**
78
+ * Sanitizes data by removing sensitive fields
79
+ * @param {Object} data - Data to sanitize
80
+ * @returns {Object} Sanitized data
81
+ */
82
+ _sanitizeData(data) {
83
+ if (!data || typeof data !== 'object') {
84
+ return data;
85
+ }
86
+
87
+ const sensitiveFields = [
88
+ 'accessToken', 'refreshToken', 'apiKey', 'password',
89
+ 'secret', 'token', 'authorization', 'auth', 'key',
90
+ 'credential', 'credentials', 'privateKey', 'clientSecret'
91
+ ];
92
+
93
+ const sanitized = Array.isArray(data) ? [...data] : { ...data };
94
+
95
+ const sanitizeRecursive = (obj) => {
96
+ if (!obj || typeof obj !== 'object') {
97
+ return obj;
98
+ }
99
+
100
+ for (const key of Object.keys(obj)) {
101
+ const lowerKey = key.toLowerCase();
102
+ if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
103
+ // eslint-disable-next-line no-param-reassign
104
+ obj[key] = '[REDACTED]';
105
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
106
+ // eslint-disable-next-line no-param-reassign
107
+ obj[key] = sanitizeRecursive(
108
+ Array.isArray(obj[key]) ? [...obj[key]] : { ...obj[key] }
109
+ );
110
+ }
111
+ }
112
+ return obj;
113
+ };
114
+
115
+ return sanitizeRecursive(sanitized);
116
+ }
117
+
118
+ /**
119
+ * Gets the complete trace data for inclusion in response
120
+ * @returns {Object} Trace data object
121
+ */
122
+ getTraceData() {
123
+ return {
124
+ requestId: this.requestId,
125
+ totalDuration: `${Date.now() - this.startTime}ms`,
126
+ traceCount: this.traces.length,
127
+ traces: this.traces
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Gets trace data and merges it into a response object
133
+ * @param {Object} response - Original response object
134
+ * @returns {Object} Response with debug trace data appended (if debug mode enabled)
135
+ */
136
+ wrapResponse(response) {
137
+ const traceData = this.getTraceData();
138
+ if (!traceData) {
139
+ return response;
140
+ }
141
+
142
+ return {
143
+ ...response,
144
+ _debug: traceData
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Static helper to create tracer from Express request
150
+ * @param {Object} req - Express request object
151
+ * @returns {DebugTracer} New tracer instance
152
+ */
153
+ static fromRequest(req) {
154
+ return new DebugTracer(req.headers || {});
155
+ }
156
+ }
157
+
158
+ module.exports = { DebugTracer };
159
+
package/lib/oauth.js CHANGED
@@ -32,7 +32,7 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
32
32
  // Other CRMs - check if token will expire within the buffer time
33
33
  if (user && user.accessToken && user.refreshToken && tokenExpiry.isBefore(now.clone().add(expiryBuffer, 'minutes'))) {
34
34
  // case: use dynamoDB to manage token refresh lock
35
- if (process.env.USE_TOKEN_REFRESH_LOCK_PLATFORMS.split(',').includes(user.platform)) {
35
+ if (process.env.USE_TOKEN_REFRESH_LOCK_PLATFORMS?.split(',')?.includes(user.platform)) {
36
36
  let newLock;
37
37
  const { Lock } = require('../models/dynamo/lockSchema');
38
38
  // Try to atomically create lock only if it doesn't exist
@@ -40,7 +40,7 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
40
40
  newLock = await Lock.create(
41
41
  {
42
42
  userId: user.id,
43
- ttl: now.unix() + 30
43
+ ttl: now.unix() + tokenLockTimeout
44
44
  },
45
45
  {
46
46
  overwrite: false
@@ -59,7 +59,7 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
59
59
  newLock = await Lock.create(
60
60
  {
61
61
  userId: user.id,
62
- ttl: now.unix() + 30
62
+ ttl: now.unix() + tokenLockTimeout
63
63
  },
64
64
  {
65
65
  overwrite: false
@@ -97,22 +97,32 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20)
97
97
  throw e;
98
98
  }
99
99
  }
100
- const startRefreshTime = moment();
101
- const token = oauthApp.createToken(user.accessToken, user.refreshToken);
102
- console.log('token refreshing...')
103
- const { accessToken, refreshToken, expires } = await token.refresh();
104
- user.accessToken = accessToken;
105
- user.refreshToken = refreshToken;
106
- user.tokenExpiry = expires;
107
- await user.save();
108
- if (newLock) {
109
- const deletionStartTime = moment();
110
- await newLock.delete();
111
- const deletionEndTime = moment();
112
- console.log(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
100
+ try {
101
+ const startRefreshTime = moment();
102
+ const token = oauthApp.createToken(user.accessToken, user.refreshToken);
103
+ console.log('token refreshing...')
104
+ const { accessToken, refreshToken, expires } = await token.refresh();
105
+ user.accessToken = accessToken;
106
+ user.refreshToken = refreshToken;
107
+ user.tokenExpiry = expires;
108
+ await user.save();
109
+ if (newLock) {
110
+ const deletionStartTime = moment();
111
+ await newLock.delete();
112
+ const deletionEndTime = moment();
113
+ console.log(`lock deleted in ${deletionEndTime.diff(deletionStartTime)}ms`)
114
+ }
115
+ const endRefreshTime = moment();
116
+ console.log(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
117
+ }
118
+ catch (e) {
119
+ console.log('token refreshing failed', e.stack)
120
+ }
121
+ finally {
122
+ if (newLock) {
123
+ await newLock.delete();
124
+ }
113
125
  }
114
- const endRefreshTime = moment();
115
- console.log(`token refreshing finished in ${endRefreshTime.diff(startRefreshTime)}ms`)
116
126
  }
117
127
  // case: run withou token refresh lock
118
128
  else {
@@ -189,10 +189,10 @@ class RingCentral {
189
189
  return response.json();
190
190
  }
191
191
 
192
- async getCallsAggregationData({ token, timezone, timeFrom, timeTo }) {
192
+ async getCallsAggregationData({ token, timezone, timeFrom, timeTo, groupBy }) {
193
193
  const body = {
194
194
  grouping: {
195
- groupBy: "Company"
195
+ groupBy
196
196
  },
197
197
  timeSettings: {
198
198
  timeZone: timezone,
@@ -0,0 +1,66 @@
1
+ // packages/core/lib/s3ErrorReport.js
2
+ const { S3Client, PutObjectCommand, GetObjectCommand, CreateBucketCommand, HeadBucketCommand } = require('@aws-sdk/client-s3');
3
+ const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
4
+ const shortid = require('shortid');
5
+
6
+ const BUCKET_NAME = process.env.ERROR_REPORT_S3_BUCKET || 'error-reports-bucket';
7
+ const PRESIGN_EXPIRY = 300; // 5 minutes
8
+ const IS_LOCAL = !process.env.ERROR_REPORT_S3_BUCKET;
9
+ const LOCALSTACK_ENDPOINT = process.env.LOCALSTACK_ENDPOINT || 'http://localhost:9001';
10
+
11
+ const s3Client = new S3Client({
12
+ region: process.env.AWS_REGION || 'us-east-1',
13
+ ...(IS_LOCAL && {
14
+ endpoint: LOCALSTACK_ENDPOINT,
15
+ forcePathStyle: true, // Required for LocalStack/MinIO
16
+ credentials: {
17
+ accessKeyId: 'minioadmin',
18
+ secretAccessKey: 'minioadmin'
19
+ }
20
+ })
21
+ });
22
+
23
+ /**
24
+ * Ensure bucket exists (useful for local testing)
25
+ */
26
+ async function ensureBucketExists() {
27
+ if (!IS_LOCAL) return;
28
+
29
+ try {
30
+ await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME }));
31
+ } catch (err) {
32
+ if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
33
+ await s3Client.send(new CreateBucketCommand({ Bucket: BUCKET_NAME }));
34
+ console.log(`[LocalStack] Created bucket: ${BUCKET_NAME}`);
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Generate presigned URL for upload
41
+ */
42
+ async function getUploadUrl({ userId, platform, metadata }) {
43
+ await ensureBucketExists();
44
+
45
+ const reportId = shortid.generate();
46
+ const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
47
+ const key = `error-reports/${timestamp}/${userId}-${reportId}.json`;
48
+
49
+ const command = new PutObjectCommand({
50
+ Bucket: BUCKET_NAME,
51
+ Key: key,
52
+ ContentType: 'application/json',
53
+ Metadata: {
54
+ 'user-id': userId,
55
+ 'platform': platform,
56
+ ...metadata
57
+ }
58
+ });
59
+
60
+ const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: PRESIGN_EXPIRY });
61
+
62
+ return uploadUrl;
63
+ }
64
+
65
+ exports.getUploadUrl = getUploadUrl;
66
+ exports.ensureBucketExists = ensureBucketExists;
package/package.json CHANGED
@@ -1,67 +1,69 @@
1
- {
2
- "name": "@app-connect/core",
3
- "version": "1.7.4",
4
- "description": "RingCentral App Connect Core",
5
- "main": "index.js",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/ringcentral/rc-unified-crm-extension.git"
9
- },
10
- "keywords": [
11
- "RingCentral",
12
- "App Connect"
13
- ],
14
- "author": "RingCentral Labs",
15
- "license": "MIT",
16
- "peerDependencies": {
17
- "axios": "^1.12.2",
18
- "express": "^4.21.2",
19
- "pg": "^8.8.0",
20
- "sequelize": "^6.29.0",
21
- "moment": "^2.29.4",
22
- "moment-timezone": "^0.5.39"
23
- },
24
- "dependencies": {
25
- "@aws-sdk/client-dynamodb": "^3.751.0",
26
- "body-parser": "^1.20.3",
27
- "client-oauth2": "^4.3.3",
28
- "cors": "^2.8.5",
29
- "country-state-city": "^3.2.1",
30
- "dotenv": "^16.0.3",
31
- "dynamoose": "^4.0.3",
32
- "jsonwebtoken": "^9.0.0",
33
- "mixpanel": "^0.18.0",
34
- "shortid": "^2.2.17",
35
- "tz-lookup": "^6.1.25",
36
- "ua-parser-js": "^1.0.38"
37
- },
38
- "scripts": {
39
- "test": "jest",
40
- "test:watch": "jest --watch",
41
- "test:coverage": "jest --coverage",
42
- "test:ci": "jest --ci --coverage --watchAll=false"
43
- },
44
- "devDependencies": {
45
- "@eslint/js": "^9.22.0",
46
- "@octokit/rest": "^19.0.5",
47
- "axios": "^1.12.2",
48
- "express": "^4.21.2",
49
- "eslint": "^9.22.0",
50
- "globals": "^16.0.0",
51
- "jest": "^29.3.1",
52
- "moment": "^2.29.4",
53
- "moment-timezone": "^0.5.39",
54
- "nock": "^13.2.9",
55
- "pg": "^8.8.0",
56
- "sequelize": "^6.29.0",
57
- "sqlite3": "^5.1.2",
58
- "supertest": "^6.3.1"
59
- },
60
- "overrides": {
61
- "js-object-utilities": "2.2.1"
62
- },
63
- "bugs": {
64
- "url": "https://github.com/ringcentral/rc-unified-crm-extension/issues"
65
- },
66
- "homepage": "https://github.com/ringcentral/rc-unified-crm-extension#readme"
67
- }
1
+ {
2
+ "name": "@app-connect/core",
3
+ "version": "1.7.8",
4
+ "description": "RingCentral App Connect Core",
5
+ "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ringcentral/rc-unified-crm-extension.git"
9
+ },
10
+ "keywords": [
11
+ "RingCentral",
12
+ "App Connect"
13
+ ],
14
+ "author": "RingCentral Labs",
15
+ "license": "MIT",
16
+ "peerDependencies": {
17
+ "axios": "^1.12.2",
18
+ "express": "^4.21.2",
19
+ "moment": "^2.29.4",
20
+ "moment-timezone": "^0.5.39",
21
+ "pg": "^8.8.0",
22
+ "sequelize": "^6.29.0"
23
+ },
24
+ "dependencies": {
25
+ "@aws-sdk/client-dynamodb": "^3.751.0",
26
+ "@aws-sdk/client-s3": "^3.947.0",
27
+ "@aws-sdk/s3-request-presigner": "^3.947.0",
28
+ "body-parser": "^1.20.3",
29
+ "client-oauth2": "^4.3.3",
30
+ "cors": "^2.8.5",
31
+ "country-state-city": "^3.2.1",
32
+ "dotenv": "^16.0.3",
33
+ "dynamoose": "^4.0.3",
34
+ "jsonwebtoken": "^9.0.0",
35
+ "mixpanel": "^0.18.0",
36
+ "shortid": "^2.2.17",
37
+ "tz-lookup": "^6.1.25",
38
+ "ua-parser-js": "^1.0.38"
39
+ },
40
+ "scripts": {
41
+ "test": "jest",
42
+ "test:watch": "jest --watch",
43
+ "test:coverage": "jest --coverage",
44
+ "test:ci": "jest --ci --coverage --watchAll=false"
45
+ },
46
+ "devDependencies": {
47
+ "@eslint/js": "^9.22.0",
48
+ "@octokit/rest": "^19.0.5",
49
+ "axios": "^1.12.2",
50
+ "eslint": "^9.22.0",
51
+ "express": "^4.21.2",
52
+ "globals": "^16.0.0",
53
+ "jest": "^29.3.1",
54
+ "moment": "^2.29.4",
55
+ "moment-timezone": "^0.5.39",
56
+ "nock": "^13.2.9",
57
+ "pg": "^8.8.0",
58
+ "sequelize": "^6.29.0",
59
+ "sqlite3": "^5.1.2",
60
+ "supertest": "^6.3.1"
61
+ },
62
+ "overrides": {
63
+ "js-object-utilities": "2.2.1"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/ringcentral/rc-unified-crm-extension/issues"
67
+ },
68
+ "homepage": "https://github.com/ringcentral/rc-unified-crm-extension#readme"
69
+ }
package/releaseNotes.json CHANGED
@@ -1,4 +1,60 @@
1
1
  {
2
+ "1.7.8": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "Error report feature on support page"
7
+ },
8
+ {
9
+ "type": "Better",
10
+ "description": "Calldown list supports better edit feature"
11
+ },
12
+ {
13
+ "type": "Better",
14
+ "description": "Connctor selection list can be opened back again"
15
+ },
16
+ {
17
+ "type": "Fix",
18
+ "description": "Call log record is shown with a wrong number after the call"
19
+ }
20
+ ]
21
+ },
22
+ "1.7.7": {
23
+ "global": [
24
+ {
25
+ "type": "New",
26
+ "description": "Admin report company stats now support grouping"
27
+ },
28
+ {
29
+ "type": "New",
30
+ "description": "Users can open back the platform selection page from user setting page"
31
+ }
32
+ ]
33
+ },
34
+ "1.7.6": {
35
+ "global": [
36
+ {
37
+ "type": "Fix",
38
+ "description": "Duplicated contact pop when complete warm transfer"
39
+ },
40
+ {
41
+ "type": "Fix",
42
+ "description": "Multi-region auth"
43
+ }
44
+ ]
45
+ },
46
+ "1.7.5": {
47
+ "global": [
48
+ {
49
+ "type": "Fix",
50
+ "description": "A not found issue for call note cache"
51
+ },
52
+ {
53
+ "type": "Better",
54
+ "description": "Embedded URL list now defaults to all domain. (Meaning quick access button and click-to-dial widget will be rendered on all web pages by default)"
55
+ }
56
+ ]
57
+ },
2
58
  "1.7.4": {
3
59
  "global": [
4
60
  {