@nodefony/framework 10.0.0-beta.5 → 10.0.0-beta.7

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 (61) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js +7 -0
  3. package/dist/index.js +14 -10
  4. package/dist/nodefony/config/config.js +16 -2
  5. package/dist/nodefony/controller/AdminApiController.js +18 -5
  6. package/dist/nodefony/controller/BenchController.js +1 -1
  7. package/dist/nodefony/controller/CspReportController.js +70 -0
  8. package/dist/nodefony/controller/IssuerMetadataController.js +3 -3
  9. package/dist/nodefony/controller/LoginPageController.js +5 -7
  10. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +75 -10
  11. package/dist/nodefony/controller/SessionAuthController.js +6 -7
  12. package/dist/nodefony/controller/TokenAuthController.js +3 -3
  13. package/dist/nodefony/decorators/routerDecorators.js +103 -15
  14. package/dist/nodefony/grpc/GrpcController.js +138 -0
  15. package/dist/nodefony/grpc/GrpcService.js +75 -0
  16. package/dist/nodefony/grpc/declarations.js +33 -0
  17. package/dist/nodefony/grpc/index.js +3 -0
  18. package/dist/nodefony/service/AdminBroker.js +1 -1
  19. package/dist/nodefony/service/IdempotencyStore.js +1 -1
  20. package/dist/nodefony/service/router.js +15 -12
  21. package/dist/nodefony/src/Controller.js +330 -115
  22. package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
  23. package/dist/nodefony/src/Resolver.js +85 -51
  24. package/dist/nodefony/src/Route.js +45 -45
  25. package/dist/nodefony/src/SecurityApiController.js +30 -7
  26. package/dist/nodefony/src/conditionalRequest.js +103 -0
  27. package/dist/nodefony/src/docsReader.js +1 -1
  28. package/dist/nodefony/src/idempotency.js +1 -1
  29. package/dist/nodefony/src/loginPage.js +3 -2
  30. package/dist/nodefony/src/scopeCatalog.js +1 -1
  31. package/dist/nodefony/src/viewNonce.js +58 -0
  32. package/dist/types/index.d.ts +4 -2
  33. package/dist/types/nodefony/config/config.d.ts +20 -0
  34. package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
  35. package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
  36. package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
  37. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
  38. package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
  39. package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
  40. package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
  41. package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
  42. package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
  43. package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
  44. package/dist/types/nodefony/grpc/index.d.ts +13 -0
  45. package/dist/types/nodefony/interfaces/IController.d.ts +4 -4
  46. package/dist/types/nodefony/service/router.d.ts +8 -6
  47. package/dist/types/nodefony/src/Controller.d.ts +124 -14
  48. package/dist/types/nodefony/src/Resolver.d.ts +35 -8
  49. package/dist/types/nodefony/src/Route.d.ts +35 -0
  50. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
  51. package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
  52. package/dist/types/nodefony/src/loginPage.d.ts +1 -1
  53. package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
  54. package/docs/controller.md +234 -35
  55. package/docs/decorateurs.md +1 -1
  56. package/docs/grpc.md +690 -0
  57. package/docs/idempotence.md +13 -8
  58. package/docs/index.md +16 -10
  59. package/docs/routing.md +30 -2
  60. package/docs/templates.md +15 -0
  61. package/package.json +28 -11
