@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.
- package/Dockerfile +20 -0
- package/docs/README.md +35 -0
- package/docs/architecture.md +171 -0
- package/docs/authentication.md +74 -0
- package/docs/tools/apps.md +59 -0
- package/docs/tools/connectors.md +76 -0
- package/docs/tools/data-pipes.md +286 -0
- package/docs/tools/data-services.md +105 -0
- package/docs/tools/deployment-groups.md +152 -0
- package/docs/tools/plugins.md +94 -0
- package/docs/tools/records.md +97 -0
- package/docs/workflows.md +195 -0
- package/env.example +16 -0
- package/package.json +43 -0
- package/readme.md +144 -0
- package/src/clients/api-keys.js +10 -0
- package/src/clients/apps.js +13 -0
- package/src/clients/base-client.js +78 -0
- package/src/clients/bots.js +10 -0
- package/src/clients/connectors.js +30 -0
- package/src/clients/data-formats.js +40 -0
- package/src/clients/data-pipes.js +33 -0
- package/src/clients/deployment-groups.js +59 -0
- package/src/clients/formulas.js +10 -0
- package/src/clients/functions.js +10 -0
- package/src/clients/plugins.js +39 -0
- package/src/clients/records.js +51 -0
- package/src/clients/services.js +63 -0
- package/src/clients/user-groups.js +10 -0
- package/src/clients/users.js +10 -0
- package/src/examples/ai-sdk-client.js +165 -0
- package/src/examples/claude_desktop_config.json +34 -0
- package/src/examples/express-integration.js +181 -0
- package/src/index.js +283 -0
- package/src/schemas/schema-converter.js +179 -0
- package/src/services/auth-manager.js +277 -0
- package/src/services/dnio-client.js +40 -0
- package/src/services/service-registry.js +150 -0
- package/src/services/session-manager.js +161 -0
- package/src/stdio-bridge.js +185 -0
- package/src/tools/_helpers.js +32 -0
- package/src/tools/api-keys.js +5 -0
- package/src/tools/apps.js +185 -0
- package/src/tools/bots.js +5 -0
- package/src/tools/connectors.js +165 -0
- package/src/tools/data-formats.js +806 -0
- package/src/tools/data-pipes.js +1305 -0
- package/src/tools/data-service-registry.js +500 -0
- package/src/tools/deployment-groups.js +511 -0
- package/src/tools/formulas.js +5 -0
- package/src/tools/functions.js +5 -0
- package/src/tools/mcp-tools-registry.js +38 -0
- package/src/tools/plugins.js +250 -0
- package/src/tools/records.js +217 -0
- package/src/tools/services.js +476 -0
- package/src/tools/user-groups.js +5 -0
- package/src/tools/users.js +5 -0
- package/src/utils/constants.js +135 -0
- 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;
|