dynamic-self-register-proxy 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +45 -1
  2. package/package.json +1 -1
  3. package/proxy.js +165 -6
package/README.md CHANGED
@@ -75,10 +75,44 @@ app.listen(port);
75
75
  http://localhost:3000/myapp/ → votre application sur le port 4000
76
76
  ```
77
77
 
78
+ ## Page d'accueil
79
+
80
+ L'URL racine `/` affiche une page listant tous les serveurs enregistrés avec des liens pour y accéder.
81
+
82
+ - **Navigateurs** (`Accept: text/html`) → Page HTML avec interface moderne
83
+ - **Clients API** (`Accept: application/json`) → Réponse JSON
84
+
85
+ ```bash
86
+ # Accès navigateur : ouvrir http://localhost:3000/
87
+
88
+ # Accès API
89
+ curl -H "Accept: application/json" http://localhost:3000/
90
+ ```
91
+
92
+ **Réponse JSON :**
93
+ ```json
94
+ {
95
+ "status": "healthy",
96
+ "uptime": 3600,
97
+ "count": 2,
98
+ "routes": [
99
+ {
100
+ "path": "/api",
101
+ "port": 4000,
102
+ "name": "API Server",
103
+ "registeredAt": "2026-01-27T10:00:00.000Z",
104
+ "target": "http://localhost:4000"
105
+ }
106
+ ],
107
+ "availablePorts": 998
108
+ }
109
+ ```
110
+
78
111
  ## API du Proxy
79
112
 
80
113
  | Endpoint | Méthode | Description |
81
114
  |----------|---------|-------------|
115
+ | `/` | GET | Page d'accueil (HTML ou JSON selon Accept header) |
82
116
  | `/proxy/register` | POST | Enregistre une nouvelle route |
83
117
  | `/proxy/unregister` | DELETE | Supprime une route |
84
118
  | `/proxy/routes` | GET | Liste toutes les routes |
@@ -332,6 +366,7 @@ proxy.setupHealthRoute(app, {
332
366
 
333
367
  ### Comportement
334
368
 
369
+ - **Période de grâce** : les nouveaux serveurs ont **60 secondes** après l'enregistrement avant d'être vérifiés (laisse le temps au serveur de démarrer)
335
370
  - Le proxy vérifie tous les serveurs toutes les **30 secondes** (par défaut)
336
371
  - Timeout de **5 secondes** par requête de health check
337
372
  - Si la réponse n'est pas un code **200**, le serveur est automatiquement désenregistré
@@ -388,7 +423,8 @@ Dans votre fichier de configuration Claude Desktop (`claude_desktop_config.json`
388
423
  "env": {
389
424
  "PROXY_PORT": "8080",
390
425
  "HEALTH_CHECK_INTERVAL": "60000",
391
- "HEALTH_CHECK_TIMEOUT": "10000"
426
+ "HEALTH_CHECK_TIMEOUT": "10000",
427
+ "HEALTH_CHECK_GRACE_PERIOD": "120000"
392
428
  }
393
429
  }
394
430
  }
@@ -402,6 +438,7 @@ Dans votre fichier de configuration Claude Desktop (`claude_desktop_config.json`
402
438
  | `PROXY_PORT` | `3000` | Port d'écoute du proxy |
403
439
  | `HEALTH_CHECK_INTERVAL` | `30000` | Intervalle du health check polling (ms) |
404
440
  | `HEALTH_CHECK_TIMEOUT` | `5000` | Timeout pour chaque health check (ms) |
441
+ | `HEALTH_CHECK_GRACE_PERIOD` | `60000` | Période de grâce pour les nouveaux serveurs (ms) |
405
442
  | `PROXY_URL` | `http://localhost:3000` | URL du proxy (pour les apps) |
406
443
  | `APP_PATH` | `/example` | Chemin de l'application |
407
444
  | `APP_NAME` | `Example App` | Nom de l'application |
@@ -417,6 +454,13 @@ const INTERNAL_PORT_START = 4000;
417
454
  const INTERNAL_PORT_END = 5000;
