@app-connect/core 1.7.18 → 1.7.20

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.
Files changed (60) hide show
  1. package/connector/proxy/index.js +2 -1
  2. package/handlers/auth.js +30 -55
  3. package/handlers/log.js +182 -10
  4. package/handlers/plugin.js +27 -0
  5. package/handlers/user.js +31 -2
  6. package/index.js +115 -22
  7. package/lib/authSession.js +21 -12
  8. package/lib/callLogComposer.js +1 -1
  9. package/lib/debugTracer.js +20 -2
  10. package/lib/util.js +21 -4
  11. package/mcp/README.md +395 -0
  12. package/mcp/mcpHandler.js +318 -82
  13. package/mcp/tools/checkAuthStatus.js +28 -35
  14. package/mcp/tools/createCallLog.js +13 -9
  15. package/mcp/tools/createContact.js +2 -6
  16. package/mcp/tools/doAuth.js +27 -157
  17. package/mcp/tools/findContactByName.js +6 -9
  18. package/mcp/tools/findContactByPhone.js +2 -6
  19. package/mcp/tools/getGoogleFilePicker.js +5 -9
  20. package/mcp/tools/getHelp.js +2 -3
  21. package/mcp/tools/getPublicConnectors.js +55 -24
  22. package/mcp/tools/index.js +11 -36
  23. package/mcp/tools/logout.js +32 -13
  24. package/mcp/tools/rcGetCallLogs.js +3 -20
  25. package/mcp/ui/App/App.tsx +358 -0
  26. package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
  27. package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
  28. package/mcp/ui/App/components/ConnectorList.tsx +82 -0
  29. package/mcp/ui/App/components/DebugPanel.tsx +43 -0
  30. package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
  31. package/mcp/ui/App/lib/callTool.ts +130 -0
  32. package/mcp/ui/App/lib/debugLog.ts +41 -0
  33. package/mcp/ui/App/lib/developerPortal.ts +111 -0
  34. package/mcp/ui/App/main.css +6 -0
  35. package/mcp/ui/App/root.tsx +13 -0
  36. package/mcp/ui/dist/index.html +53 -0
  37. package/mcp/ui/index.html +13 -0
  38. package/mcp/ui/package-lock.json +6356 -0
  39. package/mcp/ui/package.json +25 -0
  40. package/mcp/ui/tsconfig.json +26 -0
  41. package/mcp/ui/vite.config.ts +16 -0
  42. package/models/llmSessionModel.js +14 -0
  43. package/models/userModel.js +3 -0
  44. package/package.json +2 -2
  45. package/releaseNotes.json +24 -0
  46. package/test/handlers/auth.test.js +31 -0
  47. package/test/handlers/plugin.test.js +287 -0
  48. package/test/lib/util.test.js +379 -1
  49. package/test/mcp/tools/createCallLog.test.js +3 -3
  50. package/test/mcp/tools/doAuth.test.js +40 -303
  51. package/test/mcp/tools/findContactByName.test.js +3 -3
  52. package/test/mcp/tools/findContactByPhone.test.js +3 -3
  53. package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
  54. package/test/mcp/tools/getPublicConnectors.test.js +49 -70
  55. package/test/mcp/tools/logout.test.js +17 -11
  56. package/mcp/SupportedPlatforms.md +0 -12
  57. package/mcp/tools/collectAuthInfo.js +0 -91
  58. package/mcp/tools/setConnector.js +0 -69
  59. package/test/mcp/tools/collectAuthInfo.test.js +0 -234
  60. package/test/mcp/tools/setConnector.test.js +0 -177
package/index.js CHANGED
@@ -3,8 +3,11 @@ const cors = require('cors')
3
3
  const bodyParser = require('body-parser');
4
4
  require('body-parser-xml')(bodyParser);
5
5
  const dynamoose = require('dynamoose');
6
+ const Sequelize = require('sequelize');
7
+ const { DynamoDB } = require('@aws-sdk/client-dynamodb');
6
8
  const axios = require('axios');
7
9
  const { UserModel } = require('./models/userModel');
