@datanimbus/dnio-mcp 1.0.0

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 (59) hide show
  1. package/Dockerfile +20 -0
  2. package/docs/README.md +35 -0
  3. package/docs/architecture.md +171 -0
  4. package/docs/authentication.md +74 -0
  5. package/docs/tools/apps.md +59 -0
  6. package/docs/tools/connectors.md +76 -0
  7. package/docs/tools/data-pipes.md +286 -0
  8. package/docs/tools/data-services.md +105 -0
  9. package/docs/tools/deployment-groups.md +152 -0
  10. package/docs/tools/plugins.md +94 -0
  11. package/docs/tools/records.md +97 -0
  12. package/docs/workflows.md +195 -0
  13. package/env.example +16 -0
  14. package/package.json +43 -0
  15. package/readme.md +144 -0
  16. package/src/clients/api-keys.js +10 -0
  17. package/src/clients/apps.js +13 -0
  18. package/src/clients/base-client.js +78 -0
  19. package/src/clients/bots.js +10 -0
  20. package/src/clients/connectors.js +30 -0
  21. package/src/clients/data-formats.js +40 -0
  22. package/src/clients/data-pipes.js +33 -0
  23. package/src/clients/deployment-groups.js +59 -0
  24. package/src/clients/formulas.js +10 -0
  25. package/src/clients/functions.js +10 -0
  26. package/src/clients/plugins.js +39 -0
  27. package/src/clients/records.js +51 -0
  28. package/src/clients/services.js +63 -0
  29. package/src/clients/user-groups.js +10 -0
  30. package/src/clients/users.js +10 -0
  31. package/src/examples/ai-sdk-client.js +165 -0
  32. package/src/examples/claude_desktop_config.json +34 -0
  33. package/src/examples/express-integration.js +181 -0
  34. package/src/index.js +283 -0
  35. package/src/schemas/schema-converter.js +179 -0
  36. package/src/services/auth-manager.js +277 -0
  37. package/src/services/dnio-client.js +40 -0
  38. package/src/services/service-registry.js +150 -0
  39. package/src/services/session-manager.js +161 -0
  40. package/src/stdio-bridge.js +185 -0
  41. package/src/tools/_helpers.js +32 -0
  42. package/src/tools/api-keys.js +5 -0
  43. package/src/tools/apps.js +185 -0
  44. package/src/tools/bots.js +5 -0
  45. package/src/tools/connectors.js +165 -0
  46. package/src/tools/data-formats.js +806 -0
  47. package/src/tools/data-pipes.js +1305 -0
  48. package/src/tools/data-service-registry.js +500 -0
  49. package/src/tools/deployment-groups.js +511 -0
  50. package/src/tools/formulas.js +5 -0
  51. package/src/tools/functions.js +5 -0
  52. package/src/tools/mcp-tools-registry.js +38 -0
  53. package/src/tools/plugins.js +250 -0
  54. package/src/tools/records.js +217 -0
  55. package/src/tools/services.js +476 -0
  56. package/src/tools/user-groups.js +5 -0
  57. package/src/tools/users.js +5 -0
  58. package/src/utils/constants.js +135 -0
  59. package/src/utils/logger.js +63 -0
