feat: ingestion et lecture des postes (POST /hosts/<id>, GET /hosts) (#13) #20

Closed
maximus wants to merge 1 commit from issue-13-hosts-endpoints into issue-12-auth-tests
Owner

Maillon 3 de la pile chainee — base issue-12-auth-tests, pas main.

Ouvre la premiere surface d'ecriture de l'API : les postes poussent leur snapshot CPU / RAM / disque, le dashboard admin le relit date.

Ce qui change dans le routage

Le routage passe d'une liste de chemins exacts a une table de descripteurs, en gardant un seul point de controle d'auth :

resolveRoute()  -> aucun match = 404 immediat (refus par defaut)
checkAuth()     -> LE point de passage unique ; seul le token attendu varie
                   selon le descripteur (HEALTH_TOKEN en lecture,
                   HOSTS_INGEST_TOKEN en ingestion)
dispatch

L'ordre routage -> auth est preserve intentionnellement. Une route inconnue ou une mauvaise methode repond toujours 404, jamais 401 — comportement de production inchange. Les deux tests 404 (not 401) poses par #12 passent tels quels (verifies nommement).

Contrat

Code POST /hosts/<id>
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 — n'atteint jamais le handler
413 Corps au-dela de 4 Kio, connexion coupee
503 HOSTS_INGEST_TOKEN non configure (fail-closed)

GET /hosts (token de lecture) retourne { staleAfterSeconds, hosts: [...] }, une entree par identifiant de l'allowlist, online calcule cote serveur.

Controles de securite

  • La regex ^/hosts/([a-z0-9][a-z0-9-]{0,31})$, restreinte a POST, est le controle anti-traversee. Verifie en Node et contre un vrai serveur : /hosts/../../etc/passwd est normalise par URL() en /etc/passwd, ..%2f..%2f / THINKPAD / /hosts/ / -lead / 33 caracteres echouent la regex — tous tombent en 404. Le 403 reste reserve aux identifiants bien formes hors allowlist. Source unique HOST_ID_PATTERN : la regex de route et celle de l'allowlist ne peuvent pas deriver l'une de l'autre.
  • 401 avant 403 : un appelant sans token ne peut pas enumerer les identifiants valides.
  • tokenMatches() compare des empreintes SHA-256 avec timingSafeEqual (longueurs toujours egales, pas de fuite de longueur). Applique au seul chemin d'ingestion — realigner le HEALTH_TOKEN existant toucherait tous les chemins de lecture en production (issue separee). Commentaire pose dans le code pour eviter qu'un relecteur « corrige » l'asymetrie.
  • Plafond de corps a 4 Kio sur les octets recus, jamais sur le Content-Length (un client peut mentir, le chunke l'omet). Au-dela : 413 puis req.destroy() une fois la reponse ecoulee — detruire avant le flush laisserait le client avec une erreur reseau au lieu du code.
  • Objet persiste reconstruit champ par champ depuis une liste blanche (nombres via Number.isFinite, chaines plafonnees a 128 caracteres, tout le reste jete). Le fichier est relu par GET /hosts et finit dans l'arbre React de l'admin. La relecture repasse par la meme liste blanche : HOSTS_DIR est un montage en lecture-ecriture, le fichier sur disque n'est pas plus digne de confiance que le payload.
  • Ecriture atomique : fichier temporaire dans le meme repertoire puis renameSync, pour qu'un GET /hosts concurrent ne voie jamais un JSON tronque.
  • HOSTS_ALLOWED_IDS valide ses propres entrees au demarrage avec la meme regex ; les entrees refusees (../defenseurs/status, POPOS) sont journalisees et ecartees au lieu de devenir des chemins de fichier.
  • Chaque rejet 401/403 est journalise avec X-Real-IP, l'identifiant et le motif. Les valeurs sont filtrees en ASCII imprimable pour empecher la forge de lignes de log.
  • receivedAt est pose par le serveur a la reception ; un horodatage fourni par le client est ecarte par la liste blanche (test dedie).

Tests

61 au total — les 39 existants inchanges + 22 nouveaux dans __tests__/hosts.test.js.

La suite ajoutee est volontairement du smoke, pas la matrice exhaustive (c'est l'issue #14) : elle epingle les decisions couteuses a perdre en silence — ordre de routage, regex anti-traversee, 401 avant 403, cloisonnement des deux tokens, 503 fail-closed, reconstruction par liste blanche (__proto__ et hostname de 300 caracteres inclus), 413, neverSeen, fraicheur serveur, entree corrompue degradee, validation de l'allowlist au demarrage.

Points a relire en priorite

  1. La journalisation couvre TOUS les rejets 401/403, y compris ceux des routes de lecture existantes, pas seulement l'ingestion. Le critere de l'issue dit « chaque rejet 401/403 » sans qualifier, et le point de passage unique fait qu'il n'y a qu'un seul site d'appel. Effet de bord : ~12 lignes stderr de plus pendant auth.test.js, et un peu plus de volume de log en prod. A trancher si ce n'etait pas l'intention.
  2. Une entree corrompue de GET /hosts degrade en neverSeen: true plutot qu'en un champ error supplementaire — la forme de reponse contractee ne prevoit que neverSeen?, et le dashboard affiche alors « agent non installe » au lieu de planter sur un champ inconnu. Le detail reel part dans les logs serveur.
  3. HOSTS_DIR par defaut /data/hosts (non specifie par l'issue), aligne sur la convention des montages existants.
  4. Deploiement : ce montage doit etre en lecture-ecriture pour l'uid 1000 (node, l'utilisateur du conteneur), contrairement aux montages /data/defenseurs. Sinon chaque ingestion repond 500. Documente dans le README et le CLAUDE.md.
  5. HOSTS_INGEST_TOKEN a poser sur Coolify en is_runtime=true, is_buildtime=false, meme regle que HEALTH_TOKEN.

Docs

  • .env.example : HOSTS_DIR, HOSTS_ALLOWED_IDS, HOSTS_INGEST_TOKEN, HOSTS_STALE_SECONDS
  • README.md : table des endpoints avec la colonne token, contrat de POST /hosts/<id> et GET /hosts, table de config, bind-mounts scindes en lecture seule / lecture-ecriture
  • CLAUDE.md : endpoints, nouvelle section « Routage et auth » qui documente l'ordre et le point de controle unique, config, compte de tests, et le gotcha « read-only » corrige — l'API n'est plus en lecture seule

Aucun module runtime ajoute a la racine (node:crypto est un builtin), donc le COPY explicite du Dockerfile reste valide.

Resolves #13


Generated autonomously by /autopilot run of 2026-08-16

Maillon 3 de la pile chainee — base `issue-12-auth-tests`, pas `main`. Ouvre la premiere surface d'ecriture de l'API : les postes poussent leur snapshot CPU / RAM / disque, le dashboard admin le relit date. ## Ce qui change dans le routage Le routage passe d'une liste de chemins exacts a une table de descripteurs, en gardant **un seul** point de controle d'auth : ``` resolveRoute() -> aucun match = 404 immediat (refus par defaut) checkAuth() -> LE point de passage unique ; seul le token attendu varie selon le descripteur (HEALTH_TOKEN en lecture, HOSTS_INGEST_TOKEN en ingestion) dispatch ``` **L'ordre routage -> auth est preserve intentionnellement.** Une route inconnue ou une mauvaise methode repond toujours 404, jamais 401 — comportement de production inchange. Les deux tests `404 (not 401)` poses par #12 passent tels quels (verifies nommement). ## Contrat | Code | `POST /hosts/<id>` | |---|---| | 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 — n'atteint jamais le handler | | 413 | Corps au-dela de 4 Kio, connexion coupee | | 503 | `HOSTS_INGEST_TOKEN` non configure (fail-closed) | `GET /hosts` (token de lecture) retourne `{ staleAfterSeconds, hosts: [...] }`, une entree par identifiant de l'allowlist, `online` calcule cote serveur. ## Controles de securite - La regex `^/hosts/([a-z0-9][a-z0-9-]{0,31})$`, restreinte a POST, **est** le controle anti-traversee. Verifie en Node et contre un vrai serveur : `/hosts/../../etc/passwd` est normalise par `URL()` en `/etc/passwd`, `..%2f..%2f` / `THINKPAD` / `/hosts/` / `-lead` / 33 caracteres echouent la regex — tous tombent en 404. Le 403 reste reserve aux identifiants bien formes hors allowlist. Source unique `HOST_ID_PATTERN` : la regex de route et celle de l'allowlist ne peuvent pas deriver l'une de l'autre. - **401 avant 403** : un appelant sans token ne peut pas enumerer les identifiants valides. - `tokenMatches()` compare des empreintes SHA-256 avec `timingSafeEqual` (longueurs toujours egales, pas de fuite de longueur). **Applique au seul chemin d'ingestion** — realigner le `HEALTH_TOKEN` existant toucherait tous les chemins de lecture en production (issue separee). Commentaire pose dans le code pour eviter qu'un relecteur « corrige » l'asymetrie. - Plafond de corps a 4 Kio sur les octets **recus**, jamais sur le `Content-Length` (un client peut mentir, le chunke l'omet). Au-dela : 413 puis `req.destroy()` **une fois la reponse ecoulee** — detruire avant le flush laisserait le client avec une erreur reseau au lieu du code. - Objet persiste **reconstruit champ par champ** depuis une liste blanche (nombres via `Number.isFinite`, chaines plafonnees a 128 caracteres, tout le reste jete). Le fichier est relu par `GET /hosts` et finit dans l'arbre React de l'admin. La relecture repasse par la meme liste blanche : `HOSTS_DIR` est un montage en lecture-ecriture, le fichier sur disque n'est pas plus digne de confiance que le payload. - Ecriture atomique : fichier temporaire dans le **meme** repertoire puis `renameSync`, pour qu'un `GET /hosts` concurrent ne voie jamais un JSON tronque. - `HOSTS_ALLOWED_IDS` valide ses propres entrees au demarrage avec la meme regex ; les entrees refusees (` ../defenseurs/status `, `POPOS`) sont journalisees et ecartees au lieu de devenir des chemins de fichier. - Chaque rejet 401/403 est journalise avec `X-Real-IP`, l'identifiant et le motif. Les valeurs sont filtrees en ASCII imprimable pour empecher la forge de lignes de log. - `receivedAt` est pose par le **serveur** a la reception ; un horodatage fourni par le client est ecarte par la liste blanche (test dedie). ## Tests 61 au total — les 39 existants inchanges + 22 nouveaux dans `__tests__/hosts.test.js`. La suite ajoutee est volontairement du smoke, pas la matrice exhaustive (c'est l'issue #14) : elle epingle les decisions couteuses a perdre en silence — ordre de routage, regex anti-traversee, 401 avant 403, cloisonnement des deux tokens, 503 fail-closed, reconstruction par liste blanche (`__proto__` et `hostname` de 300 caracteres inclus), 413, `neverSeen`, fraicheur serveur, entree corrompue degradee, validation de l'allowlist au demarrage. ## Points a relire en priorite 1. **La journalisation couvre TOUS les rejets 401/403**, y compris ceux des routes de lecture existantes, pas seulement l'ingestion. Le critere de l'issue dit « chaque rejet 401/403 » sans qualifier, et le point de passage unique fait qu'il n'y a qu'un seul site d'appel. Effet de bord : ~12 lignes stderr de plus pendant `auth.test.js`, et un peu plus de volume de log en prod. A trancher si ce n'etait pas l'intention. 2. **Une entree corrompue de `GET /hosts` degrade en `neverSeen: true`** plutot qu'en un champ `error` supplementaire — la forme de reponse contractee ne prevoit que `neverSeen?`, et le dashboard affiche alors « agent non installe » au lieu de planter sur un champ inconnu. Le detail reel part dans les logs serveur. 3. **`HOSTS_DIR` par defaut `/data/hosts`** (non specifie par l'issue), aligne sur la convention des montages existants. 4. **Deploiement** : ce montage doit etre en lecture-ecriture pour l'uid 1000 (`node`, l'utilisateur du conteneur), contrairement aux montages `/data/defenseurs`. Sinon chaque ingestion repond 500. Documente dans le README et le CLAUDE.md. 5. **`HOSTS_INGEST_TOKEN` a poser sur Coolify** en `is_runtime=true, is_buildtime=false`, meme regle que `HEALTH_TOKEN`. ## Docs - `.env.example` : `HOSTS_DIR`, `HOSTS_ALLOWED_IDS`, `HOSTS_INGEST_TOKEN`, `HOSTS_STALE_SECONDS` - `README.md` : table des endpoints avec la colonne token, contrat de `POST /hosts/<id>` et `GET /hosts`, table de config, bind-mounts scindes en lecture seule / lecture-ecriture - `CLAUDE.md` : endpoints, nouvelle section « Routage et auth » qui documente l'ordre et le point de controle unique, config, compte de tests, et **le gotcha « read-only » corrige** — l'API n'est plus en lecture seule Aucun module runtime ajoute a la racine (`node:crypto` est un builtin), donc le `COPY` explicite du Dockerfile reste valide. Resolves #13 --- Generated autonomously by /autopilot run of 2026-08-16
maximus added 1 commit 2026-08-16 15:50:28 +00:00
Open the API's first write surface. Workstations push their CPU / memory /
disk snapshot, the admin dashboard reads it back with a server-computed
freshness.

Routing moves from an exact path list to a descriptor table, keeping ONE
authentication checkpoint:

  resolveRoute()  -> no match means an immediate 404, deny by default
  checkAuth()     -> the single gate; only the expected token varies per
                     descriptor (HEALTH_TOKEN for reads, HOSTS_INGEST_TOKEN
                     for ingestion)
  dispatch

The routing-before-auth ordering is preserved on purpose: an unknown path or
a wrong method still answers 404, never 401, exactly as before. The two
`404 (not 401)` tests added in #12 pin that ordering and still pass.

Security controls on the new write path:

- The route regex ^/hosts/([a-z0-9][a-z0-9-]{0,31})$, POST-only, IS the path
  traversal control. URL() normalises /hosts/../../etc/passwd to /etc/passwd
  and encoded traversals fail the match, so every hostile id lands on 404.
  403 is reserved for well-formed ids outside the allowlist.
- 401 wins over 403, so an unauthenticated caller cannot probe the allowlist.
- tokenMatches() compares SHA-256 digests with timingSafeEqual. Scoped to the
  ingestion path only; realigning HEALTH_TOKEN touches every read route in
  production and is tracked separately.
- Body capped at 4 KiB by counting received bytes, never Content-Length:
  a client can lie in the header and chunked encoding omits it. Past the cap,
  413 then req.destroy() once the response has flushed.
- The persisted object is rebuilt field by field from a whitelist — finite
  numbers, strings capped at 128 chars, everything else dropped — because the
  file is read back by GET /hosts and ends up in the admin React tree. The
  read path re-runs the same whitelist: the bind-mount is writable, so the
  file on disk earns no more trust than the payload did.
- Snapshots are written to a temp file in the same directory then renamed, so
  a concurrent GET /hosts can never observe a truncated JSON.
- HOSTS_ALLOWED_IDS entries are validated at startup with the same regex;
  rejects are logged and dropped rather than becoming file paths.
- Every 401/403 is logged with X-Real-IP, the host id and the reason. Log
  values are filtered to printable ASCII so a crafted header cannot forge
  extra log lines.
- receivedAt is stamped by the server on arrival; a client-supplied timestamp
  is discarded by the whitelist. online = ageSeconds <= HOSTS_STALE_SECONDS.

Runtime stays zero-dependency — node:crypto is a builtin and no new module
file was added, so the Dockerfile's explicit COPY list is unchanged.

Tests: 61 (39 existing untouched + 22 new). __tests__/hosts.test.js is smoke
coverage of the decisions that would be silent to regress; the exhaustive
matrix is issue #14.

Docs: .env.example gains HOSTS_DIR / HOSTS_ALLOWED_IDS / HOSTS_INGEST_TOKEN /
HOSTS_STALE_SECONDS; CLAUDE.md and README.md document the endpoints, the
routing/auth ordering and the config. The CLAUDE.md "read-only" gotcha is
corrected — the API now writes, and HOSTS_DIR must be writable by uid 1000.

Resolves #13
maximus added the
autopilot:pending-human
label 2026-08-16 15:50:38 +00:00
Author
Owner

Verdict : APPROVE

Seul maillon qui touche du code servi en production. Les six invariants de la vague tiennent, verification faite. La table de routes est une amelioration structurelle sur la chaine de if de main : ajouter une route ne peut plus accidentellement livrer du non-authentifie.

Invariants verifies

  1. Ordre routage -> auth preserve. resolveRoute() -> 404 avant checkAuth(). Les deux tests de #19 restent verts pour la bonne raison.
  2. Point de passage unique. checkAuth() est le seul site d'authentification ; seul expected varie via tokenKind. Aucune dispersion par branche.
  3. Regex non elargie. Passe le jeu de charges dans Node : ../../etc/passwd, ..%2f..%2f, %2e%2e, %2E%2E, THINKPAD, %00, .hidden, -lead, 33 caracteres, //, / final — tous 404. Seul [a-z0-9][a-z0-9-]{0,31} atteint path.join. Les affirmations du commentaire sont exactes telles qu'ecrites.
  4. tokenMatches cloisonne a l'ingestion. Le chemin de lecture garde header === Bearer ${TOKEN} ; #17 correctement laisse hors vague.
  5. Reconstruction par liste blanche + ecriture atomique. sanitizeSnapshot rebatit champ par champ ; __proto__ ne peut pas se propager (JSON.parse en fait une propriete propre, jamais lue, sortie via un litteral neuf). Fichier temporaire dans le meme repertoire + renameSync, mode 0600, temp supprime en cas d'echec.
  6. Auth avant lecture du corps — bonus a signaler : un POST non authentifie est rejete avant qu'un seul octet ne soit bufferise. Le plafond de 4 Kio n'est donc jamais un cout memoire non authentifie.

Egalement verifie : 401 avant 403 (allowlist non sondable), logRejection ne touche jamais l'en-tete Authorization, safeLogValue plafonne et filtre le non-imprimable, le 204 retire Content-Type, le catch-all de handler est garde par headersSent, HOSTS_STALE_SECONDS rejette 0/negatif/NaN, parseAllowedIds ecarte les entrees invalides (repose sur le hoisting de safeLogValue — valide, c'est une declaration de fonction).

Suggestions (non bloquantes)

  1. Cette PR casse le contrat que #19 venait de poser. auth.test.js porte const ROUTES = [...] avec le commentaire « Every route reachable by the handler. Any new route must be added here. » #20 ajoute GET /hosts et POST /hosts/<id> sans y toucher. Verifie contre le tip cumule (e24f5b6f), pas seulement contre cette base : ROUTES contient toujours les quatre routes d'origine, et aucun test nulle part n'affirme que GET /hosts repond 401 sans en-tete Authorization. La couverture existe pour le mauvais token (token d'ingestion -> 401) et pour HEALTH_TOKEN absent -> 401, donc l'exposition reelle est faible et checkAuth rend le trou structurellement quasi impossible — mais c'est exactement la classe de derive que #19 existait pour attraper, et elle est passee des sa premiere sortie. Correctif : une ligne dans ROUTES.

  2. sanitizeCpu accepte loadAvg: [] (comportement explicitement beni par un test de #21). Un consommateur qui fait loadAvg[0].toFixed(1) prend un TypeError. Le dashboard est le consommateur et le contrat est neuf : soit completer a 3 entrees, soit documenter que le tableau peut etre plus court. Le README documente loadAvg sans annoncer de garantie de longueur.

  3. cleanString plafonne la longueur mais ne filtre pas les caracteres de controle : un hostname peut porter \n ou des sequences ANSI. C'est delibere (un test de #21 fige « stored verbatim, not escaped ») et correct pour un consommateur React — mais la surete de ce choix repose entierement sur l'echappement cote consommateur. Une ligne dans le README sous GET /hosts disant que les valeurs sont stockees brutes et doivent etre echappees au rendu eviterait qu'un futur consommateur non-React herite du probleme sans le savoir.

  4. receivedAt dans le futur n'est pas borne : Math.max(0, …) le transforme en ageSeconds: 0 -> online: true. Atteignable seulement par qui peut ecrire dans le bind-mount (donc deja a l'interieur), donc cosmetique. #21 fige ce comportement comme voulu.

  5. Debris de fichiers temporaires : des .{id}.{pid}.{ts}.tmp s'accumulent dans HOSTS_DIR si le processus meurt entre l'ecriture et le rename. handleHostsList itere l'allowlist et non le repertoire, donc ils sont invisibles — ils restent simplement la. Un balayage au demarrage fermerait le sujet.

  6. Le corps du 503 nomme la variable d'environnement a un appelant non authentifie. Coherent avec le HEALTH_TOKEN not configured preexistant, donc pas une regression — juste a noter que cette divulgation est maintenant sur un endpoint d'ecriture public.


Revue adversariale — maillon 3/5, revu contre sa base issue-12-auth-tests. Constats revalides contre le tip cumule avant publication.

## Verdict : APPROVE Seul maillon qui touche du code servi en production. Les six invariants de la vague tiennent, verification faite. La table de routes est une amelioration structurelle sur la chaine de `if` de `main` : ajouter une route ne peut plus accidentellement livrer du non-authentifie. ### Invariants verifies 1. **Ordre routage -> auth preserve.** `resolveRoute()` -> 404 avant `checkAuth()`. Les deux tests de #19 restent verts pour la bonne raison. 2. **Point de passage unique.** `checkAuth()` est le seul site d'authentification ; seul `expected` varie via `tokenKind`. Aucune dispersion par branche. 3. **Regex non elargie.** Passe le jeu de charges dans Node : `../../etc/passwd`, `..%2f..%2f`, `%2e%2e`, `%2E%2E`, `THINKPAD`, `%00`, `.hidden`, `-lead`, 33 caracteres, `//`, `/` final — **tous 404**. Seul `[a-z0-9][a-z0-9-]{0,31}` atteint `path.join`. Les affirmations du commentaire sont exactes telles qu'ecrites. 4. **`tokenMatches` cloisonne a l'ingestion.** Le chemin de lecture garde `header === Bearer ${TOKEN}` ; #17 correctement laisse hors vague. 5. **Reconstruction par liste blanche + ecriture atomique.** `sanitizeSnapshot` rebatit champ par champ ; `__proto__` ne peut pas se propager (`JSON.parse` en fait une propriete propre, jamais lue, sortie via un litteral neuf). Fichier temporaire dans le **meme** repertoire + `renameSync`, mode 0600, temp supprime en cas d'echec. 6. **Auth avant lecture du corps** — bonus a signaler : un POST non authentifie est rejete avant qu'un seul octet ne soit bufferise. Le plafond de 4 Kio n'est donc jamais un cout memoire non authentifie. Egalement verifie : 401 avant 403 (allowlist non sondable), `logRejection` ne touche jamais l'en-tete `Authorization`, `safeLogValue` plafonne et filtre le non-imprimable, le 204 retire `Content-Type`, le catch-all de `handler` est garde par `headersSent`, `HOSTS_STALE_SECONDS` rejette 0/negatif/NaN, `parseAllowedIds` ecarte les entrees invalides (repose sur le hoisting de `safeLogValue` — valide, c'est une declaration de fonction). ### Suggestions (non bloquantes) 1. **Cette PR casse le contrat que #19 venait de poser.** `auth.test.js` porte `const ROUTES = [...]` avec le commentaire « Every route reachable by the handler. **Any new route must be added here.** » #20 ajoute `GET /hosts` et `POST /hosts/<id>` sans y toucher. Verifie contre le **tip cumule** (`e24f5b6f`), pas seulement contre cette base : `ROUTES` contient toujours les quatre routes d'origine, et **aucun test nulle part n'affirme que `GET /hosts` repond 401 sans en-tete `Authorization`**. La couverture existe pour le mauvais token (token d'ingestion -> 401) et pour `HEALTH_TOKEN` absent -> 401, donc l'exposition reelle est faible et `checkAuth` rend le trou structurellement quasi impossible — mais c'est exactement la classe de derive que #19 existait pour attraper, et elle est passee des sa premiere sortie. Correctif : une ligne dans `ROUTES`. 2. **`sanitizeCpu` accepte `loadAvg: []`** (comportement explicitement beni par un test de #21). Un consommateur qui fait `loadAvg[0].toFixed(1)` prend un TypeError. Le dashboard est le consommateur et le contrat est neuf : soit completer a 3 entrees, soit documenter que le tableau peut etre plus court. Le README documente `loadAvg` sans annoncer de garantie de longueur. 3. **`cleanString` plafonne la longueur mais ne filtre pas les caracteres de controle** : un `hostname` peut porter `\n` ou des sequences ANSI. C'est delibere (un test de #21 fige « stored verbatim, not escaped ») et correct pour un consommateur React — mais la surete de ce choix repose entierement sur l'echappement cote consommateur. Une ligne dans le README sous `GET /hosts` disant que les valeurs sont stockees brutes et doivent etre echappees au rendu eviterait qu'un futur consommateur non-React herite du probleme sans le savoir. 4. **`receivedAt` dans le futur n'est pas borne** : `Math.max(0, …)` le transforme en `ageSeconds: 0` -> `online: true`. Atteignable seulement par qui peut ecrire dans le bind-mount (donc deja a l'interieur), donc cosmetique. #21 fige ce comportement comme voulu. 5. **Debris de fichiers temporaires** : des `.{id}.{pid}.{ts}.tmp` s'accumulent dans `HOSTS_DIR` si le processus meurt entre l'ecriture et le rename. `handleHostsList` itere l'allowlist et non le repertoire, donc ils sont invisibles — ils restent simplement la. Un balayage au demarrage fermerait le sujet. 6. Le corps du 503 nomme la variable d'environnement a un appelant non authentifie. Coherent avec le `HEALTH_TOKEN not configured` preexistant, donc pas une regression — juste a noter que cette divulgation est maintenant sur un endpoint d'ecriture public. --- *Revue adversariale — maillon 3/5, revu contre sa base `issue-12-auth-tests`. Constats revalides contre le tip cumule avant publication.*
Author
Owner

Mergee localement sur main en fast-forward (pile chainee : l API de merge Forgejo ne peut pas traiter une pile dont la base n est pas main). Tip integre : e181a96. Forgejo ne detecte pas un merge ff local, donc cette PR est fermee a la main — le code EST sur main.

Mergee localement sur `main` en fast-forward (pile chainee : l API de merge Forgejo ne peut pas traiter une pile dont la base n est pas `main`). Tip integre : `e181a96`. Forgejo ne detecte pas un merge ff local, donc cette PR est fermee a la main — le code EST sur `main`.
maximus closed this pull request 2026-08-16 18:27:38 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
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#20
No description provided.