10
+ const { LlmSessionModel } = require('./models/llmSessionModel');
8
11
  const { CallDownListModel } = require('./models/callDownListModel');
9
12
  const { CallLogModel } = require('./models/callLogModel');
10
13
  const { MessageLogModel } = require('./models/messageLogModel');
@@ -29,6 +32,7 @@ const mcpHandler = require('./mcp/mcpHandler');
29
32
  const logger = require('./lib/logger');
30
33
  const { DebugTracer } = require('./lib/debugTracer');
31
34
  const s3ErrorLogReport = require('./lib/s3ErrorLogReport');
35
+ const pluginCore = require('./handlers/plugin');
32
36
  const { handleDatabaseError } = require('./lib/errorHandler');
33
37
  const { updateAuthSession } = require('./lib/authSession');
34
38
 
@@ -42,8 +46,13 @@ catch (e) {
42
46
  }
43
47
 
44
48
  // For using dynamodb in local env
49
+ // AWS SDK v3 requires a region even for local; ddb.local() omits it, so set manually.
45
50
  if (process.env.DYNAMODB_LOCALHOST) {
46
- dynamoose.aws.ddb.local(process.env.DYNAMODB_LOCALHOST);
51
+ dynamoose.aws.ddb.set(new DynamoDB({
52
+ endpoint: process.env.DYNAMODB_LOCALHOST,
53
+ region: 'local',
54
+ credentials: { accessKeyId: 'local', secretAccessKey: 'local' },
55
+ }));
47
56
  }
48
57
  // log axios requests
49
58
  if (process.env.IS_PROD === 'false') {
@@ -58,12 +67,27 @@ async function initDB() {
58
67
  if (!process.env.DISABLE_SYNC_DB_TABLE) {
59
68
  logger.info('creating db tables if not exist...');
60
69
  await UserModel.sync();
70
+ await LlmSessionModel.sync();
61
71
  await CallLogModel.sync();
62
72
  await MessageLogModel.sync();
63
73
  await AdminConfigModel.sync();
64
74
  await CacheModel.sync();
65
75
  await CallDownListModel.sync();
66
76
  await AccountDataModel.sync();
77
+
78
+ // if UserModel doesn't have hashedRcExtensionId column, add it
79
+ const queryInterface = UserModel.sequelize.getQueryInterface();
80
+ const userTableName = UserModel.getTableName();
81
+ const userTableSchema = await queryInterface.describeTable(userTableName);
82
+ if (!userTableSchema.hashedRcExtensionId) {
83
+ logger.info('adding hashedRcExtensionId column to users table...');
84
+ await queryInterface.addColumn(userTableName, 'hashedRcExtensionId', {
85
+ type: Sequelize.STRING,
86
+ allowNull: true,
87
+ });
88
+ await UserModel.sync();
89
+ logger.info('hashedRcExtensionId column added to users table');
90
+ }
67
91
  }
68
92
  }
69
93
 
@@ -756,7 +780,7 @@ function createCoreRouter() {
756
780
  res.status(400).send(tracer ? tracer.wrapResponse('User not found') : 'User not found');
757
781
  return;
758
782
  }
759
- const { userSettings } = await userCore.updateUserSettings({ user, userSettings: req.body.userSettings, platformName });
783
+ const { userSettings } = await userCore.updateUserSettings({ user, userSettings: req.body.userSettings, settingKeysToRemove: req.body.settingKeysToRemove || [], platformName });
760
784
  res.status(200).send(tracer ? tracer.wrapResponse({ userSettings }) : { userSettings });
761
785
  success = true;
762
786
  }
@@ -861,6 +885,7 @@ function createCoreRouter() {
861
885
  tokenUrl,
862
886
  query: req.query,
863
887
  proxyId: req.query.proxyId,
888
+ hashedRcExtensionId: hashedExtensionId,
864
889
  isFromMCP
865
890
  });