@@ -0,0 +1,277 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../utils/logger');
4
+
5
+ const MCP_USER_EMAIL = process.env.MCP_USER_EMAIL;
6
+ const MCP_USER_PASSWORD = process.env.MCP_USER_PASSWORD;
7
+
8
+ if (!MCP_USER_EMAIL || !MCP_USER_PASSWORD) {
9
+ logger.error(`No DNIO MCP User Found, plz create the user in dnio and set env MCP_USER_EMAIL & MCP_USER_PASSWORD`);
10
+ process.exit(1);
11
+ } else {
12
+ logger.info(`User found ${MCP_USER_EMAIL}`);
13
+ }
14
+
15
+ // Refresh 2 minutes before expiry
16
+ const REFRESH_BUFFER_MS = 2 * 60 * 1000;
17
+
18
+ class AuthManager {
19
+
20
+ // Constructor
21
+ constructor(dnioClient, config) {
22
+ this.client = dnioClient;
23
+ this.config = config;
24
+
25
+ this.adminToken = null;
26
+ this.adminExpiresAt = 0;
27
+
28
+ this.userToken = null;
29
+ this.userExpiresAt = 0;
30
+
31
+ this._adminRefreshTimer = null;
32
+ this._userRefreshTimer = null;
33
+ }
34
+
35
+ // Initialize full auth flow
36
+ async initialize() {
37
+ logger.info('AuthManager: Starting initialization...');
38
+
39
+ await this.refreshAdminToken();
40
+ logger.info('AuthManager: Admin authenticated');
41
+
42
+ await this._ensureMCPUser();
43
+ logger.info('AuthManager: MCP user ready');
44
+
45
+ await this._addUserToAllApps();
46
+ logger.info('AuthManager: MCP user added to all apps');
47
+
48
+ await this.refreshUserToken();
49
+ logger.info('AuthManager: MCP user authenticated');
50
+
51
+ this._scheduleRefresh();
52
+ }
53
+
54
+ // Get valid admin token
55
+ async getAdminToken() {
56
+ if (this._isExpired(this.adminExpiresAt)) {
57
+ await this.refreshAdminToken();
58
+ }
59
+ return this.adminToken;
60
+ }
61
+
62
+ // Get valid MCP user token
63
+ async getUserToken() {
64
+ if (this._isExpired(this.userExpiresAt)) {
65
+ await this.refreshUserToken();
66
+ }
67
+ return this.userToken;
68
+ }
69
+
70
+ // Get MCP user credentials
71
+ getMcpUserCredentials() {
72
+ return {
73
+ email: MCP_USER_EMAIL,
74
+ password: MCP_USER_PASSWORD,
75
+ };
76
+ }
77
+
78
+ // Refresh admin token
79
+ async refreshAdminToken() {
80
+ try {
81
+ const result = await this._login(this.config.username, this.config.password);
82
+ this.adminToken = result.token;
83
+ this.adminExpiresAt = result.expiresAt;
84
+
85
+ logger.info('AuthManager: Admin token refreshed', {
86
+ expiresAt: new Date(this.adminExpiresAt).toISOString()
87
+ });
88
+ } catch (error) {
89
+ logger.error('AuthManager: Admin token refresh failed', {error: error.message});
90
+ throw new Error(`Admin authentication failed: ${error.message}`);
91
+ }
92
+ }
93
+
94
+ // Refresh MCP user token
95
+ async refreshUserToken() {
96
+ try {
97
+ const result = await this._login(MCP_USER_EMAIL, MCP_USER_PASSWORD);
98
+ this.userToken = result.token;
99
+ this.userExpiresAt = result.expiresAt;
100
+
101
+ logger.info('AuthManager: User token refreshed', {
102
+ expiresAt: new Date(this.userExpiresAt).toISOString()
103
+ });
104
+ } catch (error) {
105
+ logger.error('AuthManager: User token refresh failed', {error: error.message});
106
+ throw new Error(`MCP user authentication failed: ${error.message}`);
107
+ }
108
+ }
109
+
110
+ // Ensure MCP user has access to app
111
+ async ensureUserAppAccess(appName) {
112
+ try {
113
+ const token = await this.getAdminToken();
114
+ this.client.setToken(token);
115
+
116
+ await this.client.put(
117
+ `api/a/rbac/admin/user/utils/addToApps/${MCP_USER_EMAIL}`,
118
+ {apps: [appName]}
119
+ );
120
+
121
+ logger.info(`AuthManager: MCP user added to app '${appName}'`);
122
+
123
+ await this.refreshUserToken();
124
+ } catch (error) {
125
+ logger.warn(`AuthManager: addToApps for '${appName}': ${error.message}`);
126
+ }
127
+ }
128
+
129
+ // Cleanup timers
130
+ destroy() {
131
+ if (this._adminRefreshTimer) clearInterval(this._adminRefreshTimer);
132
+ if (this._userRefreshTimer) clearInterval(this._userRefreshTimer);
133
+ }
134
+
135
+ // Internal login
136
+ async _login(username, password) {
137
+ const prevToken = this.client.token;
138
+ this.client.setToken(null);
139
+
140
+ try {
141
+ const response = await this.client.post('api/a/rbac/auth/login', {
142
+ username,
143
+ password
144
+ });
145
+
146
+ const token = response.token;
147
+
148
+ let expiresAt;
149
+ if (response.expiresIn) {
150
+ const parsed = new Date(response.expiresIn).getTime();
151
+ expiresAt = isNaN(parsed)
152
+ ? Date.now() + (this.config.tokenTTL || 1800) * 1000
153
+ : parsed;
154
+ } else {
155
+ expiresAt = Date.now() + (this.config.tokenTTL || 1800) * 1000;
156
+ }
157
+
158
+ return {token, expiresAt};
159
+ } finally {
160
+ if (prevToken) this.client.setToken(prevToken);
161
+ }
162
+ }
163
+
164
+ // Ensure MCP user exists
165
+ async _ensureMCPUser() {
166
+ this.client.setToken(this.adminToken);
167
+
168
+ const filter = JSON.stringify({_id: MCP_USER_EMAIL});
169
+ const users = await this.client.get(
170
+ `api/a/rbac/admin/user?filter=${encodeURIComponent(filter)}`
171
+ );
172
+
173
+ if (users && users.length > 0) {
174
+ logger.info('AuthManager: MCP user already exists');
175
+ return users[0];
176
+ }
177
+
178
+ logger.info('AuthManager: Creating MCP user...');
179
+
180
+ const namespace = this.config.namespace || 'DNIO';
181
+
182
+ const userData = {
183
+ user: {
184
+ auth: {authType: 'local'},
185
+ _id: MCP_USER_EMAIL,
186
+ username: MCP_USER_EMAIL,
187
+ password: MCP_USER_PASSWORD,
188
+ cpassword: MCP_USER_PASSWORD,
189
+ isSuperAdmin: false,
190
+ attributes: {},
191
+ basicDetails: {
192
+ name: 'MCP User',
193
+ phone: null,
194
+ alternateEmail: null,
195
+ description: 'Auto-created service account for DNIO MCP Server'
196
+ },
197
+ accessControl: {
198
+ accessLevel: 'Selected',
199
+ apps: [{_id: namespace, type: 'Management'}]
200
+ },
201
+ roles: null
202
+ },
203
+ groups: []
204
+ };
205
+
206
+ const created = await this.client.post(
207
+ `api/a/rbac/${namespace}/user`,
208
+ userData
209
+ );
210
+
211
+ logger.info('AuthManager: MCP user created');
212
+ return created;
213
+ }
214
+
215
+ // Add MCP user to all apps
216
+ async _addUserToAllApps() {
217
+ this.client.setToken(this.adminToken);
218
+
219
+ try {
220
+ const apps = await this.client.get(
221
+ 'api/a/rbac/admin/app?count=-1&select=_id'
222
+ );
223
+
224
+ if (!apps || !Array.isArray(apps)) {
225
+ logger.warn('AuthManager: No apps found');
226
+ return;
227
+ }
228
+
229
+ const appIds = apps.map(a => a._id);
230
+ if (appIds.length === 0) return;
231
+
232
+ await this.client.put(
233
+ `api/a/rbac/admin/user/utils/addToApps/${MCP_USER_EMAIL}`,
234
+ {apps: appIds}
235
+ );
236
+
237
+ logger.info(`AuthManager: MCP user added to ${appIds.length} apps`);
238
+ } catch (error) {
239
+ logger.warn('AuthManager: Failed to add user to all apps', {
240
+ error: error.message
241
+ });
242
+ }
243
+ }
244
+
245
+ // Check expiry
246
+ _isExpired(expiresAt) {
247
+ return Date.now() >= (expiresAt - REFRESH_BUFFER_MS);
248
+ }
249
+
250
+ // Schedule periodic refresh
251
+ _scheduleRefresh() {
252
+ const ttlMs = (this.config.tokenTTL || 1800) * 1000;
253
+ const interval = Math.max(ttlMs * 0.75, 60000);
254
+
255
+ this._adminRefreshTimer = setInterval(async () => {
256
+ try {
257
+ await this.refreshAdminToken();
258
+ } catch (err) {
259
+ logger.error('Scheduled admin refresh failed', {error: err.message});
260
+ }
261
+ }, interval);
262
+
263
+ this._userRefreshTimer = setInterval(async () => {
264
+ try {
265
+ await this.refreshUserToken();
266
+ } catch (err) {
267
+ logger.error('Scheduled user refresh failed', {error: err.message});
268
+ }
269
+ }, interval);
270
+
271
+ logger.info(
272
+ `AuthManager: Token refresh scheduled every ${Math.round(interval / 1000)}s`
273
+ );
274
+ }
275
+ }
276
+
277
+ module.exports = AuthManager;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const BaseClient = require('../clients/base-client');
4
+
5
+ const AppsClient = require('../clients/apps');
6
+ const ServicesClient = require('../clients/services');
7
+ const RecordsClient = require('../clients/records');
8
+ const ConnectorsClient = require('../clients/connectors');
9
+ const DataPipesClient = require('../clients/data-pipes');
10
+ const PluginsClient = require('../clients/plugins');
11
+ const FunctionsClient = require('../clients/functions');
12
+ const DataFormatsClient = require('../clients/data-formats');
13
+ const FormulasClient = require('../clients/formulas');
14
+ const UsersClient = require('../clients/users');
15
+ const UserGroupsClient = require('../clients/user-groups');
16
+ const ApiKeysClient = require('../clients/api-keys');
17
+ const BotsClient = require('../clients/bots');
18
+ const DeploymentGroupsClient = require('../clients/deployment-groups');
19
+
20
+ class DNIOClient extends BaseClient {
21
+ constructor(opts) {
22
+ super(opts);
23
+ this.apps = new AppsClient(this);
24
+ this.services = new ServicesClient(this);
25
+ this.records = new RecordsClient(this);
26
+ this.connectors = new ConnectorsClient(this);
27
+ this.dataPipes = new DataPipesClient(this);
28
+ this.plugins = new PluginsClient(this);
29
+ this.functions = new FunctionsClient(this);
30
+ this.dataFormats = new DataFormatsClient(this);
31
+ this.formulas = new FormulasClient(this);
32
+ this.users = new UsersClient(this);
33
+ this.userGroups = new UserGroupsClient(this);
34
+ this.apiKeys = new ApiKeysClient(this);
35
+ this.bots = new BotsClient(this);
36
+ this.deploymentGroups = new DeploymentGroupsClient(this);
37
+ }
38
+ }
39
+
40
+ module.exports = DNIOClient;
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../utils/logger');
4
+ const {schemaToDescription} = require('../schemas/schema-converter');
5
+
6
+ class ServiceRegistry {
7
+
8
+ // Constructor
9
+ constructor(dnioClient) {
10
+ this.client = dnioClient;
11
+
12
+ // Currently selected app
13
+ this.selectedApp = null;
14
+
15
+ // Map<serviceId, ServiceEntry>
16
+ this.services = new Map();
17
+
18
+ // Quick lookup: toolPrefix → serviceId
19
+ this.prefixToServiceId = new Map();
20
+
21
+ // Quick lookup: serviceName (lowercase) → serviceId
22
+ this.nameToServiceId = new Map();
23
+ }
24
+
25
+ // Load all active services for an app
26
+ async loadApp(appName, adminToken, userToken) {
27
+
28
+ // Clear previous state
29
+ this.services.clear();
30
+ this.prefixToServiceId.clear();
31
+ this.nameToServiceId.clear();
32
+ this.selectedApp = appName;
33
+
34
+ // List services with admin token
35
+ this.client.setToken(adminToken);
36
+ const services = await this.client.services.list(appName, {
37
+ filter: {status: 'Active'},
38
+ select: 'name,api,_id,attributeCount,status,definition,description'
39
+ });
40
+ const serviceList = Array.isArray(services) ? services : [];
41
+ logger.info(`Found ${serviceList.length} active data services for app '${appName}'`);
42
+
43
+ const loaded = [];
44
+ const skipped = [];
45
+
46
+ for (const svc of serviceList) {
47
+ const {_id: serviceId, name, api, definition, description: svcDescription} = svc;
48
+ const servicePath = (api || `/${name}`).replace(/^\//, '');
49
+
50
+ if (!servicePath) {
51
+ skipped.push({name, serviceId, reason: 'no API path defined'});
52
+ continue;
53
+ }
54
+
55
+ // Switch to user token for pod verification
56
+ this.client.setToken(userToken);
57
+
58
+ // Fetch full schema using admin token
59
+ this.client.setToken(adminToken);
60
+ let fullDefinition = definition;
61
+
62
+ if (!fullDefinition || fullDefinition.length === 0) {
63
+ try {
64
+ const fullSchema = await this.client.services.getSchema(appName, serviceId);
65
+ fullDefinition = fullSchema.definition || [];
66
+ } catch (err) {
67
+ logger.warn(`Failed to fetch schema for ${name} (${serviceId}): ${err.message}`);
68
+ fullDefinition = [];
69
+ }
70
+ }
71
+
72
+ const schemaDesc = fullDefinition.length > 0
73
+ ? schemaToDescription(fullDefinition)
74
+ : 'Schema not available';
75
+
76
+ const toolPrefix = this._sanitizeToolName(name);
77
+
78
+ const entry = {
79
+ serviceId,
80
+ name,
81
+ servicePath,
82
+ toolPrefix,
83
+ description: svcDescription || `Data service '${name}'`,
84
+ definition: fullDefinition,
85
+ schemaDesc,
86
+ };
87
+
88
+ this.services.set(serviceId, entry);
89
+ this.prefixToServiceId.set(toolPrefix, serviceId);
90
+ this.nameToServiceId.set(name.toLowerCase(), serviceId);
91
+
92
+ loaded.push({name, serviceId, toolPrefix, apiPath: `/${servicePath}`});
93
+ logger.info(`Loaded service: ${name} (${serviceId}) → ${toolPrefix}`);
94
+ }
95
+
96
+ // Restore user token
97
+ this.client.setToken(userToken);
98
+
99
+ logger.info(`Loaded ${loaded.length} services, skipped ${skipped.length}`);
100
+ return {loaded, skipped};
101
+ }
102
+
103
+ // Resolve service by name, prefix, or id
104
+ resolveService(identifier) {
105
+ if (!identifier) return null;
106
+
107
+ const id = identifier.trim();
108
+
109
+ if (this.services.has(id)) return this.services.get(id);
110
+
111
+ const byPrefix = this.prefixToServiceId.get(id.toLowerCase());
112
+ if (byPrefix) return this.services.get(byPrefix);
113
+
114
+ const byName = this.nameToServiceId.get(id.toLowerCase());
115
+ if (byName) return this.services.get(byName);
116
+
117
+ const sanitized = this._sanitizeToolName(id);
118
+ const byFuzzy = this.prefixToServiceId.get(sanitized);
119
+ if (byFuzzy) return this.services.get(byFuzzy);
120
+
121
+ return null;
122
+ }
123
+
124
+ // Get list of loaded services
125
+ getServiceList() {
126
+ const result = [];
127
+
128
+ for (const [, entry] of this.services) {
129
+ result.push({
130
+ serviceId: entry.serviceId,
131
+ name: entry.name,
132
+ toolPrefix: entry.toolPrefix,
133
+ apiPath: `/${entry.servicePath}`,
134
+ schemaDesc: entry.schemaDesc,
135
+ });
136
+ }
137
+
138
+ return result;
139
+ }
140
+
141
+ // Sanitize tool name
142
+ _sanitizeToolName(name) {
143
+ return name
144
+ .toLowerCase()
145
+ .replace(/[^a-z0-9]+/g, '_')
146
+ .replace(/^_|_$/g, '');
147
+ }
148
+ }
149
+
150
+ module.exports = ServiceRegistry;
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const logger = require('../utils/logger');
5
+
6
+ // Default session TTL: 2 hours
7
+ const SESSION_TTL_MS = parseInt(process.env.SESSION_TTL || '7200', 10) * 1000;
8
+
9
+ // Cleanup interval: every 5 minutes
10
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
11
+
12
+ class SessionManager {
13
+
14
+ // Constructor
15
+ constructor() {
16
+ // Map<sessionId, UserSession>
17
+ this.sessions = new Map();
18
+
19
+ // userEmail → sessionId (for reuse)
20
+ this.userSessionMap = new Map();
21
+
22
+ // Periodic cleanup of expired sessions
23
+ this._cleanupTimer = setInterval(() => this._cleanup(), CLEANUP_INTERVAL_MS);
24
+ }
25
+
26
+ // Get session by ID
27
+ getSession(sessionId) {
28
+ const session = this.sessions.get(sessionId);
29
+ if (!session) return null;
30
+
31
+ if (this._isExpired(session)) {
32
+ this.destroySession(sessionId);
33
+ return null;
34
+ }
35
+
36
+ session.lastActivity = Date.now();
37
+ return session;
38
+ }
39
+
40
+ // Get session by user email
41
+ getSessionByUser(email) {
42
+ const sessionId = this.userSessionMap.get(email);
43
+ if (!sessionId) return null;
44
+ return this.getSession(sessionId);
45
+ }
46
+
47
+ // Create new session
48
+ createSession({email, dnioToken, tokenExpiresAt}) {
49
+
50
+ // Destroy existing session for same user
51
+ const existingId = this.userSessionMap.get(email);
52
+ if (existingId) {
53
+ this.destroySession(existingId);
54
+ }
55
+
56
+ const sessionId = crypto.randomUUID();
57
+
58
+ const session = {
59
+ sessionId,
60
+ email,
61
+ dnioToken,
62
+ tokenExpiresAt,
63
+ selectedApp: null,
64
+ createdAt: Date.now(),
65
+ lastActivity: Date.now(),
66
+ transport: null,
67
+ server: null,
68
+ registry: null,
69
+ };
70
+
71
+ this.sessions.set(sessionId, session);
72
+ this.userSessionMap.set(email, sessionId);
73
+
74
+ logger.info(`Session created for ${email} (${sessionId})`);
75
+ return session;
76
+ }
77
+
78
+ // Update session token
79
+ updateToken(sessionId, dnioToken, tokenExpiresAt) {
80
+ const session = this.sessions.get(sessionId);
81
+ if (session) {
82
+ session.dnioToken = dnioToken;
83
+ session.tokenExpiresAt = tokenExpiresAt;
84
+ }
85
+ }
86
+
87
+ // Destroy session
88
+ destroySession(sessionId) {
89
+ const session = this.sessions.get(sessionId);
90
+ if (!session) return;
91
+
92
+ // Remove from maps first
93
+ this.userSessionMap.delete(session.email);
94
+ this.sessions.delete(sessionId);
95
+
96
+ // Close transport safely
97
+ const transport = session.transport;
98
+ session.transport = null;
99
+ if (transport && typeof transport.close === 'function') {
100
+ try {
101
+ transport.close();
102
+ } catch (_) {
103
+ }
104
+ }
105
+
106
+ logger.info(`Session destroyed for ${session.email} (${sessionId})`);
107
+ }
108
+
109
+ // Active session count
110
+ get activeCount() {
111
+ return this.sessions.size;
112
+ }
113
+
114
+ // Summary for debug/health
115
+ getSummary() {
116
+ const summaries = [];
117
+
118
+ for (const [id, session] of this.sessions) {
119
+ summaries.push({
120
+ sessionId: id,
121
+ email: session.email,
122
+ selectedApp: session.selectedApp || 'none',
123
+ lastActivity: new Date(session.lastActivity).toISOString(),
124
+ createdAt: new Date(session.createdAt).toISOString(),
125
+ });
126
+ }
127
+
128
+ return summaries;
129
+ }
130
+
131
+ // Shutdown manager
132
+ shutdown() {
133
+ if (this._cleanupTimer) clearInterval(this._cleanupTimer);
134
+ for (const [sessionId] of this.sessions) {
135
+ this.destroySession(sessionId);
136
+ }
137
+ }
138
+
139
+ // Check if session expired
140
+ _isExpired(session) {
141
+ return Date.now() - session.lastActivity > SESSION_TTL_MS;
142
+ }
143
+
144
+ // Cleanup expired sessions
145
+ _cleanup() {
146
+ let cleaned = 0;
147
+
148
+ for (const [sessionId, session] of this.sessions) {
149
+ if (this._isExpired(session)) {
150
+ this.destroySession(sessionId);
151
+ cleaned++;
152
+ }
153
+ }
154
+
155
+ if (cleaned > 0) {
156
+ logger.info(`Session cleanup: removed ${cleaned} expired sessions, ${this.sessions.size} active`);
157
+ }
158
+ }
159
+ }
160
+
161
+ module.exports = SessionManager;