@@ -404,31 +404,111 @@ class MeController extends Controller {
404
404
  ## 🧰 Répondre — ce que ton `return` déclenche
405
405
 
406
406
  Le traducteur unique est `Resolver.returnController()` (`Resolver.ts#Resolver.returnController`). Il regarde le **type**
407
- de ce que tu as retourné :
407
+ de ce que tu as retourné — dans l'ordre de fréquence : la promesse, puis le tableau et l'objet simple,
408
+ le cas courant, qui ne paient aucun test de plus que nécessaire (`Resolver.ts#Resolver.returnObject`) :
408
409
 
409
410
  <!-- prettier-ignore -->
410
411
  | Tu retournes… | Ce qui se passe | Ancre |
411
412
  | --- | --- | --- |
412
- | Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts#Resolver._areaSecurity` |
413
- | Une `string` | Envoyée telle quelle en corps | `Resolver.ts#Resolver._callWithIdempotency` |
414
- | Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts#Resolver.returnController` |
415
- | Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts#Resolver._callWithIdempotency` |
416
- | Un `Buffer` | Envoyé brut | `Resolver.ts#Resolver.returnController` |
417
- | Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts#Resolver._invokeAction` |
418
- | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts#NO_BODY_STATUS`) |
419
- | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts#Resolver.returnController` |
420
- | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts#Resolver.returnController` |
413
+ | Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts#Resolver.returnController` |
414
+ | Une `string` | Envoyée telle quelle en corps | `Resolver.ts#Resolver.returnController` |
415
+ | Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts#Resolver.returnObject` |
416
+ | Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts#Resolver.returnController` |
417
+ | Un `Buffer` | Envoyé brut | `Resolver.ts#Resolver.returnObject` |
418
+ | Une réponse Nodefony (rendue par un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts#Resolver.returnObject` |
419
+ | Une **`Response` Web standard** — `fetch()`, `new Response(…)`, `Response.json(…)` | **Envoyée en flux** : son statut, ses en-têtes, son corps (HTTP seulement) — détail ci-dessous | `HttpContext.openWebResponse()` (`HttpContext.ts#HttpContext.openWebResponse`, `@nodefony/http`) |
420
+ | Un **flux d'octets nu** — `Readable` Node, `ReadableStream` Web | **Envoyé en flux**, comme `this.renderStream(source)` sans option (HTTP seulement) | `Resolver.ts#Resolver.returnObject` |
421
+ | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `isContentlessStatus` (`Response.ts#isContentlessStatus`, `@nodefony/http`) |
422
+ | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts#Resolver.returnNothing` |
423
+ | Une `Date`, une `RegExp`, une `Error` | Comme `void` : rien à rendre | `Resolver.ts#Resolver.returnNothing` |
424
+ | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts#Resolver.returnObject` |
421
425
 
422
426
  > [!WARNING]
423
427
  > **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
424
428
  > répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
425
- > `NO_BODY_STATUS` (`Resolver.ts#NO_BODY_STATUS`) contient 204, 205 et 304. Donc un `@Delete` qui fait
429
+ > `isContentlessStatus` (`Response.ts#isContentlessStatus`, liste unique de `@nodefony/http`) rend vrai pour 204, 205 et 304. Donc un `@Delete` qui fait
426
430
  > `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
427
431
  > laisse la requête pendue.
428
432
 
429
433
  Même règle pour une **instance de classe** (une entité ORM renvoyée telle quelle) : elle n'est
430
434
  volontairement pas passée à `JSON.stringify`. Retourne un objet simple — ou appelle `renderJson()`.
431
435
 
436
+ ### Renvoyer une `Response` Web ou un flux
437
+
438
+ La `Response` est l'objet standard que rend `fetch()` — dans le navigateur, et dans Node depuis la
439
+ version 18. Beaucoup de bibliothèques en produisent une toute prête (un kit d'IA qui rend son texte
440
+ en flux, un client d'API). Ton action peut la **renvoyer telle quelle** : le framework envoie son
441
+ statut, ses en-têtes et son corps EN FLUX, sans le lire d'avance.
442
+
443
+ ```ts
444
+ import { Readable } from "node:stream";
445
+ import { Controller, controller, route } from "@nodefony/framework";
446
+ import type { Context } from "@nodefony/http";
447
+
448
+ @controller("/examples")
449
+ export class ExamplesController extends Controller {
450
+ constructor(context: Context) {
451
+ super("ExamplesController", context);
452
+ }
453
+
454
+ /** Relayer une API tierce : sa réponse, telle quelle. */
455
+ @route("weather", { path: "/weather" })
456
+ weather() {
457
+ // Le signal coupe l'appel amont si le client s'en va.
458
+ return fetch("https://api.exemple.org/meteo", {
459
+ signal: this.context?.signal ?? null,
460
+ });
461
+ }
462
+
463
+ /** Une réponse construite : statut, en-têtes et cookie à toi. */
464
+ @route("export", { path: "/export.csv" })
465
+ export() {
466
+ return new Response("id;libellé\n1;Cordage\n", {
467
+ status: 200,
468
+ headers: [
469
+ ["content-type", "text/csv; charset=utf-8"],
470
+ ["content-disposition", 'attachment; filename="export.csv"'],
471
+ ["set-cookie", "export=fait; Path=/; HttpOnly"],
472
+ ],
473
+ });
474
+ }
475
+
476
+ /** Un flux d'octets renvoyé nu. */
477
+ @route("lines", { path: "/lines" })
478
+ lines() {
479
+ return Readable.from(["une\n", "deux\n"], { objectMode: false });
480
+ }
481
+ }
482
+ ```
483
+
484
+ Ce que le framework fait de la réponse (`webResponse.ts#webResponseParts`, `@nodefony/http`) :
485
+
486
+ | Origine de la `Response` | En-têtes envoyés |
487
+ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
488
+ | **Construite** par ton code (`new Response`, `Response.json`, `Response.redirect`) | tous les siens, **cookies compris** |
489
+ | **Relayée** — rendue par `fetch()` | les siens, **sauf** `content-encoding` et `content-length` (`fetch` a déjà décompressé le corps : ils décriraient des octets qui ne sont plus là) et `set-cookie` (les cookies d'un service tiers seraient posés sous TON domaine) |
490
+
491
+ Dans les deux cas, les en-têtes de connexion (`connection`, `keep-alive`, `transfer-encoding`,
492
+ `upgrade`… — RFC 9110 §7.6.1) ne partent jamais : ils décrivent un saut, et HTTP/2 les refuse.
493
+ La différence se lit sur `response.type` : `"default"` pour une réponse construite, `"basic"` ou
494
+ `"cors"` pour une réponse de `fetch`.
495
+
496
+ > [!TIP]
497
+ > **Quand préférer `this.renderStream()`** : dès qu'il faut des options — délai de silence
498
+ > (`idleTimeout`), durée maximale (`maxDuration`), écouteur de fin (`onClose`), ou ne recopier que
499
+ > certains en-têtes de l'amont. `return response` est le geste court du cas courant ;
500
+ > `renderStream` est le réglage fin. Les deux passent par la même tuyauterie
501
+ > (`HttpContext.ts#HttpContext.openStream`) : contre-pression, arrêt au départ du client, `HEAD` sans
502
+ > lire le corps.
503
+
504
+ > [!WARNING]
505
+ >
506
+ > - **`Response.error()`** ne porte aucune réponse (statut 0) : le client reçoit un **502**.
507
+ > - **WebSocket** : une `Response` ou un flux renvoyé par une action de message n'est pas envoyé — une
508
+ > trame n'a ni statut ni en-têtes. Renvoie un objet (envoyé en trame JSON) ou un `Buffer`.
509
+ > - **Réponse déjà partie** (tu as appelé `render*` ou `streamFile` ET renvoyé une `Response`) : rien
510
+ > n'est réécrit, et le corps de la `Response` est annulé pour libérer l'amont.
511
+
432
512
  ### Les helpers de rendu
433
513
 
434
514
  Quand tu veux piloter l'envoi plutôt que retourner une valeur :
@@ -463,8 +543,10 @@ Deux besoins distincts, deux helpers.
463
543
  ### Téléchargement — `renderFileDownload()`
464
544
 
465
545
  `renderFileDownload(file, options?, headers?)` (`Controller.ts#Controller.renderFileDownload`) pose
466
- `Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
467
- de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts#Controller.getFileAsync`) ;
546
+ `Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier et un `Expires` déjà
547
+ passé, puis délègue au moteur de flux. Un nom de fichier accentué ou porteur de `"` part en
548
+ `filename*` UTF-8, précédé d'un repli ASCII (RFC 6266 §4.3, RFC 8187) — un nom quelconque ne fait
549
+ jamais lever la réponse. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts#Controller.getFileAsync`) ;
468
550
  la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
469
551
  et gèle le process le temps du stat.
470
552
 
@@ -473,25 +555,136 @@ et gèle le process le temps du stat.
473
555
  `renderMediaStream(file, headers?, options?)` (`Controller.ts#Controller.renderMediaStream`) implémente les **requêtes par
474
556
  plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
475
557
 
476
- | Le client envoie… | Réponse |
477
- | --------------------------------------------- | --------------------------------------------------------------- |
478
- | Pas de `Range` | 200 + fichier complet, `Accept-Ranges: bytes` |
479
- | `Range: bytes=0-499` | **206** + `Content-Range`, bornes clampées à la taille réelle |
480
- | `Range: bytes=-500` (suffixe) | 206 sur les 500 derniers octets |
481
- | Plage hors fichier | **416** + `Content-Range: bytes */<taille>` (RFC 9110 §15.5.17) |
482
- | Syntaxe invalide, multi-plage, unité inconnue | En-tête **ignoré** → 200 complet (jamais un 500) |
558
+ | Le client envoie… | Réponse |
559
+ | ------------------------------------------------ | --------------------------------------------------------------- |
560
+ | Pas de `Range` | 200 + fichier complet, `Accept-Ranges: bytes` |
561
+ | `Range: bytes=0-499` | **206** + `Content-Range`, bornes clampées à la taille réelle |
562
+ | `Range: bytes=-500` (suffixe) | 206 sur les 500 derniers octets |
563
+ | Plage hors fichier | **416** + `Content-Range: bytes */<taille>` (RFC 9110 §15.5.17) |
564
+ | Syntaxe invalide, multi-plage, unité inconnue | En-tête **ignoré** → 200 complet (jamais un 500) |
565
+ | `HEAD` avec `Range` | `Range` **ignoré** : en-têtes du GET complet (RFC 9110 §14.2) |
566
+ | `If-Range` égal à l'`ETag` ou au `Last-Modified` | 206 : la reprise est sûre, le fichier n'a pas changé |
567
+ | `If-Range` différent, ou étiquette faible | `Range` **ignoré** → 200 complet (RFC 9110 §13.1.5) |
568
+
569
+ Une condition `If-Range` fausse sert le fichier entier — plutôt qu'une plage d'un fichier qui a
570
+ changé, que le client recollerait à l'ancienne. Seuls un `ETag` FORTEMENT égal, ou une date égale
571
+ à un `Last-Modified` vieux d'au moins une seconde, autorisent la reprise.
483
572
 
484
573
  La logique est isolée dans une fonction pure exportée, `parseByteRange()` (`Controller.ts#parseByteRange`) —
485
574
  donc testable sans serveur.
486
575
 
487
576
  ### Ce que `streamFile()` garantit
488
577
 
489
- `streamFile()` (`Controller.ts#Controller.streamFile`) est le moteur commun. Sa subtilité n'est pas le pipe, c'est le
490
- **nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
491
- téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
492
- écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
493
- résout la promesse (`Controller.ts#Controller.streamFile`), puis se retire lui-même (`Controller.ts#Controller.streamFile`). Un
494
- téléchargement interrompu ne coûte donc **rien** en ressource retenue.
578
+ `streamFile()` (`Controller.ts#Controller.streamFile`) est le moteur commun, et il repose sur le
579
+ même cycle de vie que `renderStream()` ci-dessous (`HttpContext.openStream`) :
580
+
581
+ - **la réponse est prise à l'appel**, les en-têtes partent au premier morceau ;
582
+ - **un client qui raccroche détruit le flux**, donc ferme le descripteur : un téléchargement
583
+ interrompu ne retient rien ;
584
+ - **un fichier illisible à l'ouverture** (`EACCES`) rend la page d'erreur ordinaire, 500 ; une
585
+ lecture qui échoue en route coupe la connexion, jamais une fin propre ;
586
+ - **la longueur annoncée est tenue** : la lecture est bornée à la taille relevée (un fichier qui
587
+ grossit pendant l'envoi n'envoie pas plus que `Content-Length`), et un extrait `start`/`end`
588
+ annonce sa propre longueur ;
589
+ - **un fichier n'est pas un flux vivant** : ni `X-Accel-Buffering: no`, ni délai d'inactivité.
590
+
591
+ ### Requêtes conditionnelles — 304, 412, reprise
592
+
593
+ Les trois helpers posent sur le fichier ENTIER une `ETag` forte et un `Last-Modified`, tirés de sa
594
+ taille et de sa date de modification, sans le lire. Les préconditions sont évaluées dans l'ordre de
595
+ RFC 9110 §13.2.2, AVANT d'ouvrir le fichier (`conditionalRequest.ts#evaluatePreconditions`) :
596
+
597
+ | Le client envoie… | Réponse |
598
+ | ------------------------------------------------------------ | ------------------------------------------- |
599
+ | `If-None-Match` égal (comparaison faible), ou `*` | **304** en GET/HEAD, sans corps ; 412 sinon |
600
+ | `If-Modified-Since` ≥ date du fichier (sans `If-None-Match`) | **304** en GET/HEAD |
601
+ | `If-Match` différent (comparaison forte) | **412** `Precondition Failed` |
602
+ | `If-Unmodified-Since` < date du fichier (sans `If-Match`) | **412** |
603
+
604
+ Le 304 porte ce qu'aurait porté le 200 : `ETag`, `Last-Modified`, `Cache-Control`, `Expires`,
605
+ `Vary` (RFC 9110 §15.4.5). Le helper rend alors `null` : aucun fichier n'a été ouvert.
606
+
607
+ **`Cache-Control: private, no-cache` par défaut.** Sans lui, `Last-Modified` rendrait la réponse
608
+ « heuristiquement cacheable » (RFC 9111 §4.2.2) : un cache partagé pourrait garder le fichier et le
609
+ resservir sans redemander — or un fichier servi par une action est le plus souvent protégé.
610
+ `private` ferme les caches partagés, `no-cache` fait revalider avant chaque réutilisation, ce qui
611
+ ne coûte qu'un 304. Un fichier public fixe le sien :
612
+
613
+ ```ts
614
+ return this.renderMediaStream(file, {
615
+ "Cache-Control": "public, max-age=3600",
616
+ });
617
+ ```
618
+
619
+ Une `ETag` ou un `Last-Modified` passés dans les en-têtes (un hachage du contenu, une version)
620
+ remplacent ceux du fichier ET servent à la comparaison. Un extrait `streamFile(f, h, { start, end })`
621
+ n'est pas la représentation entière : il ne reçoit ni validateur ni évaluation.
622
+
623
+ **Ce que l'application peut changer** :
624
+
625
+ | Besoin | Geste |
626
+ | ----------------------------------------------------- | -------------------------------------------------------------------------- |
627
+ | Un autre `Cache-Control` (fichier public, `no-store`) | le passer dans les en-têtes, ou `@Header("Cache-Control", …)` sur l'action |
628
+ | Sa propre `ETag` (hachage du contenu, version) | la passer dans les en-têtes, ou `@Header("ETag", …)` — elle sert au 304 |
629
+ | Aucun validateur, aucun 304, aucun `Cache-Control` | `{ validators: false }` dans les options |
630
+ | Le défaut de toute l'application | la section `fileServing` de la config du module (ci-dessous) |
631
+
632
+ Un en-tête posé AVANT l'appel (`@Header`, `this.response.setHeader`) est respecté comme s'il était
633
+ passé en argument.
634
+
635
+ **Pour toute l'application**, la section `fileServing` de la config du module fixe le défaut
636
+ (`config.ts#fileServingSchema`) ; un appel le remplace toujours :
637
+
638
+ ```ts
639
+ use("@nodefony/framework", {
640
+ fileServing: {
641
+ validators: true, // false : aucun validateur, aucun 304, aucun Cache-Control par défaut
642
+ cacheControl: "private, no-cache", // null : n'en poser aucun
643
+ },
644
+ });
645
+ ```
646
+
647
+ `cacheControl` est validé au démarrage : une valeur porteuse d'un retour à la ligne (qui forgerait
648
+ un en-tête de plus) l'interrompt en nommant la clé. `{ validators: true }` dans les options d'un
649
+ appel rétablit les validateurs quand la config les a coupés.
650
+
651
+ ⚠️ **La date d'un fichier n'est pas toujours fiable.** Une archive décompressée ou un build
652
+ reproductible donne la même date à tous les fichiers : deux contenus de même taille portent alors
653
+ la même `ETag`, et le client garde l'ancien sur un 304 sans que rien ne le signale. Pour ces
654
+ fichiers, passer une `ETag` tirée du contenu — le 304 reste juste — ou couper les validateurs :
655
+
656
+ ```ts
657
+ // le 304 garde son sens : l'étiquette suit le contenu
658
+ return this.renderMediaStream(file, { ETag: `"${hash}"` });
659
+ // ou : aucune requête conditionnelle, le fichier part toujours entier
660
+ return this.renderMediaStream(file, {}, { validators: false });
661
+ ```
662
+
663
+ ### Une source qui n'est pas un fichier — `renderStream()`
664
+
665
+ La sortie d'un processus (ffmpeg, `tar`), le corps d'un `fetch()` relayé, une archive construite à
666
+ la volée : `renderStream(source, options?)` (`Controller.ts#Controller.renderStream`) répond par
667
+ ce flux d'octets, en HTTP/1.1 comme en HTTP/2. La réponse est prise à l'appel, ses en-têtes partent
668
+ au premier morceau, et la source est **détruite** à la fin quelle qu'en soit la raison. La promesse
669
+ ne rejette jamais : elle rend `{ reason, bytes }`.
670
+
671
+ ```typescript
672
+ const ff = spawn("ffmpeg", args, {
673
+ stdio: ["ignore", "pipe", "ignore"],
674
+ windowsHide: true,
675
+ });
676
+ try {
677
+ await this.renderStream(ff.stdout, {
678
+ headers: { "content-type": "video/mp4" },
679
+ });
680
+ } finally {
681
+ ff.kill("SIGKILL"); // le flux est fini, quelle qu'en soit la raison
682
+ }
683
+ ```
684
+
685
+ Page complète — options, raisons de fin, erreurs avant et après le premier octet, relais `fetch`,
686
+ pièges : [Flux d'octets](../../http/docs/byte-stream.md). Pour des **événements texte** lus par
687
+ `EventSource`, voir [Flux SSE](../../http/docs/sse.md).
495
688
 
496
689
  ## ⚠️ Erreurs — lever, rendre, observer
497
690
 
@@ -594,15 +787,21 @@ code du framework applique — et attend de toi — les règles suivantes :
594
787
 
595
788
  ## 📜 Normes appliquées
596
789
 
597
- | Domaine | Norme | Comment le code s'y conforme |
598
- | -------------------------------- | ------------------------ | ------------------------------------------------------------------------------------ |
599
- | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts#NO_BODY_STATUS`) |
600
- | Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts#parseByteRange`) |
601
- | Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts#Controller.renderResponse`) |
602
- | Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts#HttpResponse.send`) |
603
- | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts#Resolver.returnController`), vérifié par le banc `auto-json` |
604
- | Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts#Resolver._callWithIdempotency`) |
605
- | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts#DefaultErrorRenderer.renderWebsocket`) |
790
+ | Domaine | Norme | Comment le code s'y conforme |
791
+ | --------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------- |
792
+ | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.3.6/§15.4.5 | `isContentlessStatus` (`Response.ts#isContentlessStatus`) — aucun corps ne part |
793
+ | Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts#parseByteRange`) |
794
+ | Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts#Controller.renderResponse`) |
795
+ | `Range` hors GET, sous `If-Range` | RFC 9110 §14.2, §13.1.5 | `renderMediaStream()` (`Controller.ts#Controller.renderMediaStream`) |
796
+ | Validateurs d'un fichier | RFC 9110 §8.8.2, §8.8.3 | `fileValidators()` (`conditionalRequest.ts#fileValidators`) |
797
+ | Ordre des préconditions → 304/412 | RFC 9110 §13.1, §13.2.2 | `evaluatePreconditions()` (`conditionalRequest.ts#evaluatePreconditions`) |
798
+ | Cache d'un fichier protégé | RFC 9111 §4.2.2, §5.2.2 | `Cache-Control: private, no-cache` par défaut (`Controller.ts#Controller.streamFile`) |
799
+ | Nom de fichier téléchargé | RFC 6266 §4.3, RFC 8187 | `contentDisposition()` (`Controller.ts#contentDisposition`) |
800
+ | Longueur du corps tenue | RFC 9112 §6.3, RFC 9113 §8.1.1 | `streamFile()` (`Controller.ts#Controller.streamFile`) + `ByteStream.ts#ByteStream` |
801
+ | Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts#HttpResponse.send`) |
802
+ | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts#Resolver.returnController`), vérifié par le banc `auto-json` |
803
+ | Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts#Resolver._callWithIdempotency`) |
804
+ | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts#DefaultErrorRenderer.renderWebsocket`) |
606
805
 
607
806
  ## 📡 Observabilité — Studio
608
807
 
@@ -431,7 +431,7 @@ update(input: RouteInput<{ params: typeof postParams; body: typeof postBody }>)
431
431
  **Ce que fait le framework, dans l'ordre :**
432
432
 
433
433
  - chaque partie déclarée est un [Standard Schema](https://standardschema.dev) — Zod, Valibot,
434
- ArkType… ; l'interface est recopiée au cœur (`runtime/standardSchema.ts#IStandardSchemaV1`), aucune
434
+ ArkType… ; l'interface est recopiée au cœur (`runtime/standardSchemaTypes.ts#IStandardSchemaV1`), aucune
435
435
  dépendance n'est imposée ;
436
436
  - la validation a lieu **après** les gardes (`@IsGranted` : un 403 ne révèle pas le contrat) et
437
437
  **avant** l'instanciation du contrôleur : une entrée invalide ne construit rien