@app-connect/core 1.7.0 → 1.7.3
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/README.md +1 -1
- package/connector/proxy/engine.js +163 -0
- package/connector/proxy/index.js +492 -0
- package/connector/registry.js +11 -8
- package/handlers/admin.js +24 -10
- package/handlers/auth.js +20 -10
- package/handlers/contact.js +26 -9
- package/handlers/disposition.js +19 -6
- package/handlers/log.js +62 -18
- package/index.js +48 -2
- package/lib/analytics.js +1 -1
- package/lib/callLogComposer.js +196 -10
- package/lib/oauth.js +0 -1
- package/models/dynamo/connectorSchema.js +146 -0
- package/package.json +1 -1
- package/releaseNotes.json +64 -0
- package/test/connector/proxy/engine.test.js +93 -0
- package/test/connector/proxy/index.test.js +279 -0
- package/test/connector/proxy/sample.json +161 -0
- package/test/{adapter → connector}/registry.test.js +4 -4
- package/test/handlers/auth.test.js +3 -1
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const adminCore = require('./handlers/admin');
|
|
|
18
18
|
const userCore = require('./handlers/user');
|
|
19
19
|
const dispositionCore = require('./handlers/disposition');
|
|
20
20
|
const mock = require('./connector/mock');
|
|
21
|
+
const proxyConnector = require('./connector/proxy');
|
|
21
22
|
const releaseNotes = require('./releaseNotes.json');
|
|
22
23
|
const analytics = require('./lib/analytics');
|
|
23
24
|
const util = require('./lib/util');
|
|
@@ -124,6 +125,48 @@ function createCoreRouter() {
|
|
|
124
125
|
res.send(`OK`);
|
|
125
126
|
});
|
|
126
127
|
|
|
128
|
+
router.get('/implementedInterfaces', (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const platform = req.query.platform;
|
|
131
|
+
if (platform) {
|
|
132
|
+
const platformModule = connectorRegistry.getConnector(platform);
|
|
133
|
+
const result = {};
|
|
134
|
+
const authType = platformModule.getAuthType();
|
|
135
|
+
result.getAuthType = !!platformModule.getAuthType;
|
|
136
|
+
switch(authType){
|
|
137
|
+
case 'oauth':
|
|
138
|
+
result.getOauthInfo = !!platformModule.getOauthInfo;
|
|
139
|
+
break;
|
|
140
|
+
case 'apiKey':
|
|
141
|
+
result.getBasicAuth = !!platformModule.getBasicAuth;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
result.getUserInfo = !!platformModule.getUserInfo;
|
|
145
|
+
result.createCallLog = !!platformModule.createCallLog;
|
|
146
|
+
result.updateCallLog = !!platformModule.updateCallLog;
|
|
147
|
+
result.getCallLog = !!platformModule.getCallLog;
|
|
148
|
+
result.createMessageLog = !!platformModule.createMessageLog;
|
|
149
|
+
result.updateMessageLog = !!platformModule.updateMessageLog;
|
|
150
|
+
result.createContact = !!platformModule.createContact;
|
|
151
|
+
result.findContact = !!platformModule.findContact;
|
|
152
|
+
result.unAuthorize = !!platformModule.unAuthorize;
|
|
153
|
+
result.upsertCallDisposition = !!platformModule.upsertCallDisposition;
|
|
154
|
+
result.findContactWithName = !!platformModule.findContactWithName;
|
|
155
|
+
result.getUserList = !!platformModule.getUserList;
|
|
156
|
+
result.getLicenseStatus = !!platformModule.getLicenseStatus;
|
|
157
|
+
result.getLogFormatType = !!platformModule.getLogFormatType;
|
|
158
|
+
res.status(200).send(result);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
res.status(400).send('Please provide platform.');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
res.status(400).send(e);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
127
170
|
router.get('/licenseStatus', async (req, res) => {
|
|
128
171
|
const requestStartTime = new Date().getTime();
|
|
129
172
|
let platformName = null;
|
|
@@ -647,7 +690,8 @@ function createCoreRouter() {
|
|
|
647
690
|
callbackUri: req.query.callbackUri,
|
|
648
691
|
apiUrl: req.query.apiUrl,
|
|
649
692
|
username: req.query.username,
|
|
650
|
-
query: req.query
|
|
693
|
+
query: req.query,
|
|
694
|
+
proxyId: req.query.proxyId
|
|
651
695
|
});
|
|
652
696
|
if (userInfo) {
|
|
653
697
|
const jwtToken = jwt.generateJwt({
|
|
@@ -692,6 +736,7 @@ function createCoreRouter() {
|
|
|
692
736
|
platformName = platform;
|
|
693
737
|
const apiKey = req.body.apiKey;
|
|
694
738
|
const hostname = req.body.hostname;
|
|
739
|
+
const proxyId = req.body.proxyId;
|
|
695
740
|
const additionalInfo = req.body.additionalInfo;
|
|
696
741
|
if (!platform) {
|
|
697
742
|
throw 'Missing platform name';
|
|
@@ -699,7 +744,7 @@ function createCoreRouter() {
|
|
|
699
744
|
if (!apiKey) {
|
|
700
745
|
throw 'Missing api key';
|
|
701
746
|
}
|
|
702
|
-
const { userInfo, returnMessage } = await authCore.onApiKeyLogin({ platform, hostname, apiKey, additionalInfo });
|
|
747
|
+
const { userInfo, returnMessage } = await authCore.onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalInfo });
|
|
703
748
|
if (userInfo) {
|
|
704
749
|
const jwtToken = jwt.generateJwt({
|
|
705
750
|
id: userInfo.id.toString(),
|
|
@@ -1626,3 +1671,4 @@ exports.createCoreMiddleware = createCoreMiddleware;
|
|
|
1626
1671
|
exports.createCoreApp = createCoreApp;
|
|
1627
1672
|
exports.initializeCore = initializeCore;
|
|
1628
1673
|
exports.connectorRegistry = connectorRegistry;
|
|
1674
|
+
exports.proxyConnector = proxyConnector;
|
package/lib/analytics.js
CHANGED
|
@@ -33,7 +33,7 @@ exports.track = function track({ eventName, interfaceName, connectorName, accoun
|
|
|
33
33
|
mixpanel.track(eventName, {
|
|
34
34
|
distinct_id: extensionId,
|
|
35
35
|
interfaceName,
|
|
36
|
-
connectorName,
|
|
36
|
+
adapterName: connectorName,
|
|
37
37
|
rcAccountId: accountId,
|
|
38
38
|
extensionId,
|
|
39
39
|
success,
|
package/lib/callLogComposer.js
CHANGED
|
@@ -41,6 +41,11 @@ async function composeCallLog(params) {
|
|
|
41
41
|
startTime,
|
|
42
42
|
duration,
|
|
43
43
|
result,
|
|
44
|
+
ringSenseTranscript,
|
|
45
|
+
ringSenseSummary,
|
|
46
|
+
ringSenseAIScore,
|
|
47
|
+
ringSenseBulletedSummary,
|
|
48
|
+
ringSenseLink,
|
|
44
49
|
platform
|
|
45
50
|
} = params;
|
|
46
51
|
|
|
@@ -62,14 +67,18 @@ async function composeCallLog(params) {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
if (userSettings?.addRingCentralUserName?.value) {
|
|
65
|
-
const ringcentralUsername = (callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name) ??
|
|
66
|
-
|
|
70
|
+
const ringcentralUsername = (callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name) ?? null;
|
|
71
|
+
if (ringcentralUsername) {
|
|
72
|
+
body = upsertRingCentralUserName({ body, userName: ringcentralUsername, logFormat });
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
if (userSettings?.addRingCentralNumber?.value ?? false) {
|
|
77
|
+
const ringcentralNumber = callLog.direction === 'Inbound' ? callLog?.to?.phoneNumber : callLog?.from?.phoneNumber;
|
|
78
|
+
if (ringcentralNumber) {
|
|
79
|
+
const ringcentralExtensionNumber = callLog.direction === 'Inbound' ? callLog?.from?.extensionNumber : callLog?.to?.extensionNumber;
|
|
80
|
+
body = upsertRingCentralNumberAndExtension({ body, number: ringcentralNumber, extension: ringcentralExtensionNumber ?? '', logFormat });
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
if (subject && (userSettings?.addCallLogSubject?.value ?? true)) {
|
|
@@ -115,6 +124,26 @@ async function composeCallLog(params) {
|
|
|
115
124
|
body = upsertTranscript({ body, transcript, logFormat });
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
if (ringSenseTranscript && (userSettings?.addCallLogRingSenseRecordingTranscript?.value ?? true)) {
|
|
128
|
+
body = upsertRingSenseTranscript({ body, transcript: ringSenseTranscript, logFormat });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ringSenseSummary && (userSettings?.addCallLogRingSenseRecordingSummary?.value ?? true)) {
|
|
132
|
+
body = upsertRingSenseSummary({ body, summary: ringSenseSummary, logFormat });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (ringSenseAIScore && (userSettings?.addCallLogRingSenseRecordingAIScore?.value ?? true)) {
|
|
136
|
+
body = upsertRingSenseAIScore({ body, score: ringSenseAIScore, logFormat });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (ringSenseBulletedSummary && (userSettings?.addCallLogRingSenseRecordingBulletedSummary?.value ?? true)) {
|
|
140
|
+
body = upsertRingSenseBulletedSummary({ body, summary: ringSenseBulletedSummary, logFormat });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (ringSenseLink && (userSettings?.addCallLogRingSenseRecordingLink?.value ?? true)) {
|
|
144
|
+
body = upsertRingSenseLink({ body, link: ringSenseLink, logFormat });
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
if (callLog?.legs && (userSettings?.addCallLogLegs?.value ?? true)) {
|
|
119
148
|
body = upsertLegs({ body, legs: callLog.legs, logFormat });
|
|
120
149
|
}
|
|
@@ -382,7 +411,7 @@ function upsertCallDuration({ body, duration, logFormat }) {
|
|
|
382
411
|
// More flexible regex that handles both with and without newlines
|
|
383
412
|
const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
|
|
384
413
|
if (durationRegex.test(result)) {
|
|
385
|
-
result = result.replace(durationRegex, `- Duration: ${formattedDuration}
|
|
414
|
+
result = result.replace(durationRegex, `- Duration: ${formattedDuration}`);
|
|
386
415
|
} else {
|
|
387
416
|
result += `- Duration: ${formattedDuration}\n`;
|
|
388
417
|
}
|
|
@@ -415,7 +444,7 @@ function upsertCallResult({ body, result, logFormat }) {
|
|
|
415
444
|
// More flexible regex that handles both with and without newlines
|
|
416
445
|
const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
|
|
417
446
|
if (resultRegex.test(bodyResult)) {
|
|
418
|
-
bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}
|
|
447
|
+
bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}`);
|
|
419
448
|
} else {
|
|
420
449
|
bodyResult += `- Result: ${result}\n`;
|
|
421
450
|
}
|
|
@@ -429,8 +458,8 @@ function upsertCallRecording({ body, recordingLink, logFormat }) {
|
|
|
429
458
|
let result = body;
|
|
430
459
|
|
|
431
460
|
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
432
|
-
|
|
433
|
-
|
|
461
|
+
// More flexible regex that handles both <li> wrapped and unwrapped content, and existing <a> anchors
|
|
462
|
+
const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*(?:<a[^>]*>[^<]*<\/a>|[^<]+)(?:<\/li>|(?=<|$))/i;
|
|
434
463
|
if (recordingLink) {
|
|
435
464
|
if (recordingLinkRegex.test(result)) {
|
|
436
465
|
if (recordingLink.startsWith('http')) {
|
|
@@ -615,6 +644,158 @@ function upsertLegs({ body, legs, logFormat }) {
|
|
|
615
644
|
return result;
|
|
616
645
|
}
|
|
617
646
|
|
|
647
|
+
function upsertRingSenseTranscript({ body, transcript, logFormat }) {
|
|
648
|
+
if (!transcript) return body;
|
|
649
|
+
|
|
650
|
+
let result = body;
|
|
651
|
+
const clearedTranscript = transcript.replace(/\n+$/, '');
|
|
652
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
653
|
+
const formattedTranscript = clearedTranscript.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
654
|
+
const transcriptRegex = /<div><b>RingSense transcript<\/b><br>(.+?)<\/div>/;
|
|
655
|
+
if (transcriptRegex.test(result)) {
|
|
656
|
+
result = result.replace(transcriptRegex, `<div><b>RingSense transcript</b><br>${formattedTranscript}</div>`);
|
|
657
|
+
} else {
|
|
658
|
+
result += `<div><b>RingSense transcript</b><br>${formattedTranscript}</div>`;
|
|
659
|
+
}
|
|
660
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
661
|
+
const transcriptRegex = /### RingSense transcript\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
662
|
+
if (transcriptRegex.test(result)) {
|
|
663
|
+
result = result.replace(transcriptRegex, `### RingSense transcript\n${clearedTranscript}\n`);
|
|
664
|
+
} else {
|
|
665
|
+
result += `### RingSense transcript\n${clearedTranscript}\n`;
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
const transcriptRegex = /- RingSense transcript:([\s\S]*?)--- END/;
|
|
669
|
+
if (transcriptRegex.test(result)) {
|
|
670
|
+
result = result.replace(transcriptRegex, `- RingSense transcript:\n${clearedTranscript}\n--- END`);
|
|
671
|
+
} else {
|
|
672
|
+
result += `- RingSense transcript:\n${clearedTranscript}\n--- END\n`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function upsertRingSenseSummary({ body, summary, logFormat }) {
|
|
679
|
+
if (!summary) return body;
|
|
680
|
+
|
|
681
|
+
let result = body;
|
|
682
|
+
// remove new line in last line of summary
|
|
683
|
+
const clearedSummary = summary.replace(/\n+$/, '');
|
|
684
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
685
|
+
const summaryRegex = /<div><b>RingSense summary<\/b><br>(.+?)<\/div>/;
|
|
686
|
+
const formattedSummary = clearedSummary.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
687
|
+
if (summaryRegex.test(result)) {
|
|
688
|
+
result = result.replace(summaryRegex, `<div><b>RingSense summary</b><br>${formattedSummary}</div>`);
|
|
689
|
+
} else {
|
|
690
|
+
result += `<div><b>RingSense summary</b><br>${formattedSummary}</div>`;
|
|
691
|
+
}
|
|
692
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
693
|
+
const summaryRegex = /### RingSense summary\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
694
|
+
if (summaryRegex.test(result)) {
|
|
695
|
+
result = result.replace(summaryRegex, `### RingSense summary\n${summary}\n`);
|
|
696
|
+
} else {
|
|
697
|
+
result += `### RingSense summary\n${summary}\n`;
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
const summaryRegex = /- RingSense summary:([\s\S]*?)--- END/;
|
|
701
|
+
if (summaryRegex.test(result)) {
|
|
702
|
+
result = result.replace(summaryRegex, `- RingSense summary:\n${summary}\n--- END`);
|
|
703
|
+
} else {
|
|
704
|
+
result += `- RingSense summary:\n${summary}\n--- END\n`;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function upsertRingSenseAIScore({ body, score, logFormat }) {
|
|
711
|
+
if (!score) return body;
|
|
712
|
+
|
|
713
|
+
let result = body;
|
|
714
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
715
|
+
const scoreRegex = /(?:<li>)?<b>Call score<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
|
|
716
|
+
if (scoreRegex.test(result)) {
|
|
717
|
+
result = result.replace(scoreRegex, `<li><b>Call score</b>: ${score}</li>`);
|
|
718
|
+
} else {
|
|
719
|
+
result += `<li><b>Call score</b>: ${score}</li>`;
|
|
720
|
+
}
|
|
721
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
722
|
+
const scoreRegex = /\*\*Call score\*\*: [^\n]*\n*/;
|
|
723
|
+
if (scoreRegex.test(result)) {
|
|
724
|
+
result = result.replace(scoreRegex, `**Call score**: ${score}\n`);
|
|
725
|
+
} else {
|
|
726
|
+
result += `**Call score**: ${score}\n`;
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
const scoreRegex = /- Call score:\s*([^<\n]+)(?=\n|$)/i;
|
|
730
|
+
if (scoreRegex.test(result)) {
|
|
731
|
+
result = result.replace(scoreRegex, `- Call score: ${score}`);
|
|
732
|
+
} else {
|
|
733
|
+
result += `- Call score: ${score}\n`;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function upsertRingSenseBulletedSummary({ body, summary, logFormat }) {
|
|
740
|
+
if (!summary) return body;
|
|
741
|
+
|
|
742
|
+
let result = body;
|
|
743
|
+
const clearedSummary = summary.replace(/\n+$/, '');
|
|
744
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
745
|
+
const summaryRegex = /<div><b>RingSense bulleted summary<\/b><br>(.+?)<\/div>/;
|
|
746
|
+
const formattedSummary = clearedSummary.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
|
747
|
+
if (summaryRegex.test(result)) {
|
|
748
|
+
result = result.replace(summaryRegex, `<div><b>RingSense bulleted summary</b><br>${formattedSummary}</div>`);
|
|
749
|
+
} else {
|
|
750
|
+
result += `<div><b>RingSense bulleted summary</b><br>${formattedSummary}</div>`;
|
|
751
|
+
}
|
|
752
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
753
|
+
const summaryRegex = /### RingSense bulleted summary\n([\s\S]*?)(?=\n### |\n$|$)/;
|
|
754
|
+
if (summaryRegex.test(result)) {
|
|
755
|
+
result = result.replace(summaryRegex, `### RingSense bulleted summary\n${summary}\n`);
|
|
756
|
+
} else {
|
|
757
|
+
result += `### RingSense bulleted summary\n${summary}\n`;
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
const summaryRegex = /- RingSense bulleted summary:\s*([^<\n]+)(?=\n|$)/i;
|
|
761
|
+
if (summaryRegex.test(result)) {
|
|
762
|
+
result = result.replace(summaryRegex, `- RingSense bulleted summary:\n${summary}\n--- END`);
|
|
763
|
+
} else {
|
|
764
|
+
result += `- RingSense bulleted summary:\n${summary}\n--- END\n`;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function upsertRingSenseLink({ body, link, logFormat }) {
|
|
771
|
+
if (!link) return body;
|
|
772
|
+
|
|
773
|
+
let result = body;
|
|
774
|
+
if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
|
|
775
|
+
const linkRegex = /(?:<li>)?<b>RingSense recording link<\/b>:\s*(?:<a[^>]*>[^<]*<\/a>|[^<]+)(?:<\/li>|(?=<|$))/i;
|
|
776
|
+
if (linkRegex.test(result)) {
|
|
777
|
+
result = result.replace(linkRegex, `<li><b>RingSense recording link</b>: <a target="_blank" href="${link}">open</a></li>`);
|
|
778
|
+
} else {
|
|
779
|
+
result += `<li><b>RingSense recording link</b>: <a target="_blank" href="${link}">open</a></li>`;
|
|
780
|
+
}
|
|
781
|
+
} else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
|
|
782
|
+
const linkRegex = /\*\*RingSense recording link\*\*:\s*([^<\n]+)(?=\n|$)/i;
|
|
783
|
+
if (linkRegex.test(result)) {
|
|
784
|
+
result = result.replace(linkRegex, `**RingSense recording link**: ${link}\n`);
|
|
785
|
+
} else {
|
|
786
|
+
result += `**RingSense recording link**: ${link}\n`;
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
const linkRegex = /- RingSense recording link:\s*([^<\n]+)(?=\n|$)/i;
|
|
790
|
+
if (linkRegex.test(result)) {
|
|
791
|
+
result = result.replace(linkRegex, `- RingSense recording link: ${link}`);
|
|
792
|
+
} else {
|
|
793
|
+
result += `- RingSense recording link: ${link}\n`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
|
|
618
799
|
module.exports = {
|
|
619
800
|
composeCallLog,
|
|
620
801
|
// Export individual upsert functions for backward compatibility
|
|
@@ -631,4 +812,9 @@ module.exports = {
|
|
|
631
812
|
upsertAiNote,
|
|
632
813
|
upsertTranscript,
|
|
633
814
|
upsertLegs,
|
|
815
|
+
upsertRingSenseTranscript,
|
|
816
|
+
upsertRingSenseSummary,
|
|
817
|
+
upsertRingSenseAIScore,
|
|
818
|
+
upsertRingSenseBulletedSummary,
|
|
819
|
+
upsertRingSenseLink,
|
|
634
820
|
};
|
package/lib/oauth.js
CHANGED
|
@@ -3,7 +3,6 @@ const ClientOAuth2 = require('client-oauth2');
|
|
|
3
3
|
const moment = require('moment');
|
|
4
4
|
const { UserModel } = require('../models/userModel');
|
|
5
5
|
const connectorRegistry = require('../connector/registry');
|
|
6
|
-
const dynamoose = require('dynamoose');
|
|
7
6
|
|
|
8
7
|
// oauthApp strategy is default to 'code' which use credentials to get accessCode, then exchange for accessToken and refreshToken.
|
|
9
8
|
// To change to other strategies, please refer to: https://github.com/mulesoft-labs/js-client-oauth2
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const dynamoose = require('dynamoose');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
const CONNECTOR_STATUS = {
|
|
5
|
+
PRIVATE: 'private',
|
|
6
|
+
UNDER_REVIEW: 'under_review',
|
|
7
|
+
APPROVED: 'approved',
|
|
8
|
+
REJECTED: 'rejected',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const connectorSchema = new dynamoose.Schema({
|
|
12
|
+
accountId: {
|
|
13
|
+
type: String,
|
|
14
|
+
hashKey: true,
|
|
15
|
+
},
|
|
16
|
+
id: {
|
|
17
|
+
type: String,
|
|
18
|
+
rangeKey: true,
|
|
19
|
+
},
|
|
20
|
+
// Reference to original connector (for partition records)
|
|
21
|
+
originalAccountId: {
|
|
22
|
+
type: String,
|
|
23
|
+
required: false, // Only set for partition records (under_review, approved)
|
|
24
|
+
},
|
|
25
|
+
// Basic Information
|
|
26
|
+
name: {
|
|
27
|
+
type: String,
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
displayName: String,
|
|
31
|
+
description: String,
|
|
32
|
+
iconUrl: String,
|
|
33
|
+
// Status and Workflow
|
|
34
|
+
status: {
|
|
35
|
+
type: String,
|
|
36
|
+
required: true,
|
|
37
|
+
enum: Object.values(CONNECTOR_STATUS),
|
|
38
|
+
default: CONNECTOR_STATUS.PRIVATE,
|
|
39
|
+
index: {
|
|
40
|
+
name: 'statusIdIndex',
|
|
41
|
+
global: true,
|
|
42
|
+
rangeKey: 'id',
|
|
43
|
+
project: ['accountId', 'name', 'displayName', 'developer', 'originalAccountId'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
creatorId: String,
|
|
47
|
+
// Developer Information
|
|
48
|
+
developer: {
|
|
49
|
+
type: Object,
|
|
50
|
+
schema: {
|
|
51
|
+
name: String,
|
|
52
|
+
websiteUrl: String,
|
|
53
|
+
supportUrl: String,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// Manifest Management
|
|
57
|
+
manifest: {
|
|
58
|
+
type: Object,
|
|
59
|
+
required: true,
|
|
60
|
+
},
|
|
61
|
+
proxyConfig: {
|
|
62
|
+
type: Object,
|
|
63
|
+
required: false,
|
|
64
|
+
},
|
|
65
|
+
proxyId: {
|
|
66
|
+
type: String,
|
|
67
|
+
index: {
|
|
68
|
+
name: 'proxyIdIndex',
|
|
69
|
+
global: true,
|
|
70
|
+
project: ['id', 'accountId', 'creatorId', 'name', 'displayName', 'status', 'developer', 'originalAccountId', 'proxyConfig'],
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
// Review and Approval
|
|
74
|
+
submittedAt: Number,
|
|
75
|
+
reviewedBy: String,
|
|
76
|
+
reviewedAt: Number,
|
|
77
|
+
reviewNotes: String,
|
|
78
|
+
rejectionReason: String,
|
|
79
|
+
demoAccounts: String,
|
|
80
|
+
// Usage and Analytics
|
|
81
|
+
usageCount: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: 0,
|
|
84
|
+
},
|
|
85
|
+
lastUsedAt: Number,
|
|
86
|
+
allowedAccounts: {
|
|
87
|
+
type: Array,
|
|
88
|
+
schema: [String],
|
|
89
|
+
},
|
|
90
|
+
encodedSecretKey: String,
|
|
91
|
+
}, {
|
|
92
|
+
saveUnknown: ['manifest.**', 'proxyConfig.**'],
|
|
93
|
+
timestamps: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const tableOptions = {
|
|
97
|
+
prefix: process.env.DEVELOPER_DYNAMODB_TABLE_PREFIX,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (process.env.NODE_ENV === 'production') {
|
|
101
|
+
tableOptions.create = false;
|
|
102
|
+
tableOptions.waitForActive = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const Connector = dynamoose.model('connectors', connectorSchema, tableOptions);
|
|
106
|
+
|
|
107
|
+
function getDeveloperCipherKey() {
|
|
108
|
+
if (!process.env.DEVELOPER_APP_SERVER_SECRET_KEY) {
|
|
109
|
+
throw new Error('DEVELOPER_APP_SERVER_SECRET_KEY is not defined');
|
|
110
|
+
}
|
|
111
|
+
if (process.env.DEVELOPER_APP_SERVER_SECRET_KEY.length < 32) {
|
|
112
|
+
// pad secret key with spaces if it is less than 32 bytes
|
|
113
|
+
return process.env.DEVELOPER_APP_SERVER_SECRET_KEY.padEnd(32, ' ');
|
|
114
|
+
}
|
|
115
|
+
if (process.env.DEVELOPER_APP_SERVER_SECRET_KEY.length > 32) {
|
|
116
|
+
// truncate secret key if it is more than 32 bytes
|
|
117
|
+
return process.env.DEVELOPER_APP_SERVER_SECRET_KEY.slice(0, 32);
|
|
118
|
+
}
|
|
119
|
+
return process.env.DEVELOPER_APP_SERVER_SECRET_KEY;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function decode(encryptedData) {
|
|
123
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', getDeveloperCipherKey(), Buffer.alloc(16, 0));
|
|
124
|
+
return decipher.update(encryptedData, 'hex', 'utf8') + decipher.final('utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ADD static method to get connector by proxyId
|
|
128
|
+
Connector.getProxyConfig = async (proxyId) => {
|
|
129
|
+
const connectors = await Connector
|
|
130
|
+
.query('proxyId')
|
|
131
|
+
.eq(proxyId)
|
|
132
|
+
.using('proxyIdIndex')
|
|
133
|
+
.exec();
|
|
134
|
+
if (connectors.length > 0) {
|
|
135
|
+
const proxyConfig = connectors[0].proxyConfig;
|
|
136
|
+
const encodedSecretKey = connectors[0].encodedSecretKey;
|
|
137
|
+
const secretKey = encodedSecretKey ? decode(encodedSecretKey) : null;
|
|
138
|
+
if (secretKey) {
|
|
139
|
+
proxyConfig.secretKey = secretKey;
|
|
140
|
+
}
|
|
141
|
+
return proxyConfig;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
exports.Connector = Connector;
|
package/package.json
CHANGED
package/releaseNotes.json
CHANGED
|
@@ -1,4 +1,68 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.7.3": {
|
|
3
|
+
"global": [
|
|
4
|
+
{
|
|
5
|
+
"type": "New",
|
|
6
|
+
"description": "RingSense data logging in server side logging"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "New",
|
|
10
|
+
"description": "New phone setting item to group phone related settings"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "Better",
|
|
14
|
+
"description": "HUD enabled by default"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "Fix",
|
|
18
|
+
"description": "A small issue on contact search feature"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "Fix",
|
|
22
|
+
"description": "A small issue on embedded URLs"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "Fix",
|
|
26
|
+
"description": "Server-side logging logged calls cannot be edited for the first attempt"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"1.7.1": {
|
|
31
|
+
"global": [
|
|
32
|
+
{
|
|
33
|
+
"type": "Better",
|
|
34
|
+
"description": "Click-to-SMS now adds recipients so you can easily add multiple recipients"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "New",
|
|
38
|
+
"description": "Auto log setting for voicemail"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"1.6.11":{
|
|
43
|
+
"global": [
|
|
44
|
+
{
|
|
45
|
+
"type": "Fix",
|
|
46
|
+
"description": "A small issue on contact search feature"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "Fix",
|
|
50
|
+
"description": "A small issue on embedded URLs"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"1.6.10": {
|
|
55
|
+
"global": [
|
|
56
|
+
{
|
|
57
|
+
"type": "Better",
|
|
58
|
+
"description": "Click-to-SMS now adds recipients so you can easily add multiple recipients"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "New",
|
|
62
|
+
"description": "Auto log setting for voicemail"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
2
66
|
"1.6.9": {
|
|
3
67
|
"global": [
|
|
4
68
|
{
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
jest.mock('axios', () => jest.fn());
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
renderTemplateString,
|
|
8
|
+
renderDeep,
|
|
9
|
+
joinUrl,
|
|
10
|
+
performRequest,
|
|
11
|
+
} = require('../../../connector/proxy/engine');
|
|
12
|
+
|
|
13
|
+
describe('proxy engine utilities', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
axios.mockReset();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('renderTemplateString handles full and partial templates', () => {
|
|
19
|
+
const context = { a: { b: 123 }, name: 'Alice' };
|
|
20
|
+
expect(renderTemplateString('{{a.b}}', context)).toBe(123);
|
|
21
|
+
expect(renderTemplateString('Hello {{name}}', context)).toBe('Hello Alice');
|
|
22
|
+
expect(renderTemplateString('Missing {{x.y}} here', context)).toBe('Missing here');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('renderDeep renders nested objects and arrays', () => {
|
|
26
|
+
const context = { id: 42, name: 'Alice', items: ['x', 'y'] };
|
|
27
|
+
const input = {
|
|
28
|
+
url: '/users/{{id}}',
|
|
29
|
+
body: { name: '{{name}}', tags: ['a', '{{items.1}}'] },
|
|
30
|
+
};
|
|
31
|
+
const out = renderDeep(input, context);
|
|
32
|
+
expect(out.url).toBe('/users/42');
|
|
33
|
+
expect(out.body.name).toBe('Alice');
|
|
34
|
+
expect(out.body.tags).toEqual(['a', 'y']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('joinUrl joins base and path and preserves absolute urls', () => {
|
|
38
|
+
expect(joinUrl('https://api.example.com', '/v1/items')).toBe('https://api.example.com/v1/items');
|
|
39
|
+
expect(joinUrl('https://api.example.com/', 'v1/items')).toBe('https://api.example.com/v1/items');
|
|
40
|
+
expect(joinUrl('', 'https://full.example.com/x')).toBe('https://full.example.com/x');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('performRequest composes url, headers, params, body and auth', async () => {
|
|
44
|
+
axios.mockResolvedValue({ data: { ok: true } });
|
|
45
|
+
const config = {
|
|
46
|
+
secretKey: 'shh-key',
|
|
47
|
+
auth: {
|
|
48
|
+
type: 'apiKey',
|
|
49
|
+
scheme: 'Basic',
|
|
50
|
+
credentialTemplate: '{{apiKey}}',
|
|
51
|
+
encode: 'base64',
|
|
52
|
+
headerName: 'Authorization'
|
|
53
|
+
},
|
|
54
|
+
requestDefaults: {
|
|
55
|
+
baseUrl: 'https://api.example.com',
|
|
56
|
+
timeoutSeconds: 10,
|
|
57
|
+
defaultHeaders: { Accept: 'application/json', 'X-Secret-Key': '{{secretKey}}' }
|
|
58
|
+
},
|
|
59
|
+
operations: {
|
|
60
|
+
createThing: {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
url: '/things/{{thingId}}',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
query: { search: '{{q}}' },
|
|
65
|
+
body: { id: '{{thingId}}', name: '{{name}}' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const user = { accessToken: 'token-123' };
|
|
70
|
+
await performRequest({
|
|
71
|
+
config,
|
|
72
|
+
opName: 'createThing',
|
|
73
|
+
inputs: { thingId: 7, name: 'Widget', q: 'alpha' },
|
|
74
|
+
user,
|
|
75
|
+
authHeader: undefined
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(axios).toHaveBeenCalledTimes(1);
|
|
79
|
+
const args = axios.mock.calls[0][0];
|
|
80
|
+
expect(args.url).toBe('https://api.example.com/things/7');
|
|
81
|
+
expect(args.method).toBe('POST');
|
|
82
|
+
expect(args.params).toEqual({ search: 'alpha' });
|
|
83
|
+
expect(args.data).toEqual({ id: 7, name: 'Widget' });
|
|
84
|
+
expect(args.timeout).toBe(10000);
|
|
85
|
+
expect(args.headers.Accept).toBe('application/json');
|
|
86
|
+
expect(args.headers['Content-Type']).toBe('application/json');
|
|
87
|
+
expect(args.headers['X-Secret-Key']).toBe('shh-key');
|
|
88
|
+
// Basic base64('token-123')
|
|
89
|
+
expect(args.headers.Authorization).toMatch(/^Basic /);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
|