866
891
  if (userInfo) {
@@ -947,7 +972,7 @@ function createCoreRouter() {
947
972
  res.status(400).send(tracer ? tracer.wrapResponse('Missing api key') : 'Missing api key');
948
973
  return;
949
974
  }
950
- const { userInfo, returnMessage } = await authCore.onApiKeyLogin({ platform, hostname, apiKey, proxyId, additionalInfo });
975
+ const { userInfo, returnMessage } = await authCore.onApiKeyLogin({ platform, hostname, apiKey, proxyId, rcAccountId: query.rcAccountId, hashedRcExtensionId: hashedExtensionId, additionalInfo });
951
976
  if (userInfo) {
952
977
  const jwtToken = jwt.generateJwt({
953
978
  id: userInfo.id.toString(),
@@ -1323,7 +1348,7 @@ function createCoreRouter() {
1323
1348
  }
1324
1349
  const { id: userId, platform } = decodedToken;
1325
1350
  platformName = platform;
1326
- const { successful, logId, returnMessage, extraDataTracking, isRevokeUserSession } = await logCore.createCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1351
+ const { successful, logId, returnMessage, extraDataTracking, pluginAsyncTaskIds, isRevokeUserSession } = await logCore.createCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.logInfo?.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1327
1352
  if (isRevokeUserSession) {
1328
1353
  res.status(401).send(tracer ? tracer.wrapResponse({ successful, returnMessage }) : { successful, returnMessage });
1329
1354
  success = false;
@@ -1332,7 +1357,7 @@ function createCoreRouter() {
1332
1357
  if (extraDataTracking) {
1333
1358
  extraData = extraDataTracking;
1334
1359
  }
1335
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage }) : { successful, logId, returnMessage });
1360
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, returnMessage, pluginAsyncTaskIds }) : { successful, logId, returnMessage, pluginAsyncTaskIds });
1336
1361
  success = true;
1337
1362
  }
1338
1363
  }
@@ -1386,11 +1411,11 @@ function createCoreRouter() {
1386
1411
  }
1387
1412
  const { id: userId, platform } = decodedToken;
1388
1413
  platformName = platform;
1389
- const { successful, logId, updatedNote, returnMessage, extraDataTracking } = await logCore.updateCallLog({ platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1414
+ const { successful, logId, updatedNote, returnMessage, extraDataTracking, pluginAsyncTaskIds } = await logCore.updateCallLog({ jwtToken, platform, userId, incomingData: req.body, hashedAccountId: hashedAccountId ?? util.getHashValue(req.body.accountId, process.env.HASH_KEY), isFromSSCL: userAgent === 'SSCL' });
1390
1415
  if (extraDataTracking) {
1391
1416
  extraData = extraDataTracking;
1392
1417
  }
1393
- res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, updatedNote, returnMessage }) : { successful, logId, updatedNote, returnMessage });
1418
+ res.status(200).send(tracer ? tracer.wrapResponse({ successful, logId, updatedNote, returnMessage, pluginAsyncTaskIds }) : { successful, logId, updatedNote, returnMessage, pluginAsyncTaskIds });
1394
1419
  success = true;
1395
1420
  }
1396
1421
  else {
@@ -1941,6 +1966,55 @@ function createCoreRouter() {
1941
1966
  });
1942
1967
  });
1943
1968
 
