@app-connect/core 0.0.2 → 1.5.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.
- package/.env.test +5 -0
- package/README.md +175 -8
- package/adapter/registry.js +137 -5
- package/handlers/log.js +4 -3
- package/index.js +5 -3
- package/jest.config.js +57 -0
- package/lib/callLogComposer.js +98 -64
- package/lib/constants.js +9 -0
- package/lib/oauth.js +60 -23
- package/package.json +7 -1
- package/releaseNotes.json +28 -0
- package/test/adapter/registry.test.js +271 -0
- package/test/handlers/auth.test.js +231 -0
- package/test/lib/jwt.test.js +161 -0
- package/test/setup.js +176 -0
package/lib/callLogComposer.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
2
|
const { secondsToHoursMinutesSeconds } = require('./util');
|
|
3
3
|
const adapterRegistry = require('../adapter/registry');
|
|
4
|
+
const { LOG_DETAILS_FORMAT_TYPE } = require('./constants');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Centralized call log composition module
|
|
7
8
|
* Supports both plain text and HTML formats used across different CRM adapters
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
// Format types
|
|
11
|
-
const FORMAT_TYPES = {
|
|
12
|
-
PLAIN_TEXT: 'plainText',
|
|
13
|
-
HTML: 'html'
|
|
14
|
-
};
|
|
15
|
-
|
|
16
11
|
/**
|
|
17
12
|
* Compose call log details based on user settings and format type
|
|
18
13
|
* @param {Object} params - Composition parameters
|
|
@@ -33,7 +28,7 @@ const FORMAT_TYPES = {
|
|
|
33
28
|
*/
|
|
34
29
|
async function composeCallLog(params) {
|
|
35
30
|
const {
|
|
36
|
-
logFormat =
|
|
31
|
+
logFormat = LOG_DETAILS_FORMAT_TYPE.PLAIN_TEXT,
|
|
37
32
|
existingBody = '',
|
|
38
33
|
callLog,
|
|
39
34
|
contactInfo,
|
|
@@ -117,67 +112,85 @@ async function composeCallLog(params) {
|
|
|
117
112
|
|
|
118
113
|
function upsertCallAgentNote({ body, note, logFormat }) {
|
|
119
114
|
if (!note) return body;
|
|
120
|
-
|
|
115
|
+
|
|
116
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
121
117
|
// HTML logFormat with proper Agent notes section handling
|
|
122
118
|
const noteRegex = RegExp('<b>Agent notes</b>([\\s\\S]+?)Call details</b>');
|
|
123
119
|
if (noteRegex.test(body)) {
|
|
124
120
|
return body.replace(noteRegex, `<b>Agent notes</b><br>${note}<br><br><b>Call details</b>`);
|
|
125
121
|
}
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
return `<b>Agent notes</b><br>${note}<br><br><b>Call details</b><br>` + body;
|
|
123
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
124
|
+
// Markdown logFormat with proper Agent notes section handling
|
|
125
|
+
const noteRegex = /## Agent notes\n([\s\S]*?)\n## Call details/;
|
|
126
|
+
if (noteRegex.test(body)) {
|
|
127
|
+
return body.replace(noteRegex, `## Agent notes\n${note}\n\n## Call details`);
|
|
128
|
+
}
|
|
129
|
+
if (body.startsWith('## Call details')) {
|
|
130
|
+
return `## Agent notes\n${note}\n\n` + body;
|
|
128
131
|
}
|
|
132
|
+
return `## Agent notes\n${note}\n\n## Call details\n` + body;
|
|
129
133
|
} else {
|
|
130
134
|
// Plain text logFormat - FIXED REGEX for multi-line notes with blank lines
|
|
131
135
|
const noteRegex = /- (?:Note|Agent notes): ([\s\S]*?)(?=\n- [A-Z][a-zA-Z\s/]*:|\n$|$)/;
|
|
132
136
|
if (noteRegex.test(body)) {
|
|
133
137
|
return body.replace(noteRegex, `- Note: ${note}`);
|
|
134
|
-
} else {
|
|
135
|
-
return `- Note: ${note}\n` + body;
|
|
136
138
|
}
|
|
139
|
+
return `- Note: ${note}\n` + body;
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
function upsertCallSessionId({ body, id, logFormat }) {
|
|
141
144
|
if (!id) return body;
|
|
142
145
|
|
|
143
|
-
if (logFormat ===
|
|
146
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
144
147
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
145
148
|
const idRegex = /(?:<li>)?<b>Session Id<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
146
149
|
if (idRegex.test(body)) {
|
|
147
150
|
return body.replace(idRegex, `<li><b>Session Id</b>: ${id}</li>`);
|
|
148
|
-
} else {
|
|
149
|
-
return body + `<li><b>Session Id</b>: ${id}</li>`;
|
|
150
151
|
}
|
|
152
|
+
return body + `<li><b>Session Id</b>: ${id}</li>`;
|
|
153
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
154
|
+
// Markdown format: **Session Id**: value
|
|
155
|
+
const sessionIdRegex = /\*\*Session Id\*\*: [^\n]*\n*/;
|
|
156
|
+
if (sessionIdRegex.test(body)) {
|
|
157
|
+
return body.replace(sessionIdRegex, `**Session Id**: ${id}\n`);
|
|
158
|
+
}
|
|
159
|
+
return body + `**Session Id**: ${id}\n`;
|
|
151
160
|
} else {
|
|
152
161
|
// Match Session Id field and any trailing newlines, replace with single newline
|
|
153
162
|
const sessionIdRegex = /- Session Id: [^\n]*\n*/;
|
|
154
163
|
if (sessionIdRegex.test(body)) {
|
|
155
164
|
return body.replace(sessionIdRegex, `- Session Id: ${id}\n`);
|
|
156
|
-
} else {
|
|
157
|
-
return body + `- Session Id: ${id}\n`;
|
|
158
165
|
}
|
|
166
|
+
return body + `- Session Id: ${id}\n`;
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
function upsertCallSubject({ body, subject, logFormat }) {
|
|
163
171
|
if (!subject) return body;
|
|
164
172
|
|
|
165
|
-
if (logFormat ===
|
|
173
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
166
174
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
167
175
|
const subjectRegex = /(?:<li>)?<b>Summary<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
168
176
|
if (subjectRegex.test(body)) {
|
|
169
177
|
return body.replace(subjectRegex, `<li><b>Summary</b>: ${subject}</li>`);
|
|
170
|
-
} else {
|
|
171
|
-
return body + `<li><b>Summary</b>: ${subject}</li>`;
|
|
172
178
|
}
|
|
179
|
+
return body + `<li><b>Summary</b>: ${subject}</li>`;
|
|
180
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
181
|
+
// Markdown format: **Summary**: value
|
|
182
|
+
const subjectRegex = /\*\*Summary\*\*: [^\n]*\n*/;
|
|
183
|
+
if (subjectRegex.test(body)) {
|
|
184
|
+
return body.replace(subjectRegex, `**Summary**: ${subject}\n`);
|
|
185
|
+
}
|
|
186
|
+
return body + `**Summary**: ${subject}\n`;
|
|
173
187
|
} else {
|
|
174
188
|
// Match Summary field and any trailing newlines, replace with single newline
|
|
175
189
|
const subjectRegex = /- Summary: [^\n]*\n*/;
|
|
176
190
|
if (subjectRegex.test(body)) {
|
|
177
191
|
return body.replace(subjectRegex, `- Summary: ${subject}\n`);
|
|
178
|
-
} else {
|
|
179
|
-
return body + `- Summary: ${subject}\n`;
|
|
180
192
|
}
|
|
193
|
+
return body + `- Summary: ${subject}\n`;
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
196
|
|
|
@@ -187,7 +200,7 @@ function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
|
|
|
187
200
|
const label = direction === 'Outbound' ? 'Recipient' : 'Caller';
|
|
188
201
|
let result = body;
|
|
189
202
|
|
|
190
|
-
if (logFormat ===
|
|
203
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
191
204
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
192
205
|
const phoneNumberRegex = new RegExp(`(?:<li>)?<b>${label} phone number</b>:\\s*([^<\\n]+)(?:</li>|(?=<|$))`, 'i');
|
|
193
206
|
if (phoneNumberRegex.test(result)) {
|
|
@@ -195,6 +208,14 @@ function upsertContactPhoneNumber({ body, phoneNumber, direction, logFormat }) {
|
|
|
195
208
|
} else {
|
|
196
209
|
result += `<li><b>${label} phone number</b>: ${phoneNumber}</li>`;
|
|
197
210
|
}
|
|
211
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
212
|
+
// Markdown format: **Contact Number**: value
|
|
213
|
+
const phoneNumberRegex = /\*\*Contact Number\*\*: [^\n]*\n*/;
|
|
214
|
+
if (phoneNumberRegex.test(result)) {
|
|
215
|
+
result = result.replace(phoneNumberRegex, `**Contact Number**: ${phoneNumber}\n`);
|
|
216
|
+
} else {
|
|
217
|
+
result += `**Contact Number**: ${phoneNumber}\n`;
|
|
218
|
+
}
|
|
198
219
|
} else {
|
|
199
220
|
// More flexible regex that handles both with and without newlines
|
|
200
221
|
const phoneNumberRegex = /- Contact Number: ([^\n-]+)(?=\n-|\n|$)/;
|
|
@@ -225,7 +246,7 @@ function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) {
|
|
|
225
246
|
const formattedDateTime = momentTime.format('YYYY-MM-DD hh:mm:ss A');
|
|
226
247
|
let result = body;
|
|
227
248
|
|
|
228
|
-
if (logFormat ===
|
|
249
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
229
250
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
230
251
|
const dateTimeRegex = /(?:<li>)?<b>Date\/time<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
231
252
|
if (dateTimeRegex.test(result)) {
|
|
@@ -233,6 +254,14 @@ function upsertCallDateTime({ body, startTime, timezoneOffset, logFormat }) {
|
|
|
233
254
|
} else {
|
|
234
255
|
result += `<li><b>Date/time</b>: ${formattedDateTime}</li>`;
|
|
235
256
|
}
|
|
257
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
258
|
+
// Markdown format: **Date/Time**: value
|
|
259
|
+
const dateTimeRegex = /\*\*Date\/Time\*\*: [^\n]*\n*/;
|
|
260
|
+
if (dateTimeRegex.test(result)) {
|
|
261
|
+
result = result.replace(dateTimeRegex, `**Date/Time**: ${formattedDateTime}\n`);
|
|
262
|
+
} else {
|
|
263
|
+
result += `**Date/Time**: ${formattedDateTime}\n`;
|
|
264
|
+
}
|
|
236
265
|
} else {
|
|
237
266
|
// Handle duplicated Date/Time entries and match complete date/time values
|
|
238
267
|
const dateTimeRegex = /(?:- Date\/Time: [^-]*(?:-[^-]*)*)+/;
|
|
@@ -251,7 +280,7 @@ function upsertCallDuration({ body, duration, logFormat }) {
|
|
|
251
280
|
const formattedDuration = secondsToHoursMinutesSeconds(duration);
|
|
252
281
|
let result = body;
|
|
253
282
|
|
|
254
|
-
if (logFormat ===
|
|
283
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
255
284
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
256
285
|
const durationRegex = /(?:<li>)?<b>Duration<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
257
286
|
if (durationRegex.test(result)) {
|
|
@@ -259,6 +288,14 @@ function upsertCallDuration({ body, duration, logFormat }) {
|
|
|
259
288
|
} else {
|
|
260
289
|
result += `<li><b>Duration</b>: ${formattedDuration}</li>`;
|
|
261
290
|
}
|
|
291
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
292
|
+
// Markdown format: **Duration**: value
|
|
293
|
+
const durationRegex = /\*\*Duration\*\*: [^\n]*\n*/;
|
|
294
|
+
if (durationRegex.test(result)) {
|
|
295
|
+
result = result.replace(durationRegex, `**Duration**: ${formattedDuration}\n`);
|
|
296
|
+
} else {
|
|
297
|
+
result += `**Duration**: ${formattedDuration}\n`;
|
|
298
|
+
}
|
|
262
299
|
} else {
|
|
263
300
|
// More flexible regex that handles both with and without newlines
|
|
264
301
|
const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
|
|
@@ -276,7 +313,7 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
276
313
|
|
|
277
314
|
let bodyResult = body;
|
|
278
315
|
|
|
279
|
-
if (logFormat ===
|
|
316
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
280
317
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
281
318
|
const resultRegex = /(?:<li>)?<b>Result<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
282
319
|
if (resultRegex.test(bodyResult)) {
|
|
@@ -284,6 +321,14 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
284
321
|
} else {
|
|
285
322
|
bodyResult += `<li><b>Result</b>: ${result}</li>`;
|
|
286
323
|
}
|
|
324
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
325
|
+
// Markdown format: **Result**: value
|
|
326
|
+
const resultRegex = /\*\*Result\*\*: [^\n]*\n*/;
|
|
327
|
+
if (resultRegex.test(bodyResult)) {
|
|
328
|
+
bodyResult = bodyResult.replace(resultRegex, `**Result**: ${result}\n`);
|
|
329
|
+
} else {
|
|
330
|
+
bodyResult += `**Result**: ${result}\n`;
|
|
331
|
+
}
|
|
287
332
|
} else {
|
|
288
333
|
// More flexible regex that handles both with and without newlines
|
|
289
334
|
const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
|
|
@@ -297,12 +342,11 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
297
342
|
}
|
|
298
343
|
|
|
299
344
|
function upsertCallRecording({ body, recordingLink, logFormat }) {
|
|
300
|
-
// console.log({ m: "upsertCallRecording", recordingLink, hasBody: !!body, logFormat, bodyLength: body?.length });
|
|
301
345
|
if (!recordingLink) return body;
|
|
302
346
|
|
|
303
347
|
let result = body;
|
|
304
348
|
|
|
305
|
-
if (logFormat ===
|
|
349
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
306
350
|
// More flexible regex that handles both <li> wrapped and unwrapped content
|
|
307
351
|
const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
308
352
|
if (recordingLink) {
|
|
@@ -326,6 +370,14 @@ function upsertCallRecording({ body, recordingLink, logFormat }) {
|
|
|
326
370
|
}
|
|
327
371
|
}
|
|
328
372
|
}
|
|
373
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
374
|
+
// Markdown format: **Call recording link**: value
|
|
375
|
+
const recordingLinkRegex = /\*\*Call recording link\*\*: [^\n]*\n*/;
|
|
376
|
+
if (recordingLinkRegex.test(result)) {
|
|
377
|
+
result = result.replace(recordingLinkRegex, `**Call recording link**: ${recordingLink}\n`);
|
|
378
|
+
} else {
|
|
379
|
+
result += `**Call recording link**: ${recordingLink}\n`;
|
|
380
|
+
}
|
|
329
381
|
} else {
|
|
330
382
|
// Match recording link field and any trailing content, replace with single newline
|
|
331
383
|
const recordingLinkRegex = /- Call recording link: [^\n]*\n*/;
|
|
@@ -347,7 +399,7 @@ function upsertAiNote({ body, aiNote, logFormat }) {
|
|
|
347
399
|
const clearedAiNote = aiNote.replace(/\n+$/, '');
|
|
348
400
|
let result = body;
|
|
349
401
|
|
|
350
|
-
if (logFormat ===
|
|
402
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
351
403
|
const formattedAiNote = clearedAiNote.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
352
404
|
const aiNoteRegex = /<div><b>AI Note<\/b><br>(.+?)<\/div>/;
|
|
353
405
|
if (aiNoteRegex.test(result)) {
|
|
@@ -355,6 +407,14 @@ function upsertAiNote({ body, aiNote, logFormat }) {
|
|
|
355
407
|
} else {
|
|
356
408
|
result += `<div><b>AI Note</b><br>${formattedAiNote}</div><br>`;
|
|
357
409
|
}
|
|
410
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
411
|
+
// Markdown format: ### AI Note
|
|
412
|
+
const aiNoteRegex = /### AI Note\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
413
|
+
if (aiNoteRegex.test(result)) {
|
|
414
|
+
result = result.replace(aiNoteRegex, `### AI Note\n${clearedAiNote}\n`);
|
|
415
|
+
} else {
|
|
416
|
+
result += `### AI Note\n${clearedAiNote}\n`;
|
|
417
|
+
}
|
|
358
418
|
} else {
|
|
359
419
|
const aiNoteRegex = /- AI Note:([\s\S]*?)--- END/;
|
|
360
420
|
if (aiNoteRegex.test(result)) {
|
|
@@ -371,7 +431,7 @@ function upsertTranscript({ body, transcript, logFormat }) {
|
|
|
371
431
|
|
|
372
432
|
let result = body;
|
|
373
433
|
|
|
374
|
-
if (logFormat ===
|
|
434
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
375
435
|
const formattedTranscript = transcript.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
376
436
|
const transcriptRegex = /<div><b>Transcript<\/b><br>(.+?)<\/div>/;
|
|
377
437
|
if (transcriptRegex.test(result)) {
|
|
@@ -379,6 +439,14 @@ function upsertTranscript({ body, transcript, logFormat }) {
|
|
|
379
439
|
} else {
|
|
380
440
|
result += `<div><b>Transcript</b><br>${formattedTranscript}</div><br>`;
|
|
381
441
|
}
|
|
442
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
443
|
+
// Markdown format: ### Transcript
|
|
444
|
+
const transcriptRegex = /### Transcript\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
445
|
+
if (transcriptRegex.test(result)) {
|
|
446
|
+
result = result.replace(transcriptRegex, `### Transcript\n${transcript}\n`);
|
|
447
|
+
} else {
|
|
448
|
+
result += `### Transcript\n${transcript}\n`;
|
|
449
|
+
}
|
|
382
450
|
} else {
|
|
383
451
|
const transcriptRegex = /- Transcript:([\s\S]*?)--- END/;
|
|
384
452
|
if (transcriptRegex.test(result)) {
|
|
@@ -401,43 +469,9 @@ function getLogFormatType(platform) {
|
|
|
401
469
|
return platformConfig?.logFormat;
|
|
402
470
|
}
|
|
403
471
|
|
|
404
|
-
/**
|
|
405
|
-
* Create a specialized composition function for specific CRM requirements
|
|
406
|
-
* @param {string} platform - CRM platform name
|
|
407
|
-
* @returns {Function} Customized composition function
|
|
408
|
-
*/
|
|
409
|
-
function createComposer(platform) {
|
|
410
|
-
const logFormat = getLogFormatType(platform);
|
|
411
|
-
|
|
412
|
-
return async function (params) {
|
|
413
|
-
// Add platform-specific formatting
|
|
414
|
-
if (logFormat === FORMAT_TYPES.HTML && platform === 'pipedrive') {
|
|
415
|
-
// Pipedrive wraps call details in <ul> tags
|
|
416
|
-
const composed = await composeCallLog({ ...params, logFormat });
|
|
417
|
-
if (composed && !composed.includes('<ul>')) {
|
|
418
|
-
return `<b>Call details</b><ul>${composed}</ul>`;
|
|
419
|
-
}
|
|
420
|
-
return composed;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (logFormat === FORMAT_TYPES.HTML && platform === 'bullhorn') {
|
|
424
|
-
// Bullhorn also wraps call details in <ul> tags
|
|
425
|
-
const composed = await composeCallLog({ ...params, logFormat });
|
|
426
|
-
if (composed && !composed.includes('<ul>')) {
|
|
427
|
-
return `<b>Call details</b><ul>${composed}</ul>`;
|
|
428
|
-
}
|
|
429
|
-
return composed;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return composeCallLog({ ...params, logFormat });
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
472
|
module.exports = {
|
|
437
473
|
composeCallLog,
|
|
438
|
-
createComposer,
|
|
439
474
|
getLogFormatType,
|
|
440
|
-
FORMAT_TYPES,
|
|
441
475
|
// Export individual upsert functions for backward compatibility
|
|
442
476
|
upsertCallAgentNote,
|
|
443
477
|
upsertCallSessionId,
|
package/lib/constants.js
ADDED
package/lib/oauth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
2
|
const ClientOAuth2 = require('client-oauth2');
|
|
3
|
-
const { Lock } = require('../models/dynamo/lockSchema');
|
|
4
3
|
const { UserModel } = require('../models/userModel');
|
|
5
4
|
const adapterRegistry = require('../adapter/registry');
|
|
5
|
+
const dynamoose = require('dynamoose');
|
|
6
6
|
|
|
7
7
|
// oauthApp strategy is default to 'code' which use credentials to get accessCode, then exchange for accessToken and refreshToken.
|
|
8
8
|
// To change to other strategies, please refer to: https://github.com/mulesoft-labs/js-client-oauth2
|
|
@@ -31,33 +31,67 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10)
|
|
|
31
31
|
}
|
|
32
32
|
// Other CRMs
|
|
33
33
|
if (user && user.accessToken && user.refreshToken && tokenExpiry.getTime() < (dateNow.getTime() + expiryBuffer)) {
|
|
34
|
+
let newLock;
|
|
34
35
|
// case: use dynamoDB to manage token refresh lock
|
|
35
36
|
if (process.env.USE_TOKEN_REFRESH_LOCK === 'true') {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
const { Lock } = require('../models/dynamo/lockSchema');
|
|
38
|
+
// Try to atomically create lock only if it doesn't exist
|
|
39
|
+
try {
|
|
40
|
+
newLock = await Lock.create(
|
|
41
|
+
{
|
|
42
|
+
userId: user.id,
|
|
43
|
+
ttl: dateNow.getTime() + 1000 * 30
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
condition: new dynamoose.Condition().where('userId').not().exists()
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
console.log('lock created')
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// If creation failed due to condition, a lock exists
|
|
52
|
+
if (e.name === 'ConditionalCheckFailedException' || e.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
|
|
53
|
+
let lock = await Lock.get({ userId: user.id });
|
|
54
|
+
if (!!lock?.ttl && lock.ttl < dateNow.getTime()) {
|
|
55
|
+
// Try to delete expired lock and create a new one atomically
|
|
56
|
+
try {
|
|
57
|
+
await lock.delete();
|
|
58
|
+
newLock = await Lock.create(
|
|
59
|
+
{ userId: user.id },
|
|
60
|
+
{
|
|
61
|
+
condition: new dynamoose.Condition().where('userId').not().exists()
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
} catch (e2) {
|
|
65
|
+
if (e2.name === 'ConditionalCheckFailedException' || e2.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
|
|
66
|
+
// Another process created a lock between our delete and create
|
|
67
|
+
lock = await Lock.get({ userId: user.id });
|
|
68
|
+
} else {
|
|
69
|
+
throw e2;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (lock && !newLock) {
|
|
75
|
+
let processTime = 0;
|
|
76
|
+
while (!!lock && processTime < tokenLockTimeout) {
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // wait for 2 seconds
|
|
78
|
+
processTime += 2;
|
|
79
|
+
lock = await Lock.get({ userId: user.id });
|
|
80
|
+
}
|
|
81
|
+
// Timeout -> let users try another time
|
|
82
|
+
if (processTime >= tokenLockTimeout) {
|
|
83
|
+
throw new Error('Token lock timeout');
|
|
84
|
+
}
|
|
85
|
+
user = await UserModel.findByPk(user.id);
|
|
86
|
+
console.log('locked. bypass')
|
|
87
|
+
return user;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
throw e;
|
|
52
91
|
}
|
|
53
|
-
user = await UserModel.findByPk(user.id);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
newLock = await Lock.create({
|
|
57
|
-
userId: user.id
|
|
58
|
-
});
|
|
59
92
|
}
|
|
60
93
|
const token = oauthApp.createToken(user.accessToken, user.refreshToken);
|
|
94
|
+
console.log('token refreshing...')
|
|
61
95
|
const { accessToken, refreshToken, expires } = await token.refresh();
|
|
62
96
|
user.accessToken = accessToken;
|
|
63
97
|
user.refreshToken = refreshToken;
|
|
@@ -66,15 +100,18 @@ async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10)
|
|
|
66
100
|
if (newLock) {
|
|
67
101
|
await newLock.delete();
|
|
68
102
|
}
|
|
103
|
+
console.log('token refreshing finished')
|
|
69
104
|
}
|
|
70
105
|
// case: run withou token refresh lock
|
|
71
106
|
else {
|
|
107
|
+
console.log('token refreshing...')
|
|
72
108
|
const token = oauthApp.createToken(user.accessToken, user.refreshToken);
|
|
73
109
|
const { accessToken, refreshToken, expires } = await token.refresh();
|
|
74
110
|
user.accessToken = accessToken;
|
|
75
111
|
user.refreshToken = refreshToken;
|
|
76
112
|
user.tokenExpiry = expires;
|
|
77
113
|
await user.save();
|
|
114
|
+
console.log('token refreshing finished')
|
|
78
115
|
}
|
|
79
116
|
|
|
80
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@app-connect/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.5.8",
|
|
4
4
|
"description": "RingCentral App Connect Core",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"tz-lookup": "^6.1.25",
|
|
36
36
|
"ua-parser-js": "^1.0.38"
|
|
37
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
|
+
},
|
|
38
44
|
"devDependencies": {
|
|
39
45
|
"@eslint/js": "^9.22.0",
|
|
40
46
|
"@octokit/rest": "^19.0.5",
|
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.5.8": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "Fix",
|
|
6
|
+
"description": "- Error on showing a window-size warning message"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "Fix",
|
|
10
|
+
"description": "- Disconnected users shown as connected"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "Better",
|
|
14
|
+
"description": "- Every 5min, retroatively check recording links at pending state and update them to existing call logs"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "Better",
|
|
18
|
+
"description": "- More stable user session"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Better",
|
|
22
|
+
"description": "- User settings sync message suppressed"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "Better",
|
|
26
|
+
"description": "- Webpage embed (Click-to-dial and Quick-access-button) urls setting is renamed to 'Enabled domains' under General -> Appearance"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
"1.5.7": {
|
|
3
31
|
"global": [
|
|
4
32
|
{
|