418
455
  ```
419
456
 
457
+ ## Fonctionnalités
458
+
459
+ - **Attribution sécurisée des ports** : utilisation d'un mutex pour éviter les conflits lors d'enregistrements simultanés
460
+ - **Période de grâce** : les nouveaux serveurs ne sont pas vérifiés immédiatement, leur laissant le temps de démarrer
461
+ - **Négociation de contenu** : l'URL racine `/` retourne HTML pour les navigateurs, JSON pour les API
462
+ - **Health check automatique** : désenregistrement automatique des serveurs non répondants
463
+
420
464
  ## Limitations
421
465
 
422
466
  - Les routes sont stockées en mémoire (perdues au redémarrage du proxy)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dynamic-self-register-proxy",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Dynamic reverse proxy with self-registration API - applications can register themselves and receive an automatically assigned port",
5
5
  "main": "proxy-client.js",
6
6
  "bin": {
package/proxy.js CHANGED
@@ -9,11 +9,12 @@ app.use(express.json());
9
9
  // ============================================
10
10
  // CONFIGURATION
11
11
  // ============================================
12
- const PROXY_PORT = process.env.PROXY_PORT || 3005;
12
+ const PROXY_PORT = process.env.PROXY_PORT || 3000;
13
13
  const INTERNAL_PORT_START = 4000;
14
14
  const INTERNAL_PORT_END = 5000;
15
15
  const HEALTH_CHECK_INTERVAL = process.env.HEALTH_CHECK_INTERVAL || 30000; // 30 secondes par défaut
16
16
  const HEALTH_CHECK_TIMEOUT = process.env.HEALTH_CHECK_TIMEOUT || 5000; // 5 secondes timeout
17
+ const HEALTH_CHECK_GRACE_PERIOD = process.env.HEALTH_CHECK_GRACE_PERIOD || 60000; // 60 secondes de grâce pour les nouveaux serveurs
17
18
 
18
19
  // ============================================
19
20
  // REGISTRY - Stockage des routes en mémoire
@@ -166,19 +167,36 @@ async function unregisterServer(path) {
166
167
  }
167
168
  }
168
169
 
170
+ /**
171
+ * Vérifie si un serveur est encore dans sa période de grâce
172
+ * @param {object} route - Les informations de la route
173
+ * @returns {boolean} - true si le serveur est encore en période de grâce
174
+ */
175
+ function isInGracePeriod(route) {
176
+ const registeredAt = new Date(route.registeredAt).getTime();
177
+ const now = Date.now();
178
+ return (now - registeredAt) < HEALTH_CHECK_GRACE_PERIOD;
179
+ }
180
+
169
181
  /**
170
182
  * Effectue un health check sur tous les serveurs enregistrés
183
+ * Les serveurs récemment enregistrés (dans la période de grâce) sont ignorés
171
184
  */
172
185
  async function performHealthChecks() {
173
186
  if (registry.routes.size === 0) {
174
187
  return;
175
188
  }
176
189
 
177
- console.log(`[HEALTH CHECK] Checking ${registry.routes.size} registered server(s)...`);
178
-
179
190
  const checks = [];
191
+ let skippedCount = 0;
180
192
 
181
193
  for (const [path, route] of registry.routes.entries()) {
194
+ // Ignore les serveurs en période de grâce
195
+ if (isInGracePeriod(route)) {
196
+ skippedCount++;
197
+ continue;
198
+ }
199
+
182
200
  checks.push(
183
201
  checkServerHealth(path, route).then(isHealthy => ({
184
202
  path,
@@ -188,6 +206,16 @@ async function performHealthChecks() {
188
206
  );
189
207
  }
190
208
 
209
+ // Si tous les serveurs sont en période de grâce, pas de log
210
+ if (checks.length === 0) {
211
+ if (skippedCount > 0) {
212
+ console.log(`[HEALTH CHECK] ${skippedCount} server(s) in grace period, skipping checks`);
213
+ }
214
+ return;
215
+ }
216
+
217
+ console.log(`[HEALTH CHECK] Checking ${checks.length} server(s)...${skippedCount > 0 ? ` (${skippedCount} in grace period)` : ''}`);
218
+
191
219
  const results = await Promise.all(checks);
192
220
 
193
221
  for (const { path, isHealthy } of results) {
@@ -385,6 +413,52 @@ app.get('/proxy/routes', (req, res) => {
385
413
  });
386
414
  });
387
415
 
416
+ /**
417
+ * POST /proxy/check
418
+ * Déclenche manuellement un health check pour une route spécifique
419
+ * Body: { path: "/myapp" }
420
+ */
421
+ app.post('/proxy/check', async (req, res) => {
422
+ const { path } = req.body;
423
+
424
+ if (!path) {
425
+ return res.status(400).json({
426
+ success: false,
427
+ error: 'Path is required'
428
+ });
429
+ }
430
+
431
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
432
+ const route = registry.routes.get(normalizedPath);
433
+
434
+ if (!route) {
435
+ return res.status(404).json({
436
+ success: false,
437
+ error: 'Path not found'
438
+ });
439
+ }
440
+
441
+ console.log(`[MANUAL HEALTH CHECK] Checking ${normalizedPath}...`);
442
+ const isHealthy = await checkServerHealth(normalizedPath, route);
443
+
444
+ if (!isHealthy) {
445
+ await unregisterServer(normalizedPath);
446
+ return res.json({
447
+ success: true,
448
+ path: normalizedPath,
449
+ healthy: false,
450
+ message: 'Server was unhealthy and has been removed'
451
+ });
452
+ }
453
+
454
+ res.json({
455
+ success: true,
456
+ path: normalizedPath,
457
+ healthy: true,
458
+ message: 'Server is healthy'
459
+ });
460
+ });
461
+
388
462
  /**
389
463
  * GET /proxy/health
390
464
  * Health check du proxy
@@ -553,6 +627,40 @@ app.get('/', (req, res) => {
553
627
  .service-link:hover {
554
628
  opacity: 0.9;
555
629
  }
630
+ .service-actions {
631
+ display: flex;
632
+ gap: 1rem;
633
+ align-items: center;
634
+ }
635
+ .check-btn {
636
+ background: rgba(255, 255, 255, 0.1);
637
+ color: #e4e4e7;
638
+ border: 1px solid rgba(255, 255, 255, 0.2);
639
+ padding: 0.5rem 1rem;
640
+ border-radius: 8px;
641
+ cursor: pointer;
642
+ font-size: 0.85rem;
643
+ font-weight: 500;
644
+ transition: all 0.2s;
645
+ }
646
+ .check-btn:hover {
647
+ background: rgba(255, 255, 255, 0.15);
648
+ border-color: rgba(255, 255, 255, 0.3);
649
+ }
650
+ .check-btn.loading {
651
+ opacity: 0.5;
652
+ cursor: not-allowed;
653
+ }
654
+ .check-btn.healthy {
655
+ background: rgba(16, 185, 129, 0.2);
656
+ border-color: rgba(16, 185, 129, 0.4);
657
+ color: #10b981;
658
+ }
659
+ .check-btn.unhealthy {
660
+ background: rgba(239, 68, 68, 0.2);
661
+ border-color: rgba(239, 68, 68, 0.4);
662
+ color: #ef4444;
663
+ }
556
664
  .empty-state {
557
665
  text-align: center;
558
666
  padding: 4rem 2rem;
@@ -620,9 +728,14 @@ app.get('/', (req, res) => {
620
728
  <span>Port interne: ${route.port}</span>
621
729
  <span>Enregistré: ${new Date(route.registeredAt).toLocaleString('fr-FR')}</span>
622
730
  </div>
623
- <a href="${escapeHtml(route.path)}" class="service-link">
624
- Accéder au service
625
- </a>
731
+ <div class="service-actions">
732
+ <a href="${escapeHtml(route.path)}" class="service-link">
733
+ Accéder au service →
734
+ </a>
735
+ <button onclick="checkHealth('${escapeHtml(route.path)}', this)" class="check-btn">
736
+ Vérifier la santé
737
+ </button>
738
+ </div>
626
739
  </div>
627
740
  `).join('')}
