Skip to content

Server-Sent Events (SSE) — streams temps réel

StormeoOS expose plusieurs streams SSE pour pousser des données en continu vers le navigateur ou un client curl, sans poll.

Streams disponibles

EndpointAuthCas d'usage
GET /api/monitoring-streamSession cookieLive feed monitoring (uptime, métriques, alertes)
POST /api/storm-scan (SSE response)Session cookieProgression d'un scan StormScan (parc)
POST /api/site-analysis-scans (SSE response)Session cookieProgression scan analyse de site
POST /api/public/scan (SSE response)(aucune, IP rate-limited)Scan public anonyme depuis /scan

Format SSE

Conforme à la spec W3C Server-Sent Events. Chaque message :

event: <event-name>
data: <JSON-stringified>
id: <optional sequence id>

(Double \n\n à la fin de chaque message — le id est optionnel.)

Connexion

javascript
const evtSource = new EventSource('/api/monitoring-stream', {
  withCredentials: true   // pour envoyer le cookie de session
});

evtSource.addEventListener('alert', (e) => {
  const data = JSON.parse(e.data);
  console.log('Alerte:', data);
});

evtSource.addEventListener('metric', (e) => {
  const data = JSON.parse(e.data);
  // ... mise à jour graph
});

evtSource.onerror = (err) => {
  console.error('SSE error', err);
  // EventSource reconnecte automatiquement avec backoff
};

curl

bash
curl -N \
  --cookie "connect.sid=<session>" \
  https://beta.stormeo.io/api/monitoring-stream

-N = no buffer (essentiel pour SSE).

Node.js (sans EventSource natif)

javascript
import { EventSource } from 'eventsource'; // npm install eventsource

const es = new EventSource('https://beta.stormeo.io/api/monitoring-stream', {
  fetch: (url, init) => fetch(url, {
    ...init,
    headers: { ...init.headers, Cookie: `connect.sid=${session}` }
  })
});

/api/monitoring-stream

Stream live des events monitoring de l'agence.

Auth

Session cookie StormeoOS (login standard). Pas accessible via clé API publique.

Events émis

event:data:
connected{ "agencyId": 7, "ts": "2026-04-27T10:00:00Z" } (envoyé à l'open)
metric.cpu{ "siteId": "uuid", "value": 42.5, "unit": "%" }
metric.memory{ "siteId": "uuid", "valueBytes": 1234567 }
metric.disk{ "siteId": "uuid", "usedBytes": ..., "totalBytes": ... }
uptime.up{ "siteId": "uuid", "responseTimeMs": 234 }
uptime.down{ "siteId": "uuid", "lastSeenUp": "2026-04-27T09:55:00Z" }
alert{ "alertId": 42, "severity": "critical", "message": "..." }
heartbeat{} (toutes les 30s pour maintenir la connexion)

Reconnexion

Si la connexion casse (timeout reverse-proxy, network blip), EventSource reconnecte automatiquement avec backoff exponentiel. Vous pouvez stocker Last-Event-ID côté client et le repasser dans le header Last-Event-ID au reconnect (le serveur reprendra depuis ce point — best-effort).


POST /api/storm-scan (SSE)

Progression d'un scan StormScan. Connexion POST avec body JSON, réponse en SSE.

Body

json
{
  "url": "https://acme.fr",
  "mode": "parc",                    // ou "external"
  "modules": ["security", "seo", "performance"],
  "scope": "parc",                   // ou "external" / "public"
  "agencyId": 7
}

Events émis

event:data:
start{ "scanId": 123 }
module.start{ "module": "security" }
module.progress{ "module": "security", "progress": 0.42, "current": "Analyse headers HTTP" }
module.complete{ "module": "security", "score": 85, "issues": [...] }
complete{ "scanId": 123, "totalScore": 78, "duration_ms": 18200 }
error{ "code": "FETCH_FAILED", "message": "..." }

SSRF guard

Toute URL passée est validée par assertSafeUrl (cf. server/lib/ssrf-guard.ts) :

  • Refus de .local, .internal, .lan
  • Refus de IP RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Refus de loopback (127.0.0.0/8, ::1)
  • Refus de cloud metadata (169.254.169.254, etc.)
  • DNS resolve { all: true } pour anti-rebinding

→ Réponse 400 immédiate (avant SSE start) si bloqué.

Rate limit

stormScanRateLimiter : 10 scans / heure par agence (configurable). Réponse 429 avec header Retry-After si dépassé.


POST /api/public/scan (SSE — anonyme)

Scan public depuis la page marketing /scan — pas d'auth, rate limit par IP.

Body

json
{
  "url": "https://acme.fr"
}

Rate limits

  • 10 scans / heure / IP
  • 5 lead captures / heure / IP

429 si dépassé. Headers Retry-After + X-RateLimit-Remaining.

Events émis

Identiques au scan authentifié, plus :

event:data:
lead.required{ "scanId": 123, "previewScore": 78 } (à mi-parcours, demande email avant détails)

Workflow :

  1. POST avec URL → SSE démarre
  2. Le scan tourne avec preview gratuite (score global + tuiles)
  3. Event lead.required → le client doit POST l'email RGPD (/api/public/scan/:scanId/lead) pour accéder aux détails
  4. Si email valide → SSE continue avec module.complete détaillés
  5. Sinon timeout 60s → connexion close

Limites communes SSE

Reverse proxy

Traefik en prod a responseTimeout: 0 pour /api/monitoring-stream et /api/*-scan (route-level) — sinon le proxy coupe à 30s.

Si vous self-hostez derrière un proxy custom (nginx, Cloudflare), assurez-vous de :

  • Désactiver le buffering : proxy_buffering off (nginx)
  • Augmenter proxy_read_timeout à au moins 5 min
  • Cloudflare : SSE supporté nativement, mais peut avoir des comportements bizarres avec Polish/Rocket Loader — tester

Connexions concurrentes

Limite navigateur : ~6 connexions SSE par origin (HTTP/1.1). Avec HTTP/2 (cas StormeoOS prod), limite multiplexée largement plus élevée. Ne pas ouvrir plus d'1 stream /api/monitoring-stream par onglet — utilisez un Service Worker pour partager une connexion entre onglets si besoin.

Fallback

EventSource n'est pas supporté sur IE/old Edge. Fallback : long-polling via lib eventsource-polyfill.

Authentification — note importante

Les streams /api/monitoring-stream et scan-internes ne supportent pas l'auth par clé API publique (spk_*). Ils requièrent une session cookie StormeoOS classique.

Roadmap : exposition possible de streams via Public API (auth par clé) en v2 — pas planifié actuellement.

StormeoOS API