Ajouter l'ingestion et la lecture des postes (POST /hosts/<id>, GET /hosts) #13

Closed
opened 2026-08-15 20:10:16 +00:00 by maximus · 0 comments
Owner

Ouvrir la premiere surface d'ecriture de l'API : les postes de travail poussent leur snapshot CPU / RAM / disque, le dashboard admin le relit date. Le routage passe d'une liste exacte a une table de descripteurs, en conservant un point de controle d'auth unique.

Contrat

POST /hosts/<id>Authorization: Bearer $HOSTS_INGEST_TOKEN. Corps JSON de la forme de GET /health moins logto et timestamp : hostname, uptime, cpu{model,cores,loadAvg,usagePercent}, memory{totalGB,usedGB,freeGB,usagePercent}, disk{...}.

Code Cas
204 Snapshot accepte et ecrit
400 JSON illisible, champ manquant ou de mauvais type
401 Token d'ingestion absent ou faux
403 <id> bien forme mais hors allowlist
404 <id> hors format (la requete n'atteint jamais le handler)
413 Corps au-dela de 4 Kio, connexion coupee
503 HOSTS_INGEST_TOKEN non configure (fail-closed)

GET /hostsAuthorization: Bearer $HEALTH_TOKEN. Retourne { staleAfterSeconds, hosts: [{ id, hostname, uptime, cpu, memory, disk, receivedAt, ageSeconds, online, neverSeen? }] }. online = ageSeconds <= 900. Horodatage pose par le SERVEUR a la reception, jamais par le client.

Fichiers concernes

  • index.js (modifier : routage, auth, deux routes)
  • .env.example (modifier)
  • CLAUDE.md (modifier)
  • README.md (modifier : table des endpoints, table de config, liste des bind-mounts)

Depends on

  • #11 (extraction metrics.js)
  • #12 (tests d'auth existants — condition d'entree)

Criteres d'acceptation

  • La requete est resolue en descripteur { method, handler, requiredToken }, refus par defaut si aucune route ne correspond, puis UN SEUL controle de token avant dispatch
  • Regex de route ^/hosts/([a-z0-9][a-z0-9-]{0,31})$ restreinte a POST, jamais elargie
  • tokenMatches() en timingSafeEqual sur empreintes SHA-256, applique au seul nouveau chemin d'ingestion
  • Corps plafonne a 4 Kio : 413 puis req.destroy() au-dela, sans se fier au Content-Length
  • Allowlist HOSTS_ALLOWED_IDS ; ses propres entrees validees par la meme regex au demarrage, entrees refusees journalisees et ecartees
  • Objet persiste RECONSTRUIT depuis une liste blanche de champs, nombres coerces via Number.isFinite, chaines plafonnees a 128 caracteres — jamais le payload verbatim
  • Ecriture atomique : fichier temporaire dans le meme repertoire puis renameSync
  • Chaque rejet 401 / 403 journalise avec X-Real-IP, l'identifiant et le motif
  • GET /hosts degrade une entree corrompue sans faire tomber la reponse entiere
  • HOSTS_DIR, HOSTS_ALLOWED_IDS, HOSTS_INGEST_TOKEN, HOSTS_STALE_SECONDS (defaut 900) dans .env.example
  • Le gotcha « read-only » du CLAUDE.md, devenu faux, est corrige
  • Les tests de #12 et les 14 tests existants passent inchanges

Review caveats

  • SECURITE + ARCHITECTURE (CRITIQUE) : passer d'un controle unique a un controle par branche est le changement le plus risque du lot. Garder UN point de passage, dont seul le token requis varie. Ne pas disperser l'auth dans chaque branche.
  • SECURITE + TECHNIQUE (CRITIQUE) : le 403 pour identifiant malforme est INATTEIGNABLE — verifie en Node, new URL() normalise /hosts/../../etc/passwd en /etc/passwd, et ..%2f comme THINKPAD echouent la regex. Tous tombent en 404. La regex etroite EST le controle anti-traversee : ne jamais l'elargir en ^/hosts/(.+)$ pour faire passer un test.
  • SECURITE (MEDIUM) : le fichier ecrit est relu puis fusionne par GET /hosts et finit dans l'arbre React de l'admin — d'ou la liste blanche de champs (une cle __proto__ ou un hostname de 4 Kio ne doit jamais y arriver).

Decisions prises en planification

  • Allowlist livree avec thinkpad seul : le Pop!_OS est hors scope.
  • L'etat neverSeen est conserve, mais justifie par le diagnostic de mise en service (entre le deploiement et le premier battement, la carte dit « agent non installe » au lieu de rester muette).
  • Le durcissement timing-safe du HEALTH_TOKEN existant est explicitement HORS de cette issue (issue separee) : il touche tous les chemins de lecture en production.

Spec source

la-compagnie-maximus/spec-plan-monitoring-postes.md + spec-decisions-monitoring-postes.md (depot different : ce body est auto-suffisant, ne pas compter sur le fichier) (Issue 3)

Ouvrir la premiere surface d'ecriture de l'API : les postes de travail poussent leur snapshot CPU / RAM / disque, le dashboard admin le relit date. Le routage passe d'une liste exacte a une table de descripteurs, en conservant un point de controle d'auth unique. ## Contrat `POST /hosts/<id>` — `Authorization: Bearer $HOSTS_INGEST_TOKEN`. Corps JSON de la forme de `GET /health` moins `logto` et `timestamp` : `hostname`, `uptime`, `cpu{model,cores,loadAvg,usagePercent}`, `memory{totalGB,usedGB,freeGB,usagePercent}`, `disk{...}`. | Code | Cas | |---|---| | 204 | Snapshot accepte et ecrit | | 400 | JSON illisible, champ manquant ou de mauvais type | | 401 | Token d'ingestion absent ou faux | | 403 | `<id>` bien forme mais hors allowlist | | 404 | `<id>` hors format (la requete n'atteint jamais le handler) | | 413 | Corps au-dela de 4 Kio, connexion coupee | | 503 | `HOSTS_INGEST_TOKEN` non configure (fail-closed) | `GET /hosts` — `Authorization: Bearer $HEALTH_TOKEN`. Retourne `{ staleAfterSeconds, hosts: [{ id, hostname, uptime, cpu, memory, disk, receivedAt, ageSeconds, online, neverSeen? }] }`. `online = ageSeconds <= 900`. Horodatage pose par le SERVEUR a la reception, jamais par le client. ## Fichiers concernes - `index.js` (modifier : routage, auth, deux routes) - `.env.example` (modifier) - `CLAUDE.md` (modifier) - `README.md` (modifier : table des endpoints, table de config, liste des bind-mounts) ## Depends on - #11 (extraction metrics.js) - #12 (tests d'auth existants — condition d'entree) ## Criteres d'acceptation - [ ] La requete est resolue en descripteur `{ method, handler, requiredToken }`, refus par defaut si aucune route ne correspond, puis UN SEUL controle de token avant dispatch - [ ] Regex de route `^/hosts/([a-z0-9][a-z0-9-]{0,31})$` restreinte a POST, jamais elargie - [ ] `tokenMatches()` en `timingSafeEqual` sur empreintes SHA-256, applique au seul nouveau chemin d'ingestion - [ ] Corps plafonne a 4 Kio : 413 puis `req.destroy()` au-dela, sans se fier au `Content-Length` - [ ] Allowlist `HOSTS_ALLOWED_IDS` ; ses propres entrees validees par la meme regex au demarrage, entrees refusees journalisees et ecartees - [ ] Objet persiste RECONSTRUIT depuis une liste blanche de champs, nombres coerces via `Number.isFinite`, chaines plafonnees a 128 caracteres — jamais le payload verbatim - [ ] Ecriture atomique : fichier temporaire dans le meme repertoire puis `renameSync` - [ ] Chaque rejet 401 / 403 journalise avec `X-Real-IP`, l'identifiant et le motif - [ ] `GET /hosts` degrade une entree corrompue sans faire tomber la reponse entiere - [ ] `HOSTS_DIR`, `HOSTS_ALLOWED_IDS`, `HOSTS_INGEST_TOKEN`, `HOSTS_STALE_SECONDS` (defaut 900) dans `.env.example` - [ ] Le gotcha « read-only » du CLAUDE.md, devenu faux, est corrige - [ ] Les tests de #12 et les 14 tests existants passent inchanges ## Review caveats - SECURITE + ARCHITECTURE (CRITIQUE) : passer d'un controle unique a un controle par branche est le changement le plus risque du lot. Garder UN point de passage, dont seul le token requis varie. Ne pas disperser l'auth dans chaque branche. - SECURITE + TECHNIQUE (CRITIQUE) : le 403 pour identifiant malforme est INATTEIGNABLE — verifie en Node, `new URL()` normalise `/hosts/../../etc/passwd` en `/etc/passwd`, et `..%2f` comme `THINKPAD` echouent la regex. Tous tombent en 404. La regex etroite EST le controle anti-traversee : ne jamais l'elargir en `^/hosts/(.+)$` pour faire passer un test. - SECURITE (MEDIUM) : le fichier ecrit est relu puis fusionne par `GET /hosts` et finit dans l'arbre React de l'admin — d'ou la liste blanche de champs (une cle `__proto__` ou un `hostname` de 4 Kio ne doit jamais y arriver). ## Decisions prises en planification - Allowlist livree avec `thinkpad` seul : le Pop!_OS est hors scope. - L'etat `neverSeen` est conserve, mais justifie par le diagnostic de mise en service (entre le deploiement et le premier battement, la carte dit « agent non installe » au lieu de rester muette). - Le durcissement timing-safe du `HEALTH_TOKEN` existant est explicitement HORS de cette issue (issue separee) : il touche tous les chemins de lecture en production. ## Spec source la-compagnie-maximus/spec-plan-monitoring-postes.md + spec-decisions-monitoring-postes.md (depot different : ce body est auto-suffisant, ne pas compter sur le fichier) (Issue 3)
maximus added this to the planned-2026-08-15-monitoring-postes milestone 2026-08-15 20:10:16 +00:00
maximus added the
status:ready
type:feature
source:human
labels 2026-08-15 20:10:16 +00:00
maximus added
status:review
and removed
status:ready
labels 2026-08-16 15:50:44 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-08-16 18:12:47 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#13
No description provided.