@imenam/self-register-proxy 1.0.37
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/LICENSE +15 -0
- package/README.md +510 -0
- package/config.js +31 -0
- package/logger.js +88 -0
- package/meta-config.js +336 -0
- package/package.json +71 -0
- package/proxy-client.js +203 -0
- package/proxy.js +169 -0
- package/routes/dashboard.js +152 -0
- package/routes/metaConfig.js +180 -0
- package/routes/registration.js +227 -0
- package/routes/terminal.js +68 -0
- package/services/auth.js +151 -0
- package/services/healthCheck.js +95 -0
- package/services/portManager.js +82 -0
- package/services/proxyRouter.js +99 -0
- package/services/registry.js +41 -0
- package/terminal-server.js +154 -0
- package/views/dashboard.js +688 -0
- package/views/login.js +130 -0
- package/views/metaConfigs.js +643 -0
- package/views/terminal.html +1029 -0
- package/views/terminal.js +30 -0
package/meta-config.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetaConfigManager - Gestion des méta-configurations pour le proxy
|
|
3
|
+
*
|
|
4
|
+
* Permet de grouper les configurations de plusieurs MCPs en un seul profil
|
|
5
|
+
* applicable en un clic depuis le dashboard.
|
|
6
|
+
*
|
|
7
|
+
* Persistence : meta-configs.json dans le répertoire du proxy (__dirname)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// Détermine le dossier de configuration
|
|
14
|
+
// Utilise PROXY_CONFIG_DIR si défini, sinon le dossier courant (__dirname)
|
|
15
|
+
const configDir = process.env.PROXY_CONFIG_DIR
|
|
16
|
+
? path.resolve(__dirname, process.env.PROXY_CONFIG_DIR)
|
|
17
|
+
: __dirname;
|
|
18
|
+
|
|
19
|
+
// Création du dossier s'il n'existe pas
|
|
20
|
+
if (!fs.existsSync(configDir)) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`[META-CONFIG] Erreur lors de la création du dossier ${configDir}:`, err.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const META_CONFIGS_FILE = path.join(configDir, 'meta-configs.json');
|
|
29
|
+
|
|
30
|
+
const DISCOVERY_TIMEOUT = 3000; // 3s timeout per MCP
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Structure du fichier meta-configs.json :
|
|
34
|
+
* {
|
|
35
|
+
* "metaConfigs": {
|
|
36
|
+
* "<id>": {
|
|
37
|
+
* "name": "string",
|
|
38
|
+
* "description": "string",
|
|
39
|
+
* "mappings": { "<mcpPath>": "<configId>", ... },
|
|
40
|
+
* "createdAt": "ISO8601"
|
|
41
|
+
* }
|
|
42
|
+
* },
|
|
43
|
+
* "activeMetaConfig": "<id>" | null
|
|
44
|
+
* }
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Charge les méta-configurations depuis le fichier JSON
|
|
49
|
+
* @returns {{ metaConfigs: Object, activeMetaConfig: string|null }}
|
|
50
|
+
*/
|
|
51
|
+
function loadMetaConfigs() {
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(META_CONFIGS_FILE)) {
|
|
54
|
+
const raw = fs.readFileSync(META_CONFIGS_FILE, 'utf-8');
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('[META-CONFIG] Error loading meta-configs:', err.message);
|
|
59
|
+
}
|
|
60
|
+
return { metaConfigs: {}, activeMetaConfig: null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sauvegarde les méta-configurations dans le fichier JSON
|
|
65
|
+
* @param {{ metaConfigs: Object, activeMetaConfig: string|null }} data
|
|
66
|
+
*/
|
|
67
|
+
function saveMetaConfigs(data) {
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(META_CONFIGS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('[META-CONFIG] Error saving meta-configs:', err.message);
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retourne toutes les méta-configurations
|
|
78
|
+
* @returns {{ metaConfigs: Object, activeMetaConfig: string|null }}
|
|
79
|
+
*/
|
|
80
|
+
function getAll() {
|
|
81
|
+
return loadMetaConfigs();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retourne une méta-configuration par son ID
|
|
86
|
+
* @param {string} id
|
|
87
|
+
* @returns {Object|null}
|
|
88
|
+
*/
|
|
89
|
+
function getById(id) {
|
|
90
|
+
const data = loadMetaConfigs();
|
|
91
|
+
return data.metaConfigs[id] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Crée une nouvelle méta-configuration
|
|
96
|
+
* @param {string} id - Identifiant unique (slug)
|
|
97
|
+
* @param {string} name - Nom affiché
|
|
98
|
+
* @param {string} description - Description
|
|
99
|
+
* @param {Object} mappings - { mcpPath: configId, ... }
|
|
100
|
+
* @returns {Object} La méta-configuration créée
|
|
101
|
+
*/
|
|
102
|
+
function create(id, name, description, mappings) {
|
|
103
|
+
const data = loadMetaConfigs();
|
|
104
|
+
|
|
105
|
+
if (data.metaConfigs[id]) {
|
|
106
|
+
throw new Error(`Meta-config "${id}" already exists`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
data.metaConfigs[id] = {
|
|
110
|
+
name,
|
|
111
|
+
description: description || '',
|
|
112
|
+
mappings: mappings || {},
|
|
113
|
+
createdAt: new Date().toISOString()
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
saveMetaConfigs(data);
|
|
117
|
+
return data.metaConfigs[id];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Met à jour partiellement une méta-configuration
|
|
122
|
+
* @param {string} id
|
|
123
|
+
* @param {Object} updates - Champs à mettre à jour (name, description, mappings)
|
|
124
|
+
* @returns {Object} La méta-configuration mise à jour
|
|
125
|
+
*/
|
|
126
|
+
function update(id, updates) {
|
|
127
|
+
const data = loadMetaConfigs();
|
|
128
|
+
|
|
129
|
+
if (!data.metaConfigs[id]) {
|
|
130
|
+
throw new Error(`Meta-config "${id}" not found`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const config = data.metaConfigs[id];
|
|
134
|
+
if (updates.name !== undefined) config.name = updates.name;
|
|
135
|
+
if (updates.description !== undefined) config.description = updates.description;
|
|
136
|
+
if (updates.mappings !== undefined) config.mappings = updates.mappings;
|
|
137
|
+
|
|
138
|
+
saveMetaConfigs(data);
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Supprime une méta-configuration
|
|
144
|
+
* @param {string} id
|
|
145
|
+
*/
|
|
146
|
+
function remove(id) {
|
|
147
|
+
const data = loadMetaConfigs();
|
|
148
|
+
|
|
149
|
+
if (!data.metaConfigs[id]) {
|
|
150
|
+
throw new Error(`Meta-config "${id}" not found`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
delete data.metaConfigs[id];
|
|
154
|
+
|
|
155
|
+
if (data.activeMetaConfig === id) {
|
|
156
|
+
data.activeMetaConfig = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
saveMetaConfigs(data);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Définit la méta-configuration active
|
|
164
|
+
* @param {string|null} id
|
|
165
|
+
*/
|
|
166
|
+
function setActive(id) {
|
|
167
|
+
const data = loadMetaConfigs();
|
|
168
|
+
|
|
169
|
+
if (id !== null && !data.metaConfigs[id]) {
|
|
170
|
+
throw new Error(`Meta-config "${id}" not found`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
data.activeMetaConfig = id;
|
|
174
|
+
saveMetaConfigs(data);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Retourne l'ID de la méta-configuration active
|
|
179
|
+
* @returns {string|null}
|
|
180
|
+
*/
|
|
181
|
+
function getActive() {
|
|
182
|
+
const data = loadMetaConfigs();
|
|
183
|
+
return data.activeMetaConfig;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Découvre les configurations disponibles auprès de tous les MCPs enregistrés
|
|
188
|
+
* Interroge GET /api/configurations sur chaque MCP via son target direct
|
|
189
|
+
*
|
|
190
|
+
* @param {Map} routes - Le registry.routes du proxy
|
|
191
|
+
* @returns {Promise<Array<{ path: string, name: string, configurations: Array|null, error: string|null }>>}
|
|
192
|
+
*/
|
|
193
|
+
async function discoverConfigurations(routes) {
|
|
194
|
+
const results = [];
|
|
195
|
+
|
|
196
|
+
const promises = [];
|
|
197
|
+
for (const [mcpPath, route] of routes.entries()) {
|
|
198
|
+
const target = route.target || 'http://localhost';
|
|
199
|
+
const url = `${target}:${route.port}/api/configurations`;
|
|
200
|
+
|
|
201
|
+
promises.push(
|
|
202
|
+
(async () => {
|
|
203
|
+
try {
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
|
|
206
|
+
|
|
207
|
+
const response = await fetch(url, {
|
|
208
|
+
method: 'GET',
|
|
209
|
+
signal: controller.signal
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
|
|
214
|
+
if (response.status === 200) {
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
return {
|
|
217
|
+
path: mcpPath,
|
|
218
|
+
name: route.name,
|
|
219
|
+
configurations: data.configurations || [],
|
|
220
|
+
error: null
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
path: mcpPath,
|
|
226
|
+
name: route.name,
|
|
227
|
+
configurations: null,
|
|
228
|
+
error: `HTTP ${response.status}`
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
path: mcpPath,
|
|
233
|
+
name: route.name,
|
|
234
|
+
configurations: null,
|
|
235
|
+
error: err.name === 'AbortError' ? 'timeout' : err.message
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
})()
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const settled = await Promise.all(promises);
|
|
243
|
+
results.push(...settled);
|
|
244
|
+
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Applique une méta-configuration : envoie POST /api/configurations/apply à chaque MCP
|
|
250
|
+
*
|
|
251
|
+
* @param {string} metaConfigId - ID de la méta-configuration à appliquer
|
|
252
|
+
* @param {Map} routes - Le registry.routes du proxy
|
|
253
|
+
* @returns {Promise<{ success: boolean, results: Array<{ path: string, success: boolean, error?: string }> }>}
|
|
254
|
+
*/
|
|
255
|
+
async function applyMetaConfig(metaConfigId, routes) {
|
|
256
|
+
const config = getById(metaConfigId);
|
|
257
|
+
if (!config) {
|
|
258
|
+
throw new Error(`Meta-config "${metaConfigId}" not found`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const missingResults = [];
|
|
262
|
+
const promises = [];
|
|
263
|
+
|
|
264
|
+
for (const [mcpPath, configId] of Object.entries(config.mappings)) {
|
|
265
|
+
const route = routes.get(mcpPath);
|
|
266
|
+
if (!route) {
|
|
267
|
+
missingResults.push({
|
|
268
|
+
path: mcpPath,
|
|
269
|
+
success: false,
|
|
270
|
+
error: 'MCP not registered'
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const target = route.target || 'http://localhost';
|
|
276
|
+
const url = `${target}:${route.port}/api/configurations/apply`;
|
|
277
|
+
|
|
278
|
+
promises.push(
|
|
279
|
+
(async () => {
|
|
280
|
+
try {
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
|
|
283
|
+
|
|
284
|
+
const response = await fetch(url, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { 'Content-Type': 'application/json' },
|
|
287
|
+
body: JSON.stringify({ configId }),
|
|
288
|
+
signal: controller.signal
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
|
|
293
|
+
if (response.ok) {
|
|
294
|
+
const data = await response.json();
|
|
295
|
+
return { path: mcpPath, success: true, appliedConfig: data.appliedConfig || configId };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const errorData = await response.json().catch(() => ({}));
|
|
299
|
+
return { path: mcpPath, success: false, error: errorData.error || `HTTP ${response.status}` };
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return {
|
|
302
|
+
path: mcpPath,
|
|
303
|
+
success: false,
|
|
304
|
+
error: err.name === 'AbortError' ? 'timeout' : err.message
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
})()
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const settled = await Promise.all(promises);
|
|
312
|
+
|
|
313
|
+
const allAppliedSuccess = settled.length === 0 || settled.every(r => r.success);
|
|
314
|
+
|
|
315
|
+
if (allAppliedSuccess) {
|
|
316
|
+
setActive(metaConfigId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: allAppliedSuccess,
|
|
321
|
+
warnings: missingResults,
|
|
322
|
+
results: [...missingResults, ...settled]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
getAll,
|
|
328
|
+
getById,
|
|
329
|
+
create,
|
|
330
|
+
update,
|
|
331
|
+
remove,
|
|
332
|
+
setActive,
|
|
333
|
+
getActive,
|
|
334
|
+
discoverConfigurations,
|
|
335
|
+
applyMetaConfig
|
|
336
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imenam/self-register-proxy",
|
|
3
|
+
"version": "1.0.37",
|
|
4
|
+
"description": "Dynamic reverse proxy with self-registration API - applications can register themselves and receive an automatically assigned port",
|
|
5
|
+
"main": "proxy-client.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"self-register-proxy": "proxy.js",
|
|
8
|
+
"dynamic-proxy": "proxy.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./proxy-client.js",
|
|
12
|
+
"./client": "./proxy-client.js",
|
|
13
|
+
"./server": "./proxy.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"proxy.js",
|
|
17
|
+
"proxy-client.js",
|
|
18
|
+
"logger.js",
|
|
19
|
+
"config.js",
|
|
20
|
+
"meta-config.js",
|
|
21
|
+
"terminal-server.js",
|
|
22
|
+
"services",
|
|
23
|
+
"routes",
|
|
24
|
+
"views",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
30
|
+
"start": "node proxy.js",
|
|
31
|
+
"example": "node example-app.js",
|
|
32
|
+
"release": "npm version patch && npm publish --access public && git push origin main --follow-tags"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"proxy",
|
|
36
|
+
"reverse-proxy",
|
|
37
|
+
"dynamic-proxy",
|
|
38
|
+
"self-register-proxy",
|
|
39
|
+
"self-register",
|
|
40
|
+
"auto-register",
|
|
41
|
+
"microservices",
|
|
42
|
+
"docker",
|
|
43
|
+
"gateway",
|
|
44
|
+
"api-gateway",
|
|
45
|
+
"load-balancer",
|
|
46
|
+
"http-proxy",
|
|
47
|
+
"express"
|
|
48
|
+
],
|
|
49
|
+
"author": "Matthieu Pesnot-Pin",
|
|
50
|
+
"license": "ISC",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/Matthieu-Pesnot-Pin/dynamic-self-register-proxy.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/Matthieu-Pesnot-Pin/dynamic-self-register-proxy#readme",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/Matthieu-Pesnot-Pin/dynamic-self-register-proxy/issues"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"dotenv": "^17.2.3",
|
|
64
|
+
"express": "^5.2.1",
|
|
65
|
+
"express-rate-limit": "^8.2.1",
|
|
66
|
+
"helmet": "^8.1.0",
|
|
67
|
+
"http-proxy-middleware": "^3.0.5",
|
|
68
|
+
"node-pty": "^1.1.0",
|
|
69
|
+
"ws": "^8.19.0"
|
|
70
|
+
}
|
|
71
|
+
}
|
package/proxy-client.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProxyClient - Helper pour s'enregistrer auprès du Dynamic Proxy
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const ProxyClient = require('./proxy-client');
|
|
6
|
+
* const proxy = new ProxyClient('http://localhost:3000');
|
|
7
|
+
* const { port } = await proxy.register('/myapp', 'My App');
|
|
8
|
+
* app.listen(port);
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// New comment to test the freshness
|
|
12
|
+
|
|
13
|
+
class ProxyClient {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} proxyUrl - URL du proxy (ex: 'http://localhost:3000')
|
|
16
|
+
*/
|
|
17
|
+
constructor(proxyUrl = 'http://localhost:3000') {
|
|
18
|
+
this.proxyUrl = proxyUrl;
|
|
19
|
+
this.registeredPath = null;
|
|
20
|
+
this.assignedPort = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Enregistre une route auprès du proxy (attribution automatique du port)
|
|
25
|
+
* @param {string} path - Chemin URL (ex: '/myapp')
|
|
26
|
+
* @param {string} [name] - Nom de l'application
|
|
27
|
+
* @returns {Promise<{success: boolean, path: string, port: number, name: string}>}
|
|
28
|
+
*/
|
|
29
|
+
async register(path, name) {
|
|
30
|
+
const body = { path, name };
|
|
31
|
+
// Port is automatically assigned by the proxy
|
|
32
|
+
|
|
33
|
+
const response = await fetch(`${this.proxyUrl}/proxy/register`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify(body)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(data.error || 'Registration failed');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.registeredPath = data.path;
|
|
46
|
+
this.assignedPort = data.port;
|
|
47
|
+
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Supprime l'enregistrement de cette application
|
|
53
|
+
* @returns {Promise<{success: boolean, path: string, freedPort: number}>}
|
|
54
|
+
*/
|
|
55
|
+
async unregister() {
|
|
56
|
+
if (!this.registeredPath) {
|
|
57
|
+
return { success: false, error: 'Not registered' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`${this.proxyUrl}/proxy/unregister`, {
|
|
62
|
+
method: 'DELETE',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ path: this.registeredPath })
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
|
|
69
|
+
if (response.ok) {
|
|
70
|
+
this.registeredPath = null;
|
|
71
|
+
this.assignedPort = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return data;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Unregister error:', error.message);
|
|
77
|
+
return { success: false, error: error.message };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Liste toutes les routes enregistrées
|
|
83
|
+
* @returns {Promise<{count: number, routes: Array, availablePorts: number}>}
|
|
84
|
+
*/
|
|
85
|
+
async listRoutes() {
|
|
86
|
+
const response = await fetch(`${this.proxyUrl}/proxy/routes`);
|
|
87
|
+
return response.json();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Vérifie l'état du proxy
|
|
92
|
+
* @returns {Promise<{status: string, uptime: number, registeredRoutes: number}>}
|
|
93
|
+
*/
|
|
94
|
+
async health() {
|
|
95
|
+
const response = await fetch(`${this.proxyUrl}/proxy/health`);
|
|
96
|
+
return response.json();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Configure les handlers pour un arrêt propre
|
|
101
|
+
* @param {http.Server} server - Instance du serveur HTTP/Express
|
|
102
|
+
*/
|
|
103
|
+
setupGracefulShutdown(server) {
|
|
104
|
+
const shutdown = async () => {
|
|
105
|
+
console.log('Shutting down...');
|
|
106
|
+
await this.unregister();
|
|
107
|
+
server.close(() => {
|
|
108
|
+
console.log('Server closed.');
|
|
109
|
+
process.exit(0);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
process.on('SIGTERM', shutdown);
|
|
114
|
+
process.on('SIGINT', shutdown);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ajoute la route /proxy/health requise par le proxy pour le health check polling
|
|
119
|
+
* @param {express.Application} app - Instance de l'application Express
|
|
120
|
+
* @param {object} [options] - Options de configuration
|
|
121
|
+
* @param {function} [options.healthCheck] - Fonction personnalisée pour vérifier la santé (doit retourner true/false)
|
|
122
|
+
*/
|
|
123
|
+
setupHealthRoute(app, options = {}) {
|
|
124
|
+
app.get('/proxy/health', async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
// Si une fonction de health check personnalisée est fournie
|
|
127
|
+
if (options.healthCheck) {
|
|
128
|
+
const isHealthy = await options.healthCheck();
|
|
129
|
+
if (isHealthy) {
|
|
130
|
+
return res.status(200).json({ status: 'healthy' });
|
|
131
|
+
} else {
|
|
132
|
+
return res.status(503).json({ status: 'unhealthy' });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Par défaut, retourne 200 si le serveur répond
|
|
137
|
+
res.status(200).json({ status: 'healthy' });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
res.status(503).json({ status: 'unhealthy', error: error.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ajoute les routes de configuration requises pour la fonctionnalité Meta-Config du proxy.
|
|
146
|
+
*
|
|
147
|
+
* Expose deux endpoints sur l'application Express du MCP :
|
|
148
|
+
* - GET /api/configurations → liste les profils de configuration disponibles
|
|
149
|
+
* - POST /api/configurations/apply → applique un profil par son ID
|
|
150
|
+
*
|
|
151
|
+
* @param {express.Application} app - Instance de l'application Express du MCP
|
|
152
|
+
* @param {object} provider - Fournisseur de configurations
|
|
153
|
+
* @param {function} provider.getConfigurations - Async function retournant un tableau de configurations
|
|
154
|
+
* Chaque configuration doit avoir : { id: string, name: string, description?: string, active?: boolean }
|
|
155
|
+
* @param {function} provider.applyConfiguration - Async function(configId) qui applique la configuration
|
|
156
|
+
* Doit throw en cas d'erreur, ou retourner normalement en cas de succès
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* proxy.setupConfigurationsRoute(app, {
|
|
160
|
+
* getConfigurations: async () => [
|
|
161
|
+
* { id: 'default', name: 'Default', description: 'Standard settings', active: true },
|
|
162
|
+
* { id: 'production', name: 'Production', description: 'Optimized for prod' }
|
|
163
|
+
* ],
|
|
164
|
+
* applyConfiguration: async (configId) => {
|
|
165
|
+
* // Appliquer la configuration identifiée par configId
|
|
166
|
+
* await switchToConfig(configId);
|
|
167
|
+
* }
|
|
168
|
+
* });
|
|
169
|
+
*/
|
|
170
|
+
setupConfigurationsRoute(app, provider) {
|
|
171
|
+
if (!provider || typeof provider.getConfigurations !== 'function' || typeof provider.applyConfiguration !== 'function') {
|
|
172
|
+
throw new Error('setupConfigurationsRoute requires a provider with getConfigurations() and applyConfiguration(configId) methods');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
app.get('/api/configurations', async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const configurations = await provider.getConfigurations();
|
|
178
|
+
res.json({ configurations });
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('[CONFIGURATIONS] Error listing configurations:', error.message);
|
|
181
|
+
res.status(500).json({ error: error.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
app.post('/api/configurations/apply', async (req, res) => {
|
|
186
|
+
const { configId } = req.body;
|
|
187
|
+
|
|
188
|
+
if (!configId) {
|
|
189
|
+
return res.status(400).json({ success: false, error: 'configId is required' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await provider.applyConfiguration(configId);
|
|
194
|
+
res.json({ success: true, appliedConfig: configId });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('[CONFIGURATIONS] Error applying configuration:', error.message);
|
|
197
|
+
res.status(500).json({ success: false, error: error.message });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = ProxyClient;
|