@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/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
+ }
@@ -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;