1969
+ router.post('/pluginAsyncTask', async function (req, res) {
1970
+ const requestStartTime = new Date().getTime();
1971
+ const tracer = req.headers['is-debug'] === 'true' ? DebugTracer.fromRequest(req) : null;
1972
+ tracer?.trace('pluginAsyncTask:start', { query: req.query });
1973
+ let platformName = null;
1974
+ let success = false;
1975
+ const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
1976
+ const { jwtToken } = req.query;
1977
+ try {
1978
+ if (!jwtToken) {
1979
+ tracer?.trace('pluginAsyncTask:noToken', {});
1980
+ res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
1981
+ return;
1982
+ }
1983
+ const unAuthData = jwt.decodeJwt(jwtToken);
1984
+ const user = await UserModel.findByPk(unAuthData?.id);
1985
+ if (!user) {
1986
+ tracer?.trace('pluginAsyncTask:userNotFound', {});
1987
+ res.status(400).send(tracer ? tracer.wrapResponse('User not found') : 'User not found');
1988
+ return;
1989
+ }
1990
+ const { asyncTaskIds } = req.body;
1991
+ const filteredTasksIds = asyncTaskIds.filter(taskId => taskId.startsWith(user.id));
1992
+ const tasks = await pluginCore.getPluginAsyncTasks({ asyncTaskIds: filteredTasksIds });
1993
+ res.status(200).send(tracer ? tracer.wrapResponse({ tasks }) : { tasks });
1994
+ success = true;
1995
+ }
1996
+ catch (e) {
1997
+ console.log(`platform: ${platformName} \n${e.stack}`);
1998
+ res.status(400).send(tracer ? tracer.wrapResponse({ error: e.message || e }) : { error: e.message || e });
1999
+ tracer?.traceError('pluginAsyncTask:error', e, { platform: platformName });
2000
+ success = false;
2001
+ }
2002
+ const requestEndTime = new Date().getTime();
2003
+ analytics.track({
2004
+ eventName: 'Plugin Async Task',
2005
+ interfaceName: 'pluginAsyncTask',
2006
+ connectorName: platformName,
2007
+ accountId: hashedAccountId,
2008
+ extensionId: hashedExtensionId,
2009
+ success,
2010
+ requestDuration: (requestEndTime - requestStartTime) / 1000,
2011
+ userAgent,
2012
+ ip,
2013
+ author,
2014
+ eventAddedVia
2015
+ });
2016
+ });
2017
+
1944
2018
  if (process.env.IS_PROD === 'false') {
1945
2019
  router.post('/registerMockUser', async function (req, res) {
1946
2020
  const secretKey = req.query.secretKey;
@@ -2057,24 +2131,32 @@ function createCoreRouter() {
2057
2131
  });
2058
2132
  });
2059
2133
 
