dynamic-self-register-proxy 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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # Dynamic Self-Register Proxy
2
+
3
+ [![npm version](https://badge.fury.io/js/dynamic-self-register-proxy.svg)](https://www.npmjs.com/package/dynamic-self-register-proxy)
4
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
+
6
+ Un reverse proxy Node.js avec API d'auto-enregistrement, permettant à vos applications de s'enregistrer dynamiquement et de recevoir un port automatiquement attribué.
7
+
8
+ ## Cas d'usage
9
+
10
+ - **Conteneur Docker unique** exposant un seul port vers l'extérieur
11
+ - **Microservices dynamiques** qui démarrent/s'arrêtent sans configuration manuelle
12
+ - **Environnement de développement** avec plusieurs applications Node.js
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install dynamic-self-register-proxy
18
+ ```
19
+
20
+ Ou pour une installation globale :
21
+
22
+ ```bash
23
+ npm install -g dynamic-self-register-proxy
24
+ ```
25
+
26
+ ## Démarrage rapide
27
+
28
+ ### 1. Démarrer le proxy
29
+
30
+ ```bash
31
+ # Via npx (sans installation globale)
32
+ npx dynamic-self-register-proxy
33
+
34
+ # Ou si installé globalement
35
+ dynamic-proxy
36
+
37
+ # Ou via npm script (développement local)
38
+ npm start
39
+ ```
40
+
41
+ Le proxy démarre par défaut sur le port `3000`. Pour changer le port :
42
+
43
+ ```bash
44
+ # Windows PowerShell
45
+ $env:PROXY_PORT="3002"; npx dynamic-self-register-proxy
46
+
47
+ # Linux/Mac
48
+ PROXY_PORT=3002 npx dynamic-self-register-proxy
49
+ ```
50
+
51
+ ### 2. Enregistrer une application
52
+
53
+ Votre application doit s'enregistrer au démarrage :
54
+
55
+ ```javascript
56
+ const response = await fetch('http://localhost:3000/proxy/register', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({
60
+ path: '/myapp', // Chemin d'accès via le proxy
61
+ name: 'My App' // Nom de l'application (optionnel)
62
+ })
63
+ });
64
+
65
+ const { port } = await response.json();
66
+ // port = 4000 (attribué automatiquement)
67
+
68
+ // Démarrez votre serveur sur ce port
69
+ app.listen(port);
70
+ ```
71
+
72
+ ### 3. Accéder à votre application
73
+
74
+ ```
75
+ http://localhost:3000/myapp/ → votre application sur le port 4000
76
+ ```
77
+
78
+ ## API du Proxy
79
+
80
+ | Endpoint | Méthode | Description |
81
+ |----------|---------|-------------|
82
+ | `/proxy/register` | POST | Enregistre une nouvelle route |
83
+ | `/proxy/unregister` | DELETE | Supprime une route |
84
+ | `/proxy/routes` | GET | Liste toutes les routes |
85
+ | `/proxy/health` | GET | Health check du proxy |
86
+
87
+ ### POST /proxy/register
88
+
89
+ Enregistre une nouvelle route et attribue un port.
90
+
91
+ **Request:**
92
+ ```json
93
+ {
94
+ "path": "/myapp",
95
+ "name": "My Application",
96
+ "port": 4005
97
+ }
98
+ ```
99
+
100
+ | Champ | Type | Requis | Description |
101
+ |-------|------|--------|-------------|
102
+ | `path` | string | ✅ | Chemin URL pour accéder à l'app |
103
+ | `name` | string | ❌ | Nom descriptif |
104
+ | `port` | number | ❌ | Port spécifique (sinon auto-attribué) |
105
+
106
+ **Response (201):**
107
+ ```json
108
+ {
109
+ "success": true,
110
+ "path": "/myapp",
111
+ "port": 4000,
112
+ "name": "My Application",
113
+ "message": "Route registered. Start your server on port 4000"
114
+ }
115
+ ```
116
+
117
+ **Erreurs:**
118
+ - `400` - Path manquant
119
+ - `409` - Path ou port déjà utilisé
120
+
121
+ ### DELETE /proxy/unregister
122
+
123
+ Supprime une route enregistrée.
124
+
125
+ **Request:**
126
+ ```json
127
+ {
128
+ "path": "/myapp"
129
+ }
130
+ ```
131
+
132
+ **Response (200):**
133
+ ```json
134
+ {
135
+ "success": true,
136
+ "path": "/myapp",
137
+ "freedPort": 4000
138
+ }
139
+ ```
140
+
141
+ ### GET /proxy/routes
142
+
143
+ Liste toutes les routes enregistrées.
144
+
145
+ **Response:**
146
+ ```json
147
+ {
148
+ "count": 2,
149
+ "routes": [
150
+ {
151
+ "path": "/api",
152
+ "port": 4000,
153
+ "name": "API Server",
154
+ "registeredAt": "2026-01-27T10:00:00.000Z",
155
+ "target": "http://localhost:4000"
156
+ },
157
+ {
158
+ "path": "/web",
159
+ "port": 4001,
160
+ "name": "Web App",
161
+ "registeredAt": "2026-01-27T10:01:00.000Z",
162
+ "target": "http://localhost:4001"
163
+ }
164
+ ],
165
+ "availablePorts": 998
166
+ }
167
+ ```
168
+
169
+ ### GET /proxy/health
170
+
171
+ Vérifie l'état du proxy.
172
+
173
+ **Response:**
174
+ ```json
175
+ {
176
+ "status": "healthy",
177
+ "uptime": 3600,
178
+ "registeredRoutes": 2,
179
+ "usedPorts": 2
180
+ }
181
+ ```
182
+
183
+ ## Intégration dans votre application
184
+
185
+ ### Exemple complet avec Express
186
+
187
+ ```javascript
188
+ const express = require('express');
189
+
190
+ const PROXY_URL = process.env.PROXY_URL || 'http://localhost:3000';
191
+ const APP_PATH = process.env.APP_PATH || '/myapp';
192
+ const APP_NAME = process.env.APP_NAME || 'My App';
193
+
194
+ async function start() {
195
+ // 1. S'enregistrer auprès du proxy
196
+ const res = await fetch(`${PROXY_URL}/proxy/register`, {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify({ path: APP_PATH, name: APP_NAME })
200
+ });
201
+
202
+ const { port } = await res.json();
203
+
204
+ // 2. Créer l'application
205
+ const app = express();
206
+
207
+ app.get('/', (req, res) => {
208
+ res.json({ message: 'Hello!' });
209
+ });
210
+
211
+ // 3. Démarrer sur le port attribué
212
+ const server = app.listen(port, () => {
213
+ console.log(`App accessible via ${PROXY_URL}${APP_PATH}`);
214
+ });
215
+
216
+ // 4. Se désenregistrer à l'arrêt
217
+ process.on('SIGTERM', async () => {
218
+ await fetch(`${PROXY_URL}/proxy/unregister`, {
219
+ method: 'DELETE',
220
+ headers: { 'Content-Type': 'application/json' },
221
+ body: JSON.stringify({ path: APP_PATH })
222
+ });
223
+ server.close();
224
+ });
225
+ }
226
+
227
+ start();
228
+ ```
229
+
230
+ ### Utilisation du ProxyClient
231
+
232
+ Le package inclut un client helper pour faciliter l'enregistrement :
233
+
234
+ ```javascript
235
+ const ProxyClient = require('dynamic-self-register-proxy');
236
+
237
+ const proxy = new ProxyClient('http://localhost:3000');
238
+ const { port } = await proxy.register('/myapp', 'My App');
239
+
240
+ app.listen(port);
241
+
242
+ // Arrêt propre automatique
243
+ proxy.setupGracefulShutdown(server);
244
+ ```
245
+
246
+ Méthodes disponibles :
247
+
248
+ - `register(path, name, port?)` - Enregistre une route
249
+ - `unregister()` - Supprime l'enregistrement
250
+ - `listRoutes()` - Liste toutes les routes
251
+ - `health()` - Vérifie l'état du proxy
252
+ - `setupGracefulShutdown(server)` - Configure l'arrêt propre
253
+ - `setupHealthRoute(app, options?)` - Ajoute la route de health check
254
+
255
+ ## Docker
256
+
257
+ ### Build et lancement
258
+
259
+ ```bash
260
+ # Build
261
+ npm run docker:build
262
+
263
+ # Démarrer (proxy + 2 apps exemples)
264
+ npm run docker:up
265
+
266
+ # Arrêter
267
+ npm run docker:down
268
+ ```
269
+
270
+ ### Accès
271
+
272
+ - Proxy : `http://localhost:8081`
273
+ - App 1 : `http://localhost:8081/app1`
274
+ - App 2 : `http://localhost:8081/app2`
275
+ - Routes : `http://localhost:8081/proxy/routes`
276
+
277
+ ### Ajouter votre propre service
278
+
279
+ Dans `docker-compose.yml` :
280
+
281
+ ```yaml
282
+ services:
283
+ my-service:
284
+ build: ./my-service
285
+ environment:
286
+ - PROXY_URL=http://proxy:3000
287
+ - APP_PATH=/my-service
288
+ - APP_NAME=My Service
289
+ depends_on:
290
+ - proxy
291
+ networks:
292
+ - proxy-network
293
+ ```
294
+
295
+ ## Health Check Polling
296
+
297
+ Le proxy vérifie périodiquement que les serveurs enregistrés sont toujours actifs. Si un serveur ne répond pas correctement, il est automatiquement désenregistré.
298
+
299
+ ### Implémentation requise
300
+
301
+ Chaque application enregistrée **doit** implémenter la route `GET /proxy/health` qui retourne un code **200** :
302
+
303
+ ```javascript
304
+ // Avec Express
305
+ app.get('/proxy/health', (req, res) => {
306
+ res.status(200).json({ status: 'healthy' });
307
+ });
308
+ ```
309
+
310
+ ### Avec ProxyClient
311
+
312
+ Le helper `ProxyClient` fournit une méthode pour ajouter automatiquement cette route :
313
+
314
+ ```javascript
315
+ const ProxyClient = require('dynamic-self-register-proxy');
316
+
317
+ const proxy = new ProxyClient('http://localhost:3000');
318
+ const app = express();
319
+
320
+ // Ajoute automatiquement GET /proxy/health
321
+ proxy.setupHealthRoute(app);
322
+
323
+ // Ou avec un health check personnalisé
324
+ proxy.setupHealthRoute(app, {
325
+ healthCheck: async () => {
326
+ // Vérifier la connexion à la DB, etc.
327
+ const dbOk = await checkDatabase();
328
+ return dbOk;
329
+ }
330
+ });
331
+ ```
332
+
333
+ ### Comportement
334
+
335
+ - Le proxy vérifie tous les serveurs toutes les **30 secondes** (par défaut)
336
+ - Timeout de **5 secondes** par requête de health check
337
+ - Si la réponse n'est pas un code **200**, le serveur est automatiquement désenregistré
338
+ - Les erreurs de connexion (serveur arrêté, timeout) entraînent aussi le désenregistrement
339
+
340
+ ## Configuration
341
+
342
+ | Variable | Défaut | Description |
343
+ |----------|--------|-------------|
344
+ | `PROXY_PORT` | `3000` | Port d'écoute du proxy |
345
+ | `HEALTH_CHECK_INTERVAL` | `30000` | Intervalle du health check polling (ms) |
346
+ | `HEALTH_CHECK_TIMEOUT` | `5000` | Timeout pour chaque health check (ms) |
347
+ | `PROXY_URL` | `http://localhost:3000` | URL du proxy (pour les apps) |
348
+ | `APP_PATH` | `/example` | Chemin de l'application |
349
+ | `APP_NAME` | `Example App` | Nom de l'application |
350
+
351
+ ## Plage de ports
352
+
353
+ Par défaut, le proxy attribue des ports entre **4000** et **5000** (1000 ports disponibles).
354
+
355
+ Pour modifier cette plage, éditez `proxy.js` :
356
+
357
+ ```javascript
358
+ const INTERNAL_PORT_START = 4000;
359
+ const INTERNAL_PORT_END = 5000;
360
+ ```
361
+
362
+ ## Limitations
363
+
364
+ - Les routes sont stockées en mémoire (perdues au redémarrage du proxy)
365
+ - Pas de persistance des enregistrements
366
+ - Pas d'authentification sur l'API de registration
367
+ - Les applications doivent implémenter `GET /proxy/health` pour ne pas être désenregistrées automatiquement
368
+
369
+ ## Licence
370
+
371
+ ISC
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "dynamic-self-register-proxy",
3
+ "version": "1.0.0",
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
+ "dynamic-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
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "test": "echo \"Error: no test specified\" && exit 1",
23
+ "start": "node proxy.js",
24
+ "example": "node example-app.js"
25
+ },
26
+ "keywords": [
27
+ "proxy",
28
+ "reverse-proxy",
29
+ "dynamic-proxy",
30
+ "self-register",
31
+ "auto-register",
32
+ "microservices",
33
+ "docker",
34
+ "gateway",
35
+ "api-gateway",
36
+ "load-balancer",
37
+ "http-proxy",
38
+ "express"
39
+ ],
40
+ "author": "Matthieu Pesnot-Pin",
41
+ "license": "ISC",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/matthieu-music/dynamic-self-register-proxy.git"
45
+ },
46
+ "homepage": "https://github.com/matthieu-music/dynamic-self-register-proxy#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/matthieu-music/dynamic-self-register-proxy/issues"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ },
53
+ "dependencies": {
54
+ "express": "^5.2.1",
55
+ "http-proxy-middleware": "^3.0.5"
56
+ }
57
+ }
@@ -0,0 +1,144 @@
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
+ class ProxyClient {
12
+ /**
13
+ * @param {string} proxyUrl - URL du proxy (ex: 'http://localhost:3000')
14
+ */
15
+ constructor(proxyUrl = 'http://localhost:3000') {
16
+ this.proxyUrl = proxyUrl;
17
+ this.registeredPath = null;
18
+ this.assignedPort = null;
19
+ }
20
+
21
+ /**
22
+ * Enregistre une route auprès du proxy
23
+ * @param {string} path - Chemin URL (ex: '/myapp')
24
+ * @param {string} [name] - Nom de l'application
25
+ * @param {number} [port] - Port spécifique (optionnel, sinon auto-attribué)
26
+ * @returns {Promise<{success: boolean, path: string, port: number, name: string}>}
27
+ */
28
+ async register(path, name, port) {
29
+ const body = { path, name };
30
+ if (port) body.port = port;
31
+
32
+ const response = await fetch(`${this.proxyUrl}/proxy/register`, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify(body)
36
+ });
37
+
38
+ const data = await response.json();
39
+
40
+ if (!response.ok) {
41
+ throw new Error(data.error || 'Registration failed');
42
+ }
43
+
44
+ this.registeredPath = data.path;
45
+ this.assignedPort = data.port;
46
+
47
+ return data;
48
+ }
49
+
50
+ /**
51
+ * Supprime l'enregistrement de cette application
52
+ * @returns {Promise<{success: boolean, path: string, freedPort: number}>}
53
+ */
54
+ async unregister() {
55
+ if (!this.registeredPath) {
56
+ return { success: false, error: 'Not registered' };
57
+ }
58
+
59
+ try {
60
+ const response = await fetch(`${this.proxyUrl}/proxy/unregister`, {
61
+ method: 'DELETE',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ path: this.registeredPath })
64
+ });
65
+
66
+ const data = await response.json();
67
+
68
+ if (response.ok) {
69
+ this.registeredPath = null;
70
+ this.assignedPort = null;
71
+ }
72
+
73
+ return data;
74
+ } catch (error) {
75
+ console.error('Unregister error:', error.message);
76
+ return { success: false, error: error.message };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Liste toutes les routes enregistrées
82
+ * @returns {Promise<{count: number, routes: Array, availablePorts: number}>}
83
+ */
84
+ async listRoutes() {
85
+ const response = await fetch(`${this.proxyUrl}/proxy/routes`);
86
+ return response.json();
87
+ }
88
+
89
+ /**
90
+ * Vérifie l'état du proxy
91
+ * @returns {Promise<{status: string, uptime: number, registeredRoutes: number}>}
92
+ */
93
+ async health() {
94
+ const response = await fetch(`${this.proxyUrl}/proxy/health`);
95
+ return response.json();
96
+ }
97
+
98
+ /**
99
+ * Configure les handlers pour un arrêt propre
100
+ * @param {http.Server} server - Instance du serveur HTTP/Express
101
+ */
102
+ setupGracefulShutdown(server) {
103
+ const shutdown = async () => {
104
+ console.log('Shutting down...');
105
+ await this.unregister();
106
+ server.close(() => {
107
+ console.log('Server closed.');
108
+ process.exit(0);
109
+ });
110
+ };
111
+
112
+ process.on('SIGTERM', shutdown);
113
+ process.on('SIGINT', shutdown);
114
+ }
115
+
116
+ /**
117
+ * Ajoute la route /proxy/health requise par le proxy pour le health check polling
118
+ * @param {express.Application} app - Instance de l'application Express
119
+ * @param {object} [options] - Options de configuration
120
+ * @param {function} [options.healthCheck] - Fonction personnalisée pour vérifier la santé (doit retourner true/false)
121
+ */
122
+ setupHealthRoute(app, options = {}) {
123
+ app.get('/proxy/health', async (req, res) => {
124
+ try {
125
+ // Si une fonction de health check personnalisée est fournie
126
+ if (options.healthCheck) {
127
+ const isHealthy = await options.healthCheck();
128
+ if (isHealthy) {
129
+ return res.status(200).json({ status: 'healthy' });
130
+ } else {
131
+ return res.status(503).json({ status: 'unhealthy' });
132
+ }
133
+ }
134
+
135
+ // Par défaut, retourne 200 si le serveur répond
136
+ res.status(200).json({ status: 'healthy' });
137
+ } catch (error) {
138
+ res.status(503).json({ status: 'unhealthy', error: error.message });
139
+ }
140
+ });
141
+ }
142
+ }
143
+
144
+ module.exports = ProxyClient;
package/proxy.js ADDED
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env node
2
+
3
+ const express = require('express');
4
+ const { createProxyMiddleware } = require('http-proxy-middleware');
5
+
6
+ const app = express();
7
+ app.use(express.json());
8
+
9
+ // ============================================
10
+ // CONFIGURATION
11
+ // ============================================
12
+ const PROXY_PORT = process.env.PROXY_PORT || 3005;
13
+ const INTERNAL_PORT_START = 4000;
14
+ const INTERNAL_PORT_END = 5000;
15
+ const HEALTH_CHECK_INTERVAL = process.env.HEALTH_CHECK_INTERVAL || 30000; // 30 secondes par défaut
16
+ const HEALTH_CHECK_TIMEOUT = process.env.HEALTH_CHECK_TIMEOUT || 5000; // 5 secondes timeout
17
+
18
+ // ============================================
19
+ // REGISTRY - Stockage des routes en mémoire
20
+ // ============================================
21
+ const registry = {
22
+ routes: new Map(), // path -> { port, name, registeredAt }
23
+ usedPorts: new Set(), // Ports déjà attribués
24
+ nextPort: INTERNAL_PORT_START
25
+ };
26
+
27
+ /**
28
+ * Trouve le prochain port disponible
29
+ */
30
+ function getNextAvailablePort() {
31
+ while (registry.usedPorts.has(registry.nextPort)) {
32
+ registry.nextPort++;
33
+ if (registry.nextPort > INTERNAL_PORT_END) {
34
+ registry.nextPort = INTERNAL_PORT_START;
35
+ // Vérifie si tous les ports sont utilisés
36
+ if (registry.usedPorts.size >= (INTERNAL_PORT_END - INTERNAL_PORT_START)) {
37
+ throw new Error('No available ports');
38
+ }
39
+ }
40
+ }
41
+ const port = registry.nextPort;
42
+ registry.nextPort++;
43
+ return port;
44
+ }
45
+
46
+ /**
47
+ * Trouve la route correspondant à un chemin de requête
48
+ */
49
+ function findRouteForPath(requestPath) {
50
+ // Tri des routes par longueur décroissante pour matcher le plus spécifique d'abord
51
+ const sortedPaths = [...registry.routes.keys()].sort((a, b) => b.length - a.length);
52
+
53
+ for (const routePath of sortedPaths) {
54
+ if (requestPath.startsWith(routePath)) {
55
+ return registry.routes.get(routePath);
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // ============================================
62
+ // HEALTH CHECK POLLING
63
+ // ============================================
64
+
65
+ /**
66
+ * Vérifie la santé d'un serveur enregistré
67
+ * @param {string} path - Le chemin enregistré
68
+ * @param {object} route - Les informations de la route
69
+ * @returns {Promise<boolean>} - true si le serveur est en bonne santé
70
+ */
71
+ async function checkServerHealth(path, route) {
72
+ const healthUrl = `http://localhost:${route.port}/proxy/health`;
73
+
74
+ try {
75
+ const controller = new AbortController();
76
+ const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
77
+
78
+ const response = await fetch(healthUrl, {
79
+ method: 'GET',
80
+ signal: controller.signal
81
+ });
82
+
83
+ clearTimeout(timeoutId);
84
+
85
+ return response.status === 200;
86
+ } catch (error) {
87
+ // Timeout, connexion refusée, ou autre erreur
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Désenregistre un serveur (utilisé par le health check)
94
+ * @param {string} path - Le chemin à désenregistrer
95
+ */
96
+ function unregisterServer(path) {
97
+ const route = registry.routes.get(path);
98
+ if (route) {
99
+ registry.routes.delete(path);
100
+ registry.usedPorts.delete(route.port);
101
+ console.log(`[HEALTH CHECK] Unregistered ${path} (was on port ${route.port}) - server unhealthy`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Effectue un health check sur tous les serveurs enregistrés
107
+ */
108
+ async function performHealthChecks() {
109
+ if (registry.routes.size === 0) {
110
+ return;
111
+ }
112
+
113
+ console.log(`[HEALTH CHECK] Checking ${registry.routes.size} registered server(s)...`);
114
+
115
+ const checks = [];
116
+
117
+ for (const [path, route] of registry.routes.entries()) {
118
+ checks.push(
119
+ checkServerHealth(path, route).then(isHealthy => ({
120
+ path,
121
+ route,
122
+ isHealthy
123
+ }))
124
+ );
125
+ }
126
+
127
+ const results = await Promise.all(checks);
128
+
129
+ for (const { path, isHealthy } of results) {
130
+ if (!isHealthy) {
131
+ unregisterServer(path);
132
+ }
133
+ }
134
+
135
+ const healthyCount = results.filter(r => r.isHealthy).length;
136
+ const unhealthyCount = results.length - healthyCount;
137
+
138
+ if (unhealthyCount > 0) {
139
+ console.log(`[HEALTH CHECK] Complete: ${healthyCount} healthy, ${unhealthyCount} removed`);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Démarre le polling de health check
145
+ */
146
+ function startHealthCheckPolling() {
147
+ console.log(`[HEALTH CHECK] Starting polling every ${HEALTH_CHECK_INTERVAL}ms`);
148
+ setInterval(performHealthChecks, HEALTH_CHECK_INTERVAL);
149
+ }
150
+
151
+ // ============================================
152
+ // API D'ENREGISTREMENT
153
+ // ============================================
154
+
155
+ /**
156
+ * POST /proxy/register
157
+ * Enregistre une nouvelle route
158
+ * Body: { path: "/myapp", name: "My Application", port?: 4001 }
159
+ * Response: { success: true, path: "/myapp", port: 4001 }
160
+ */
161
+ app.post('/proxy/register', (req, res) => {
162
+ try {
163
+ const { path, name, port: requestedPort } = req.body;
164
+
165
+ if (!path) {
166
+ return res.status(400).json({
167
+ success: false,
168
+ error: 'Path is required'
169
+ });
170
+ }
171
+
172
+ // Normalise le path (doit commencer par /)
173
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
174
+
175
+ // Vérifie si le path existe déjà
176
+ if (registry.routes.has(normalizedPath)) {
177
+ const existing = registry.routes.get(normalizedPath);
178
+ return res.status(409).json({
179
+ success: false,
180
+ error: 'Path already registered',
181
+ existing: {
182
+ path: normalizedPath,
183
+ port: existing.port,
184
+ name: existing.name
185
+ }
186
+ });
187
+ }
188
+
189
+ // Attribution du port
190
+ let port;
191
+ if (requestedPort) {
192
+ // Port spécifique demandé
193
+ if (registry.usedPorts.has(requestedPort)) {
194
+ return res.status(409).json({
195
+ success: false,
196
+ error: `Port ${requestedPort} already in use`
197
+ });
198
+ }
199
+ port = requestedPort;
200
+ } else {
201
+ // Attribution automatique
202
+ port = getNextAvailablePort();
203
+ }
204
+
205
+ // Enregistrement
206
+ registry.routes.set(normalizedPath, {
207
+ port,
208
+ name: name || normalizedPath,
209
+ registeredAt: new Date().toISOString()
210
+ });
211
+ registry.usedPorts.add(port);
212
+
213
+ console.log(`[REGISTER] ${normalizedPath} -> localhost:${port} (${name || 'unnamed'})`);
214
+
215
+ res.json({
216
+ success: true,
217
+ path: normalizedPath,
218
+ port,
219
+ name: name || normalizedPath,
220
+ message: `Route registered. Start your server on port ${port}`
221
+ });
222
+
223
+ } catch (error) {
224
+ console.error('[REGISTER ERROR]', error.message);
225
+ res.status(500).json({
226
+ success: false,
227
+ error: error.message
228
+ });
229
+ }
230
+ });
231
+
232
+ /**
233
+ * DELETE /proxy/unregister
234
+ * Supprime une route
235
+ * Body: { path: "/myapp" }
236
+ */
237
+ app.delete('/proxy/unregister', (req, res) => {
238
+ try {
239
+ const { path } = req.body;
240
+
241
+ if (!path) {
242
+ return res.status(400).json({
243
+ success: false,
244
+ error: 'Path is required'
245
+ });
246
+ }
247
+
248
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
249
+
250
+ if (!registry.routes.has(normalizedPath)) {
251
+ return res.status(404).json({
252
+ success: false,
253
+ error: 'Path not found'
254
+ });
255
+ }
256
+
257
+ const route = registry.routes.get(normalizedPath);
258
+ registry.routes.delete(normalizedPath);
259
+ registry.usedPorts.delete(route.port);
260
+
261
+ console.log(`[UNREGISTER] ${normalizedPath} (was on port ${route.port})`);
262
+
263
+ res.json({
264
+ success: true,
265
+ path: normalizedPath,
266
+ freedPort: route.port
267
+ });
268
+
269
+ } catch (error) {
270
+ console.error('[UNREGISTER ERROR]', error.message);
271
+ res.status(500).json({
272
+ success: false,
273
+ error: error.message
274
+ });
275
+ }
276
+ });
277
+
278
+ /**
279
+ * GET /proxy/routes
280
+ * Liste toutes les routes enregistrées
281
+ */
282
+ app.get('/proxy/routes', (req, res) => {
283
+ const routes = [];
284
+ registry.routes.forEach((value, path) => {
285
+ routes.push({
286
+ path,
287
+ port: value.port,
288
+ name: value.name,
289
+ registeredAt: value.registeredAt,
290
+ target: `http://localhost:${value.port}`
291
+ });
292
+ });
293
+
294
+ res.json({
295
+ count: routes.length,
296
+ routes,
297
+ availablePorts: (INTERNAL_PORT_END - INTERNAL_PORT_START) - registry.usedPorts.size
298
+ });
299
+ });
300
+
301
+ /**
302
+ * GET /proxy/health
303
+ * Health check du proxy
304
+ */
305
+ app.get('/proxy/health', (req, res) => {
306
+ res.json({
307
+ status: 'healthy',
308
+ uptime: process.uptime(),
309
+ registeredRoutes: registry.routes.size,
310
+ usedPorts: registry.usedPorts.size
311
+ });
312
+ });
313
+
314
+ // ============================================
315
+ // PROXY MIDDLEWARE
316
+ // ============================================
317
+
318
+ /**
319
+ * Router dynamique - détermine la cible en fonction de la requête
320
+ */
321
+ const dynamicRouter = (req) => {
322
+ const route = findRouteForPath(req.path);
323
+ if (route) {
324
+ return `http://localhost:${route.port}`;
325
+ }
326
+ return null;
327
+ };
328
+
329
+ /**
330
+ * Middleware de proxy pour toutes les autres requêtes
331
+ */
332
+ app.use((req, res, next) => {
333
+ // Ignore les routes de l'API proxy
334
+ if (req.path.startsWith('/proxy/')) {
335
+ return next();
336
+ }
337
+
338
+ const target = dynamicRouter(req);
339
+
340
+ if (!target) {
341
+ return res.status(404).json({
342
+ error: 'No route registered for this path',
343
+ path: req.path,
344
+ hint: 'Register a route with POST /proxy/register'
345
+ });
346
+ }
347
+
348
+ // Trouve le path de la route pour le pathRewrite
349
+ const route = findRouteForPath(req.path);
350
+ const routePath = [...registry.routes.entries()]
351
+ .find(([, v]) => v === route)?.[0];
352
+
353
+ const proxyMiddleware = createProxyMiddleware({
354
+ target,
355
+ changeOrigin: true,
356
+ pathRewrite: (path) => {
357
+ // Retire le préfixe de la route du path
358
+ if (routePath && path.startsWith(routePath)) {
359
+ const newPath = path.slice(routePath.length) || '/';
360
+ return newPath;
361
+ }
362
+ return path;
363
+ },
364
+ on: {
365
+ proxyReq: (proxyReq, req) => {
366
+ // Ré-écriture du body si présent (nécessaire car express.json() a consommé le stream)
367
+ if (req.body && Object.keys(req.body).length > 0) {
368
+ const bodyData = JSON.stringify(req.body);
369
+ proxyReq.setHeader('Content-Type', 'application/json');
370
+ proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
371
+ proxyReq.write(bodyData);
372
+ }
373
+ console.log(`[PROXY] ${req.method} ${req.path} -> ${target}`);
374
+ },
375
+ error: (err, req, res) => {
376
+ console.error(`[PROXY ERROR] ${req.path}:`, err.message);
377
+ res.status(502).json({
378
+ error: 'Proxy error',
379
+ message: err.message,
380
+ target
381
+ });
382
+ }
383
+ }
384
+ });
385
+
386
+ proxyMiddleware(req, res, next);
387
+ });
388
+
389
+ // ============================================
390
+ // DÉMARRAGE DU SERVEUR
391
+ // ============================================
392
+
393
+ app.listen(PROXY_PORT, () => {
394
+ console.log('='.repeat(50));
395
+ console.log('🚀 Dynamic Proxy Server');
396
+ console.log('='.repeat(50));
397
+ console.log(`Listening on port ${PROXY_PORT}`);
398
+ console.log(`Internal ports range: ${INTERNAL_PORT_START}-${INTERNAL_PORT_END}`);
399
+ console.log(`Health check interval: ${HEALTH_CHECK_INTERVAL}ms`);
400
+ console.log('');
401
+ console.log('API Endpoints:');
402
+ console.log(' POST /proxy/register - Register a new route');
403
+ console.log(' DELETE /proxy/unregister - Remove a route');
404
+ console.log(' GET /proxy/routes - List all routes');
405
+ console.log(' GET /proxy/health - Health check');
406
+ console.log('');
407
+ console.log('Note: Registered servers must implement GET /proxy/health');
408
+ console.log(' returning status 200 to stay registered.');
409
+ console.log('='.repeat(50));
410
+
411
+ // Démarre le polling de health check
412
+ startHealthCheckPolling();
413
+ });
414
+
415
+ module.exports = { app, registry };