628
741
  </div>
@@ -638,6 +751,51 @@ app.get('/', (req, res) => {
638
751
  <p>API: <a href="/proxy/routes">/proxy/routes</a> | <a href="/proxy/health">/proxy/health</a></p>
639
752
  </footer>
640
753
  </div>
754
+
755
+ <script>
756
+ async function checkHealth(path, btn) {
757
+ if (btn.classList.contains('loading')) return;
758
+
759
+ const originalText = btn.innerText;
760
+ btn.innerText = 'Vérification...';
761
+ btn.classList.add('loading');
762
+
763
+ try {
764
+ const response = await fetch('/proxy/check', {
765
+ method: 'POST',
766
+ headers: { 'Content-Type': 'application/json' },
767
+ body: JSON.stringify({ path })
768
+ });
769
+
770
+ const data = await response.json();
771
+
772
+ if (data.healthy) {
773
+ btn.innerText = '✅ Sain';
774
+ btn.classList.remove('loading');
775
+ btn.classList.add('healthy');
776
+ setTimeout(() => {
777
+ btn.innerText = originalText;
778
+ btn.classList.remove('healthy');
779
+ }, 3000);
780
+ } else {
781
+ btn.innerText = '❌ Hors ligne';
782
+ btn.classList.remove('loading');
783
+ btn.classList.add('unhealthy');
784
+
785
+ // Si le serveur a été supprimé, on rafraîchit la liste après un court délai
786
+ setTimeout(() => {
787
+ window.location.reload();
788
+ }, 2000);
789
+ }
790
+ } catch (error) {
791
+ btn.innerText = '⚠️ Erreur';
792
+ btn.classList.remove('loading');
793
+ setTimeout(() => {
794
+ btn.innerText = originalText;
795
+ }, 3000);
796
+ }
797
+ }
798
+ </script>
641
799
  </body>
642
800
  </html>
643
801
  `.trim();
@@ -745,6 +903,7 @@ app.listen(PROXY_PORT, () => {
745
903
  console.log(`Listening on port ${PROXY_PORT}`);
746
904
  console.log(`Internal ports range: ${INTERNAL_PORT_START}-${INTERNAL_PORT_END}`);
747
905
  console.log(`Health check interval: ${HEALTH_CHECK_INTERVAL}ms`);
906
+ console.log(`Health check grace period: ${HEALTH_CHECK_GRACE_PERIOD}ms`);
748
907
  console.log('');
749
908
  console.log('API Endpoints:');
750
909
  console.log(' POST /proxy/register - Register a new route');