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
| Endpoint | Auth | Cas d'usage |
|---|---|---|
GET /api/monitoring-stream | Session cookie | Live feed monitoring (uptime, métriques, alertes) |
POST /api/storm-scan (SSE response) | Session cookie | Progression d'un scan StormScan (parc) |
POST /api/site-analysis-scans (SSE response) | Session cookie | Progression 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
Navigateur (EventSource)
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
curl -N \
--cookie "connect.sid=<session>" \
https://beta.stormeo.io/api/monitoring-stream
-N= no buffer (essentiel pour SSE).
Node.js (sans EventSource natif)
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
{
"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
{
"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 :
- POST avec URL → SSE démarre
- Le scan tourne avec preview gratuite (score global + tuiles)
- Event
lead.required→ le client doit POST l'email RGPD (/api/public/scan/:scanId/lead) pour accéder aux détails - Si email valide → SSE continue avec
module.completedétaillés - 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.