2060
- router.use('/mcp', (req, res, next) => {// LOG EVERYTHING
2061
- console.log(`[${req.method}] /mcp`);
2062
- console.log("Headers:", JSON.stringify(req.headers['authorization'] ? "Auth Token Present" : "No Auth"));
2063
- console.log("Body:", JSON.stringify(req.body));
2064
- // return next();
2065
- // Capture the response finish to see the status code
2066
- res.on('finish', () => {
2067
- console.log(`[Response] Status: ${res.statusCode}`);
2068
- console.log(`[Response] data: ${JSON.stringify(res.data)}`);
2069
- });
2134
+ router.use('/mcp', (req, res, next) => {
2135
+ // Widget tool calls are unauthenticated — they come from the iframe
2136
+ // which has no access to the RC bearer token.
2137
+ if (req.path === '/widget-tool-call') {
2138
+ return next();
2139
+ }
2070
2140
 
2071
2141
  const authHeader = req.headers.authorization;
2072
2142
  const token = authHeader?.split(' ')[1]; // Remove "Bearer "
2073
- // Allow the initial connection (GET) and CORS checks (OPTIONS) to pass freely.
2074
- // We only want to block the actual commands (POST).
2143
+ // Allow GET and OPTIONS (CORS preflight) to pass freely.
2075
2144
  if (req.method === 'GET' || req.method === 'OPTIONS') {
2076
2145
  return next();
2077
2146
  }
2147
+ // Allow MCP discovery/handshake methods — these carry no user data and must be
2148
+ // reachable without auth so the ChatGPT developer portal can scan tools.
2149
+ const mcpMethod = req.body?.method;
2150
+ const UNAUTHENTICATED_MCP_METHODS = new Set([
2151
+ 'initialize',
2152
+ 'tools/list',
2153
+ 'ping',
2154
+ 'notifications/initialized',
2155
+ 'notifications/cancelled',
2156
+ ]);
2157
+ if (mcpMethod && UNAUTHENTICATED_MCP_METHODS.has(mcpMethod)) {
2158
+ return next();
2159
+ }
2078
2160
  // SCENARIO 1: No Token provided. Kick off the OAuth flow.
2079
2161
  if (!token) {
2080
2162
  res.setHeader('WWW-Authenticate', `Bearer realm="mcp", resource_metadata="${process.env.APP_SERVER}/.well-known/oauth-protected-resource"`);
@@ -2084,9 +2166,7 @@ function createCoreRouter() {
2084
2166
  // SCENARIO 2: Token provided. Verify it.
2085
2167
  try {
2086
2168
  next();
2087
- } catch (error) {
2088
- console.error("Token validation failed:", error.message);
2089
- // Token is invalid or expired
2169
+ } catch {
2090
2170
  res.setHeader('WWW-Authenticate', `Bearer realm="mcp", resource_metadata="${process.env.APP_SERVER}/.well-known/oauth-protected-resource"`);
2091
2171
  return res.status(401).send();
2092
2172
  }
@@ -2109,6 +2189,19 @@ function createCoreRouter() {
2109
2189
  await mcpHandler.handleMcpRequest(req, res);
2110
2190
  });
2111
2191
 
2192
+ // Lightweight endpoint for widget tool calls (bypasses MCP protocol)
2193
+ router.options('/mcp/widget-tool-call', (req, res) => {
2194
+ res.setHeader('Access-Control-Allow-Origin', '*');
2195
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
2196
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
2197
+ res.status(200).end();
2198
+ });
2199
+ router.post('/mcp/widget-tool-call', async (req, res) => {
2200
+ res.setHeader('Access-Control-Allow-Origin', '*');
2201
+ res.setHeader('Content-Type', 'application/json');
2202
+ await mcpHandler.handleWidgetToolCall(req, res);
2203
+ });
2204
+
2112
2205
  return router;
2113
2206
  }
2114
2207
 
@@ -10,20 +10,29 @@ const AUTH_SESSION_PREFIX = 'auth-session';
10
10
  const SESSION_EXPIRY_MINUTES = 5;
11
11
 
12
12
  /**
13
- * Create a new auth session
13
+ * Create (or reset) an auth session.
14
+ * If a record already exists for the sessionId (e.g., user retries auth within
15
+ * the same ChatGPT conversation), it is reset to 'pending' so polling works
16
+ * correctly for the new attempt.
14
17
  */
15
18
  async function createAuthSession(sessionId, data) {
16
- await CacheModel.create({
17
- id: `${AUTH_SESSION_PREFIX}-${sessionId}`,
18
- cacheKey: AUTH_SESSION_PREFIX,
19
- userId: sessionId,
20
- status: 'pending',
21
- data: {
22
- ...data,
23
- createdAt: new Date().toISOString()
24
- },
25
- expiry: new Date(Date.now() + SESSION_EXPIRY_MINUTES * 60 * 1000)
26
- });
19
+ const id = `${AUTH_SESSION_PREFIX}-${sessionId}`;
20
+ const expiry = new Date(Date.now() + SESSION_EXPIRY_MINUTES * 60 * 1000);
21
+ const sessionData = { ...data, createdAt: new Date().toISOString() };
22
+
23
+ const existing = await CacheModel.findByPk(id);
24
+ if (existing) {
25
+ await existing.update({ status: 'pending', data: sessionData, expiry });
26
+ } else {
27
+ await CacheModel.create({
28
+ id,
29
+ cacheKey: AUTH_SESSION_PREFIX,
30
+ userId: sessionId,
31
+ status: 'pending',
32
+ data: sessionData,
33
+ expiry,
34
+ });
35
+ }
27
36
  }
28
37
 
29
38
  /**
@@ -115,7 +115,7 @@ function composeCallLog(params) {
115
115
  body = upsertCallRecording({ body, recordingLink, logFormat });
116
116
  }
117
117
 
118
- if (aiNote && (userSettings?.addCallLogAINote?.value ?? true)) {
118
+ if (aiNote && (userSettings?.addCallLogAiNote?.value ?? true)) {
119
119
  body = upsertAiNote({ body, aiNote, logFormat });
120
120
  }
121
121
 
@@ -40,7 +40,7 @@ class DebugTracer {
40
40
  */
41
41
  trace(methodName, data = {}, options = {}) {
42
42
  const { includeStack = true, level = 'info' } = options;
43
-
43
+
44
44
  const traceEntry = {
45
45
  timestamp: new Date().toISOString(),
46
46
  elapsed: Date.now() - this.startTime,
@@ -85,7 +85,7 @@ class DebugTracer {
85
85
  }
86
86
 
87
87
  const sensitiveFields = [
88
- 'accessToken', 'refreshToken', 'apiKey', 'password',
88
+ 'accessToken', 'refreshToken', 'apiKey', 'password',
89
89
  'secret', 'token', 'authorization', 'auth', 'key',
90
90
  'credential', 'credentials', 'privateKey', 'clientSecret'
91
91
  ];
@@ -115,12 +115,30 @@ class DebugTracer {
115
115
  return sanitizeRecursive(sanitized);
116
116
  }
117
117
 
118
+ /**
119
+ * Builds a compact summary of all recorded actions, one entry per trace.
120
+ * Each entry contains the method name, log level, and elapsed time at the
121
+ * point the trace was recorded, making it easy to skim what happened without
122
+ * reading the full trace list.
123
+ * @returns {string[]} Array of human-readable action summary strings
124
+ */
125
+ _buildActionSummary() {
126
+ return this.traces.map((t, i) => ({
127
+ index: i + 1,
128
+ timestamp: t.timestamp,
129
+ level: t.level.toUpperCase(),
130
+ method: t.methodName,
131
+ elapsedMs: t.elapsed
132
+ }));
133
+ }
134
+
118
135
  /**
119
136
  * Gets the complete trace data for inclusion in response
120
137
  * @returns {Object} Trace data object
121
138
  */
122
139
  getTraceData() {
123
140
  return {
141
+ sum: this._buildActionSummary(),
124
142
  requestId: this.requestId,
125
143
  totalDuration: `${Date.now() - this.startTime}ms`,
126
144
  traceCount: this.traces.length,
package/lib/util.js CHANGED
@@ -40,8 +40,7 @@ function secondsToHoursMinutesSeconds(seconds) {
40
40
  function getMostRecentDate({ allDateValues }) {
41
41
  var result = 0;
42
42
  for (const date of allDateValues) {
43
- if(!date)
44
- {
43
+ if (!date) {
45
44
  continue;
46
45
  }
47
46
  if (date > result) {
@@ -53,16 +52,34 @@ function getMostRecentDate({ allDateValues }) {
53
52
 
54
53
  // media reader link: https://ringcentral.github.io/ringcentral-media-reader/?media=https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
55
54
  // platform media link: https://media.ringcentral.com/restapi/v1.0/account/{accountId}/extension/{extensionId}/message-store/{messageId}/content/{contentId}
56
- function getMediaReaderLinkByPlatformMediaLink(platformMediaLink){
57
- if(!platformMediaLink){
55
+ function getMediaReaderLinkByPlatformMediaLink(platformMediaLink) {
56
+ if (!platformMediaLink) {
58
57
  return null;
59
58
  }
60
59
  const encodedPlatformMediaLink = encodeURIComponent(platformMediaLink);
61
60
  return `https://ringcentral.github.io/ringcentral-media-reader/?media=${encodedPlatformMediaLink}`;
62
61
  }
63
62
 
63
+ function getPluginsFromUserSettings({ userSettings, logType }) {
64
+ const result = [];
65
+ if (!userSettings) {
66
+ return result;
67
+ }
68
+ for (const userSettingKey in userSettings) {
69
+ if (!userSettingKey.startsWith('plugin_')) {
70
+ continue;
71
+ }
72
+ const pluginUserSetting = userSettings[userSettingKey];
73
+ if (pluginUserSetting.value.logTypes.includes(logType)) {
74
+ result.push({ id: userSettingKey.replace('plugin_', ''), value: pluginUserSetting.value });
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+
64
80
  exports.getTimeZone = getTimeZone;
65
81
  exports.getHashValue = getHashValue;
66
82
  exports.secondsToHoursMinutesSeconds = secondsToHoursMinutesSeconds;
67
83
  exports.getMostRecentDate = getMostRecentDate;
68
84
  exports.getMediaReaderLinkByPlatformMediaLink = getMediaReaderLinkByPlatformMediaLink;
85
+ exports.getPluginsFromUserSettings = getPluginsFromUserSettings;