@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
@@ -1,4 +1,16 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Requêtes conditionnelles des fichiers servis par une action (`streamFile`,
4
+ * `renderFileDownload`, `renderMediaStream`). Le défaut de TOUTE
5
+ * l'application ; un appel le remplace par ses options (`validators`) ou ses
6
+ * en-têtes (`Cache-Control`, `ETag`).
7
+ */
8
+ export declare const fileServingSchema: z.ZodObject<{
9
+ validators: z.ZodDefault<z.ZodBoolean>;
10
+ cacheControl: z.ZodDefault<z.ZodNullable<z.ZodString>>;
11
+ }, z.core.$strict>;
12
+ /** Réglages des fichiers servis, défauts appliqués. */
13
+ export type IFileServingConfig = z.infer<typeof fileServingSchema>;
2
14
  export declare const frameworkConfigSchema: z.ZodObject<{
3
15
  router: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
4
16
  adminBroker: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
@@ -7,6 +19,10 @@ export declare const frameworkConfigSchema: z.ZodObject<{
7
19
  gcIntervalS: z.ZodDefault<z.ZodNumber>;
8
20
  gcJitter: z.ZodDefault<z.ZodBoolean>;
9
21
  }, z.core.$strict>>;
22
+ fileServing: z.ZodDefault<z.ZodObject<{
23
+ validators: z.ZodDefault<z.ZodBoolean>;
24
+ cacheControl: z.ZodDefault<z.ZodNullable<z.ZodString>>;
25
+ }, z.core.$strict>>;
10
26
  }, z.core.$strict>;
11
27
  /** Type de sortie (config normalisée + défauts appliqués). */
12
28
  export type IFrameworkConfig = z.infer<typeof frameworkConfigSchema>;
@@ -24,6 +40,10 @@ declare const _default: {
24
40
  gcIntervalS: number;
25
41
  gcJitter: boolean;
26
42
  };
43
+ fileServing: {
44
+ validators: boolean;
45
+ cacheControl: string | null;
46
+ };
27
47
  "module-security": {
28
48
  areas: {
29
49
  "nodefony-liveness": {
@@ -38,8 +38,10 @@ declare class AdminApiController extends Controller {
38
38
  * (une action, deux transports) : même exécution sur les deux transports, seul
39
39
  * l'emballage diffère.
40
40
  *
41
- * - **HTTP** : rendu historique inchangé (`renderJson` + status + header
42
- * `x-nodefony-instance`).
41
+ * - **HTTP** : `renderJson` + status + header `x-nodefony-instance`. Quand
42
+ * l'application active un codec binaire, le corps passe par le codec texte
43
+ * de la socket (convention `$bytes`, annoncée par `x-nodefony-bytes`) : des
44
+ * octets se relisent alors à l'identique sur les deux transports.
43
45
  * - **Pont WS-RPC `api.request`** : valeur **nue** (le pont l'enveloppe
44
46
  * `{id, result}` — snapshot ≡ GET par construction) ; statut ≥ 400 →
45
47
  * {@link RpcError} (`data.status` + `data.body`), symétrie d'un `fetch`
@@ -0,0 +1,35 @@
1
+ import { type Module } from "nodefony";
2
+ import { type ContextType } from "@nodefony/http";
3
+ import Controller from "../src/Controller.js";
4
+ /**
5
+ * Réception PUBLIQUE des rapports de violation CSP (ADR-0024 §C7) —
6
+ * `POST /nodefony/security/api/csp-report`, adaptateur MINCE au-dessus du
7
+ * service `cspReports`.
8
+ *
9
+ * Le navigateur poste sans identifiant : la route contourne le pare-feu, et la
10
+ * défense CSRF (`@CsrfExempt`) — elle ne change l'état de personne, et un
11
+ * rapport n'a aucune session à détourner. Le corps arrive en FLUX
12
+ * (`@Body({ stream: true })`) pour être borné PENDANT la lecture : au-delà de
13
+ * {@link CSP_REPORT_MAX_BYTES}, 413 sans rien garder. Tout le reste — quota,
14
+ * type de contenu, forme, expurgation — appartient au collecteur, et la
15
+ * réponse est 204 quoi qu'il en pense : l'émetteur n'apprend rien.
16
+ */
17
+ declare class CspReportController extends Controller {
18
+ constructor(context: ContextType);
19
+ /**
20
+ * Reçoit un rapport, ou un lot de rapports.
21
+ *
22
+ * @param stream - le corps brut, jamais lu par le pipeline.
23
+ * @throws HttpError 413 quand le corps dépasse le plafond.
24
+ */
25
+ report(stream: AsyncIterable<Uint8Array>): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
26
+ }
27
+ /**
28
+ * Monte la route de réception — appelé par le module framework à
29
+ * `onKernelReady`, seulement si le service `cspReports` est présent (rapports
30
+ * demandés vers la route du framework).
31
+ *
32
+ * @param frameworkModule - module qui porte le contrôleur.
33
+ */
34
+ export declare function mountCspReportRoute(frameworkModule: Module): void;
35
+ export default CspReportController;
@@ -6,8 +6,9 @@ import Controller from "../src/Controller.js";
6
6
  * feuille `nodefony/login.css`, script `nodefony/login.js`. Aucun front, aucun
7
7
  * build côté application.
8
8
  *
9
- * Joignable sans session ; jamais mise en cache (`no-store`) ni affichable
10
- * dans un cadre (`frame-ancestors 'none'`).
9
+ * Joignable sans session ; jamais mise en cache (`no-store`). L’encadrement
10
+ * suit `http.securityHeaders.frameOptions`, qui pose `X-Frame-Options` ET
11
+ * `frame-ancestors` sur toute la politique (ADR-0024 §C5).
11
12
  */
12
13
  declare class LoginPageController extends Controller {
13
14
  constructor(context: ContextType);
@@ -113,10 +113,30 @@ export interface IServiceScan {
113
113
  * interface à implémenter formellement — le même couplage que `tokenService` ou
114
114
  * `adminBroker`.
115
115
  *
116
+ * Deux sources peuvent déclarer la MÊME ressource — une zone du pare-feu et la
117
+ * porte MCP qu'elle couvre, typiquement — : elles sont fusionnées
118
+ * ({@link mergeProtectedResources}), jamais départagées par leur ordre.
119
+ *
116
120
  * @param container - conteneur de services de l'application
117
- * @returns les ressources déclarées, dans l'ordre des services
121
+ * @returns les ressources déclarées, une entrée par ressource, dans l'ordre des
122
+ * services
118
123
  */
119
124
  export declare function collectProtectedResources(container: IServiceScan): readonly IProtectedResourceInput[];
125
+ /**
126
+ * Une ressource, UN document : fusionne les déclarations d'une même ressource
127
+ * (même URI canonique) — l'union de leurs serveurs d'autorisation et de leurs
128
+ * scopes ; le premier nom et la première documentation déclarés.
129
+ *
130
+ * Pourquoi fusionner plutôt que garder la première : une zone du pare-feu
131
+ * publie sa ressource avec les émetteurs de confiance, et la porte MCP qu'elle
132
+ * couvre publie la MÊME ressource avec les scopes de ses outils. Garder la
133
+ * première jetait les scopes sans un mot — et un agent qui suit le document
134
+ * demandait alors un jeton sans eux, qui n'ouvrait pas l'outil.
135
+ *
136
+ * @param entries - déclarations, dans l'ordre des services
137
+ * @returns une entrée par ressource, à la place de la première déclaration
138
+ */
139
+ export declare function mergeProtectedResources(entries: readonly IProtectedResourceInput[]): IProtectedResourceInput[];
120
140
  /**
121
141
  * Chemins bien connus à monter pour un jeu de ressources déclarées.
122
142
  *
@@ -62,12 +62,11 @@ export interface ISessionAuthFlow {
62
62
  * (Auth.js, better-auth). Le `401` y écrivait une erreur réseau dans la console
63
63
  * de chaque visiteur anonyme, sans rien protéger : la réponse ne révèle rien.
64
64
  *
65
- * @remarks Dérogation RFC 9110 §15.5.2 DÉLIBÉRÉE : les 401 de `login` ne
66
- * portent PAS de `WWW-Authenticate` — aucun scheme HTTP ne décrit un cookie de
67
- * session, et un challenge `Basic` mensonger déclencherait le popup natif du
68
- * navigateur (l'anti-pattern que le BFF élimine). Les zones du firewall, elles,
69
- * restent strictement conformes (challenge du premier authenticator qui en
70
- * déclare un).
65
+ * @remarks Les 401 de `login` portent le défi `Session realm="nodefony"`
66
+ * (RFC 9110 §15.5.2, posé par `SecurityApiController`), celui du pare-feu :
67
+ * aucun schéma HTTP ne décrit un cookie de session, et un `Basic` ouvrirait la
68
+ * fenêtre de connexion native du navigateur — l'anti-pattern que le BFF
69
+ * élimine. Un schéma inconnu des navigateurs n'en ouvre aucune.
71
70
  */
72
71
  declare class SessionAuthController extends SecurityApiController {
73
72
  #private;
@@ -33,9 +33,9 @@ export interface ITokenIssuer {
33
33
  * framework ne peut pas importer les classes d'erreur de security ; un 429
34
34
  * reporte le `Retry-After` (RFC 6585) du throttler NIST.
35
35
  *
36
- * @remarks Dérogation RFC 9110 §15.5.2 (comme `SessionAuthController`) : ces 401 ne
37
- * portent pas de `WWW-Authenticate` — l'endpoint d'émission n'est pas une
38
- * ressource protégée par Bearer, il DÉLIVRE le Bearer.
36
+ * @remarks Ces 401 portent le défi `Session realm="nodefony"` (RFC 9110
37
+ * §15.5.2, posé par `SecurityApiController`) et non un `Bearer` : l'endpoint
38
+ * d'émission n'est pas une ressource protégée par Bearer, il DÉLIVRE le Bearer.
39
39
  */
40
40
  declare class TokenAuthController extends SecurityApiController {
41
41
  #private;
@@ -9,6 +9,7 @@ import { ControllerConstructor } from "../src/Route.js";
9
9
  import type { IRouteSchema, RawRouteInput, RouteInput } from "../src/routeSchema.js";
10
10
  import type { SessionIntent } from "@nodefony/http";
11
11
  type Constructor<T = {}> = new (...args: any[]) => T;
12
+ declare const getReservedActionNames: () => Map<string, string>;
12
13
  /**
13
14
  * Rattache un ou plusieurs contrôleurs à un module, dont ils suivent le cycle de vie.
14
15
  *
@@ -794,7 +795,7 @@ declare function resolveActionMeta(routeDef: {
794
795
  classMethod?: string | undefined;
795
796
  actionMeta?: RouteActionMeta | undefined;
796
797
  }): RouteActionMeta;
797
- export { route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, resolveSessionIntent, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, resolveParamArg, buildParamArgs, routeExpectsBodyStream, computeActionMeta, resolveActionMeta, extractActionScopes, };
798
+ export { route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, resolveSessionIntent, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, resolveParamArg, buildParamArgs, routeExpectsBodyStream, computeActionMeta, resolveActionMeta, extractActionScopes, applyDeclaredRouteOptions, getReservedActionNames, };
798
799
  /** Options de {@link RequireAuthentication}. */
799
800
  export interface IRequireAuthenticationOptions {
800
801
  /** Âge maximal de la connexion, en secondes. Omis = aucune limite d'âge. */
@@ -0,0 +1,75 @@
1
+ import Controller from "../src/Controller.js";
2
+ /**
3
+ * Classe de base d'un point d'entrée gRPC (ADR-0026 D1), jumelle de
4
+ * `RealtimeController` : marquée par `@GrpcService(Contrat)`, déclarée par
5
+ * `@controllers([...])`. Chaque méthode du contrat est une méthode de la
6
+ * classe, appelée par l'adaptateur Connect avec `(request, HandlerContext)` ;
7
+ * la logique métier vit dans un service injecté.
8
+ *
9
+ * Singleton par défaut : aucune instance par appel, l'adaptateur lie les
10
+ * méthodes une fois. `@Scope("request")` donne une instance par appel.
11
+ *
12
+ * Ce que le framework décide pour chaque méthode, sans code de ta part :
13
+ * - les gardes (`@IsGranted`, `@RequireAuthentication`…) posées sur la
14
+ * méthode ou la classe s'appliquent ; un refus part en 16
15
+ * `UNAUTHENTICATED` sans identité, en 7 `PERMISSION_DENIED` sinon ;
16
+ * - `this.user`, l'ALS et la portée `request` valent pendant tout l'appel,
17
+ * flux compris ;
18
+ * - une `ConnectError` part telle quelle ; une `HttpError` est traduite par
19
+ * la table de `google/rpc/code.proto` ; toute autre exception devient 13
20
+ * `INTERNAL`. Dans un flux, la traduction ne vaut qu'avant le premier
21
+ * message ;
22
+ * - un flux est interrompu en 14 `UNAVAILABLE` à l'arrêt du serveur et en 16
23
+ * à la révocation de son identité : passer `ctx.signal` à ses sources ;
24
+ * - messages bornés à 4 Mio (section `grpc` de la configuration http).
25
+ *
26
+ * Un décorateur sans effet en gRPC (`@Body`, `@UseSession`, `@Csp`,
27
+ * `@Idempotent`…) sur une méthode du contrat interrompt le démarrage.
28
+ * Guide complet : `docs/grpc.md` du paquet.
29
+ *
30
+ * @example
31
+ * \@GrpcService(GreetService)
32
+ * export class GreetController extends GrpcController {
33
+ * constructor(context: ContextType) {
34
+ * super("GreetController", context);
35
+ * }
36
+ * greet(request: GreetRequest) {
37
+ * return { greeting: `Bonjour, ${request.name} !` };
38
+ * }
39
+ * }
40
+ *
41
+ * @experimental Surface gRPC, ADR-0026 D11.
42
+ */
43
+ export declare abstract class GrpcController extends Controller {
44
+ /**
45
+ * Le point d'entrée de transport, posé au démarrage sur le prototype de la
46
+ * classe déclarée (`assertScope`) : une lecture de prototype par appel.
47
+ */
48
+ private readonly grpcEndpoint;
49
+ /**
50
+ * Vérifie la déclaration gRPC de la classe et fige ses routes — au
51
+ * démarrage (`Router.setController`), jamais à la première requête.
52
+ *
53
+ * Refuse : une classe sans `@GrpcService` ; une méthode du contrat absente
54
+ * de la classe ou dont le nom est réservé par le framework ; un décorateur
55
+ * sans effet en gRPC sur une méthode du contrat (ou sur la classe) — le
56
+ * refus nomme la méthode et le décorateur.
57
+ *
58
+ * Fige ensuite, sur chaque route, la méta d'action lue sur la MÉTHODE DU
59
+ * CONTRAT (l'action exécutée reste celle de la base) et le corps laissé en
60
+ * flux : les gardes valent quel que soit l'ordre des décorateurs de classe.
61
+ *
62
+ * @throws BootConfigurationError sur une déclaration refusée.
63
+ */
64
+ static assertScope(): void;
65
+ /**
66
+ * L'action de base de toute route gRPC : remet la requête et la réponse
67
+ * brutes à l'adaptateur, qui appelle la méthode du contrat et écrit la
68
+ * réponse. Synchrone, sauf pour un flux dont la session est à sauver : la
69
+ * promesse de la remise est alors rendue au résolveur, qui en lit l'échec.
70
+ *
71
+ * @throws Error quand la classe n'a pas été enregistrée par le routeur, ou
72
+ * hors d'une requête HTTP.
73
+ */
74
+ serveGrpc(): void | Promise<void>;
75
+ }
@@ -0,0 +1,51 @@
1
+ import type { DescService } from "@bufbuild/protobuf";
2
+ import type { ServiceImpl } from "@connectrpc/connect";
3
+ import type { GrpcController } from "./GrpcController.js";
4
+ /**
5
+ * Une classe qui peut servir le contrat `S` : elle étend `GrpcController` et
6
+ * implémente CHAQUE méthode du contrat avec sa signature générée. Une méthode
7
+ * manquante ou mal typée ne compile pas.
8
+ *
9
+ * @experimental Surface gRPC, ADR-0026 D11.
10
+ */
11
+ export type GrpcServiceClass<S extends DescService> = new (...args: never[]) => GrpcController & ServiceImpl<S>;
12
+ /**
13
+ * Déclare un contrôleur comme le point d'entrée du service gRPC `service`
14
+ * (le descripteur généré par `buf`, ex. `EchoService` de `echo_pb.ts`).
15
+ *
16
+ * Monte une route littérale du Router par méthode du contrat,
17
+ * `POST /<paquet.Service>/<Méthode>`, plus `GET` pour une méthode déclarée
18
+ * `option idempotency_level = NO_SIDE_EFFECTS` (le GET de Connect). Ces
19
+ * routes passent par tout le pipeline — pare-feu, `@IsGranted`, hôtes
20
+ * virtuels, profileur, liste des routes —, et leurs gardes se lisent sur la
21
+ * méthode du contrat. La classe se déclare ensuite par `@controllers([...])`,
22
+ * comme tout point d'entrée d'un module.
23
+ *
24
+ * Les décorateurs de classe `@Domain` et `@BypassFirewall` se placent SOUS
25
+ * `@GrpcService` (lus au montage des routes, comme sous `@controller`).
26
+ *
27
+ * Le descripteur vient du code généré, jamais écrit à la main : contrat dans
28
+ * `proto/<paquet>/v1/*.proto`, `npx buf generate` vers `nodefony/gen/`, code
29
+ * commité. Les routes répondent sur 5152 (gRPC, gRPC-Web, Connect), 5151
30
+ * (gRPC-Web, Connect : HTTP/1.1, jamais le gRPC natif) et 5153 si h2c est
31
+ * activé. Guide complet : `docs/grpc.md` du paquet.
32
+ *
33
+ * @param service - le descripteur du service.
34
+ * @returns le décorateur de classe.
35
+ * @throws BootConfigurationError quand une autre classe sert déjà ce contrat.
36
+ *
37
+ * @example
38
+ * \@GrpcService(EchoService)
39
+ * export class EchoController extends GrpcController {
40
+ * constructor(context: ContextType) {
41
+ * super("EchoController", context);
42
+ * }
43
+ * \@IsGranted("ROLE_ECHO")
44
+ * say(request: SayRequest) {
45
+ * return { sentence: request.sentence };
46
+ * }
47
+ * }
48
+ *
49
+ * @experimental Surface gRPC, ADR-0026 D11.
50
+ */
51
+ export declare function GrpcService<S extends DescService>(service: S): <C extends GrpcServiceClass<S>>(ctor: C) => C;
@@ -0,0 +1,12 @@
1
+ import type { DescMethod, DescService } from "@bufbuild/protobuf";
2
+ import type Route from "../src/Route.js";
3
+ /** Une route montée par `@GrpcService`, et la méthode du contrat qu'elle sert. */
4
+ export interface IGrpcRoute {
5
+ readonly route: Route;
6
+ readonly method: DescMethod;
7
+ }
8
+ /** Ce que `@GrpcService` a déclaré pour une classe, lu au démarrage. */
9
+ export interface IGrpcDeclaration {
10
+ readonly service: DescService;
11
+ readonly routes: readonly IGrpcRoute[];
12
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@nodefony/framework/grpc` — déclarer un service gRPC dans un module
3
+ * (ADR-0026 D1).
4
+ *
5
+ * Sous-chemin à part : le point d'entrée principal de `@nodefony/framework`
6
+ * ne cite ni Connect ni protobuf (D12). L'importer charge Connect au
7
+ * démarrage — ce que fait tout contrôleur gRPC, et seulement lui.
8
+ *
9
+ * @experimental Surface gRPC, ADR-0026 D11.
10
+ */
11
+ export { GrpcController } from "./GrpcController.js";
12
+ export { GrpcService } from "./GrpcService.js";
13
+ export type { GrpcServiceClass } from "./GrpcService.js";
@@ -1,4 +1,4 @@
1
- import type { contextRequest, HTTPMethod, HttpResponse, Http2Response, WebsocketResponse, Session, ContextType } from "@nodefony/http";
1
+ import type { contextRequest, HTTPMethod, HttpResponse, Http2Response, WebsocketResponse, Session, ContextType, IUploadedFile } from "@nodefony/http";
2
2
  import type { Module, FileClass, MaybePromise } from "nodefony";
3
3
  import type { OutgoingHttpHeaders } from "node:http";
4
4
  import type { ReadStream } from "node:fs";
@@ -11,7 +11,7 @@ export interface IController {
11
11
  session?: Session | null;
12
12
  method?: HTTPMethod | undefined;
13
13
  query: Record<string, unknown>;
14
- files: readonly unknown[];
14
+ files: readonly IUploadedFile[];
15
15
  body: Record<string, unknown>;
16
16
  module?: Module | undefined;
17
17
  setContext(context: ContextType): void;
@@ -34,6 +34,6 @@ export interface IController {
34
34
  getFile(file: FileClass | string): FileClass;
35
35
  /** Variante async de `getFile` (stat non bloquant via `FileClass.from`). */
36
36
  getFileAsync(file: FileClass | string): Promise<FileClass>;
37
- renderFileDownload(file: unknown, options?: unknown, headers?: OutgoingHttpHeaders): Promise<ReadStream>;
38
- streamFile(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: Record<string, unknown>): Promise<ReadStream>;
37
+ renderFileDownload(file: unknown, options?: unknown, headers?: OutgoingHttpHeaders): Promise<ReadStream | null>;
38
+ streamFile(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: Record<string, unknown>): Promise<ReadStream | null>;
39
39
  }
@@ -60,10 +60,11 @@ declare class Router extends Service implements IRequestRouter {
60
60
  * lieu de `context.request.url` — permet de router un path **porté par un message**
61
61
  * (WS-RPC `invoke`) vers une action, sans muter l'URL de la connexion (état partagé).
62
62
  * `undefined` (cas hot path normal) → comportement inchangé.
63
- * @param methodOverride - méthode HTTP **logique** à exiger en plus du transport
64
- * WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
65
- * GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
66
- * `undefined` (GET/HTTP) → match historique sur `context.method`.
63
+ * @param methodOverride - méthode HTTP **logique** du pont WS-RPC `api.request`,
64
+ * GET compris, exigée en plus du transport WEBSOCKET par la règle du cœur
65
+ * `bridgeServesVerb` : un GET n'atteint jamais une action POST du même
66
+ * chemin. `undefined` (requête HTTP, ouverture de socket) → match sur
67
+ * `context.method`.
67
68
  * @returns un `Resolver` (`.resolve === true` si une route a matché) — ou,
68
69
  * pour un chemin servi en WebSocket SEULEMENT et demandé en HTTP, un
69
70
  * `Resolver` dont `exception` porte le `426` (en-têtes `Upgrade` posés) :
@@ -111,8 +112,9 @@ declare class Router extends Service implements IRequestRouter {
111
112
  * installées du framework feraient échouer `instanceof` en silence. Une
112
113
  * lecture de propriété.
113
114
  *
114
- * @param router - le routeur lu sur le contexte (`context.router`).
115
+ * @param router - le routeur lu sur le contexte (`context.router`) ou le
116
+ * service `router` du conteneur.
115
117
  * @returns `true` si c'est le Router du framework.
116
118
  */
117
- export declare function isFrameworkRouter(router: IRequestRouter | null | undefined): router is Router;
119
+ export declare function isFrameworkRouter(router: unknown): router is Router;
118
120
  export default Router;
@@ -1,12 +1,13 @@
1
1
  import { Service, Module, type Pci, type Severity, type Msgid, type Message, FileClass } from "nodefony";
2
- import type { IRequestUser, MaybePromise } from "nodefony";
2
+ import type { DefaultEventsMap, EventsMap, IRequestUser, MaybePromise } from "nodefony";
3
3
  import type { IController } from "../interfaces/index.js";
4
4
  import Route from "./Route.js";
5
5
  import { contextRequest, HTTPMethod, HttpResponse, Session, ContextType, Http2Response, WebsocketResponse } from "@nodefony/http";
6
- import type { ISseStreamOptions, SseStream } from "@nodefony/http";
6
+ import type { ByteStreamSource, IByteStreamOptions, IByteStreamResult, ISseStreamOptions, IUploadedFile, SseStream } from "@nodefony/http";
7
7
  import { OutgoingHttpHeaders } from "node:http";
8
8
  import { ReadStream } from "node:fs";
9
9
  import Eta from "../service/Eta.js";
10
+ import type { IFileServingConfig } from "../config/config.js";
10
11
  type ReadStreamOptions = {
11
12
  flags?: string;
12
13
  encoding?: BufferEncoding;
@@ -17,6 +18,17 @@ type ReadStreamOptions = {
17
18
  start?: number;
18
19
  end?: number;
19
20
  highWaterMark?: number;
21
+ /**
22
+ * `false` coupe les requêtes conditionnelles : ni `ETag`, ni
23
+ * `Last-Modified`, ni 304/412, ni `Cache-Control` par défaut, et
24
+ * `If-Range` n'autorise aucune reprise. Pour des fichiers dont la date
25
+ * n'est pas fiable (archive décompressée, build reproductible : même taille
26
+ * et même date pour deux contenus) — passer plutôt sa propre `ETag`
27
+ * (hachage du contenu) garde le 304. Défaut : `fileServing.validators` de
28
+ * la config du module (`true` d'usine) ; `true` ici le rétablit pour un
29
+ * appel quand la config l'a coupé.
30
+ */
31
+ validators?: boolean;
20
32
  };
21
33
  /**
22
34
  * Parse un header `Range` mono-plage en octets (RFC 9110 §14.1.2).
@@ -50,6 +62,14 @@ export declare function parseByteRange(range: string, length: number): {
50
62
  * dans `initialize()`, ou garde un état sur `this`.
51
63
  */
52
64
  export type ControllerScope = "request" | "singleton";
65
+ /**
66
+ * Fixe le défaut des fichiers servis (validateurs, `Cache-Control`) pour
67
+ * toute l'application. Appelé par le module au boot avec sa config validée.
68
+ *
69
+ * @param config - section `fileServing` validée
70
+ */
71
+ export declare function configureFileServing(config: IFileServingConfig): void;
72
+ export declare function contentDisposition(type: "attachment" | "inline", name: string): string;
53
73
  declare class Controller extends Service implements IController {
54
74
  #private;
55
75
  static prefix: string;
@@ -125,8 +145,8 @@ declare class Controller extends Service implements IController {
125
145
  * texte du même corps sont dans {@link body}. Pour une action typée,
126
146
  * préférer `@UploadedFiles()`.
127
147
  */
128
- get files(): readonly unknown[];
129
- set files(value: readonly unknown[]);
148
+ get files(): readonly IUploadedFile[];
149
+ set files(value: readonly IUploadedFile[]);
130
150
  /**
131
151
  * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
132
152
  * Fastify, NestJS). Jamais un paramètre d'URL : ceux-là sont dans
@@ -176,6 +196,13 @@ declare class Controller extends Service implements IController {
176
196
  */
177
197
  renderResponse(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: OutgoingHttpHeaders): MaybePromise<Http2Response | HttpResponse | WebsocketResponse>;
178
198
  renderView(path: string | FileClass, param?: Record<string, unknown>, status?: string | number, headers?: Record<string, string | number>): Promise<Http2Response | HttpResponse | WebsocketResponse>;
199
+ /**
200
+ * Expose le nonce CSP de la requête à la vue (`cspNonce`), lu seulement si
201
+ * le gabarit s'en sert : un accesseur, pas une valeur — une vue sans
202
+ * `<script>` ni `<style>` ne tire aucun nonce. Une valeur `cspNonce`
203
+ * fournie par l'action est gardée.
204
+ */
205
+ private withNonceLocal;
179
206
  /**
180
207
  * Injecte les helpers frontend (`frontendTags`/`frontendDocument`) dans les
181
208
  * locals du template (passés en data, pas de registre global de fonctions).
@@ -217,11 +244,72 @@ declare class Controller extends Service implements IController {
217
244
  * }
218
245
  * ```
219
246
  *
247
+ * Typé par le contrat temps réel de l'application, le flux lit la MÊME carte
248
+ * que la socket : `this.renderSse<ContractServerEmit<ChatContract>>()`, puis
249
+ * `sse.send(m, { event: "chat:room" })`. Sans paramètre de type, il reste
250
+ * libre — un flux hors contrat est valide.
251
+ *
252
+ * @typeParam Events - carte des événements du flux (défaut : libre).
220
253
  * @param options - battement de cœur, délai `retry:` initial.
221
254
  * @returns le flux, ou sa promesse quand une session est à sauver d'abord.
222
255
  * @throws HttpError 500 hors HTTP (WebSocket) ou quand la réponse est partie.
223
256
  */
224
- renderSse(options?: ISseStreamOptions): SseStream | Promise<SseStream>;
257
+ renderSse<Events extends EventsMap = DefaultEventsMap>(options?: ISseStreamOptions): SseStream<Events> | Promise<SseStream<Events>>;
258
+ /**
259
+ * Répond par un FLUX D'OCTETS : un `<video src>`, un `fetch()`, un
260
+ * téléchargement ou un outil tiers (curl) le consomment comme n'importe quelle
261
+ * réponse HTTP, une réponse par consommateur. Pour des ÉVÉNEMENTS texte lus
262
+ * par `EventSource`, voir {@link Controller.renderSse}. Pour UNE source
263
+ * d'octets diffusée à N lecteurs par la socket, publier un `Uint8Array` sur
264
+ * un canal temps réel (`@nodefony/realtime`), pas une réponse HTTP.
265
+ *
266
+ * La réponse est prise à l'appel ; les en-têtes partent au premier morceau.
267
+ * Une erreur de la source AVANT lui rend la page d'erreur ordinaire (500, ou
268
+ * le code d'une `HttpError`) ; APRÈS, le client voit une coupure. La source
269
+ * appartient au flux dès l'appel : détruite à la fin, quelle qu'en soit la
270
+ * raison, et détruite aussi quand l'appel lève (un flux web est annulé). La
271
+ * promesse ne rejette jamais : elle rend la raison de la fin et les octets
272
+ * envoyés.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * const ff = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
277
+ * try {
278
+ * await this.renderStream(ff.stdout, { headers: { "content-type": "video/mp4" } });
279
+ * } finally {
280
+ * ff.kill("SIGKILL"); // le flux est fini, quelle qu'en soit la raison
281
+ * }
282
+ * ```
283
+ *
284
+ * Relais d'un autre service — le signal coupe l'amont au départ du client ;
285
+ * ne recopier que `content-type` (`fetch` a déjà décompressé le corps) :
286
+ * ```ts
287
+ * const r = await fetch(url, { signal: this.context.signal });
288
+ * if (!r.ok || r.body === null) throw new HttpError(502);
289
+ * return this.renderStream(r.body, {
290
+ * headers: { "content-type": r.headers.get("content-type") ?? "application/octet-stream" },
291
+ * });
292
+ * ```
293
+ *
294
+ * HEAD sans lancer le producteur — toute route GET sert HEAD, la source n'y
295
+ * est jamais lue. `Readable.from([])` seul est en mode OBJET, donc refusé
296
+ * (500) : le mode octets se demande.
297
+ * ```ts
298
+ * if (this.method === "HEAD") {
299
+ * return this.renderStream(Readable.from([], { objectMode: false }), {
300
+ * headers: { "content-type": "video/mp4" },
301
+ * });
302
+ * }
303
+ * ```
304
+ *
305
+ * @param source - flux Node, ou flux web ; jamais en mode objet.
306
+ * @param options - statut, en-têtes, `idleTimeout`, `stallTimeout`,
307
+ * `maxDuration`, `onClose(reason)`.
308
+ * @returns la raison de la fin et le nombre d'octets envoyés.
309
+ * @throws HttpError 500 hors HTTP (WebSocket) ou quand la réponse est partie ;
310
+ * 429 au-delà du plafond de connexions par IP. La source est alors détruite.
311
+ */
312
+ renderStream(source: ByteStreamSource, options?: IByteStreamOptions): Promise<IByteStreamResult>;
225
313
  /**
226
314
  * Pose la route de la requête courante sur l'instance (per-request).
227
315
  *
@@ -281,9 +369,10 @@ declare class Controller extends Service implements IController {
281
369
  * @param file - chemin ou `FileClass` du fichier à servir.
282
370
  * @param options - options de `createReadStream`.
283
371
  * @param headers - en-têtes ajoutés (écrasent ceux posés par défaut).
284
- * @returns le flux de lecture, une fois la réponse écoulée.
372
+ * @returns le flux de lecture, une fois la réponse écoulée — `null` quand
373
+ * les préconditions ont répondu 304 ou 412 (cf {@link Controller.streamFile}).
285
374
  */
286
- renderFileDownload(file: FileClass | string, options?: ReadStreamOptions, headers?: OutgoingHttpHeaders): Promise<ReadStream>;
375
+ renderFileDownload(file: FileClass | string, options?: ReadStreamOptions, headers?: OutgoingHttpHeaders): Promise<ReadStream | null>;
287
376
  /**
288
377
  * Envoie un fichier en FLUX, du disque vers la réponse, sans jamais le charger
289
378
  * en mémoire.
@@ -295,17 +384,38 @@ declare class Controller extends Service implements IController {
295
384
  * `Content-Range`). Annoncer `Accept-Ranges: bytes` depuis ici est un piège :
296
385
  * le client croit pouvoir se déplacer et reçoit tout le fichier.
297
386
  *
298
- * Ce que cette méthode garantit et qui justifie son existence : le NETTOYAGE.
299
- * Le flux est ouvert en `autoClose: false` ; un client qui raccroche en plein
300
- * téléchargement laisserait sinon un descripteur ouvert et une promesse pendue.
387
+ * Le cycle de vie est celui de toute réponse en flux
388
+ * ({@link Controller.renderStream}) : la réponse est prise à l'appel, les
389
+ * en-têtes partent au premier morceau, le flux est DÉTRUIT (descripteur
390
+ * fermé) quand le client raccroche. Un fichier illisible à l'ouverture
391
+ * (`EACCES`) rend la page d'erreur ordinaire, 500 ; une lecture qui échoue
392
+ * en cours de route coupe la connexion. Sans délai d'inactivité (un disque
393
+ * n'est pas une source muette).
394
+ *
395
+ * **Requêtes conditionnelles** (RFC 9110 §13) : le fichier ENTIER porte une
396
+ * `ETag` forte et un `Last-Modified`, tirés de sa taille et de sa date de
397
+ * modification ; `If-Match`, `If-Unmodified-Since`, `If-None-Match` et
398
+ * `If-Modified-Since` y sont évalués dans l'ordre de §13.2.2 → 304 ou 412,
399
+ * sans ouvrir le fichier. Une `ETag` ou un `Last-Modified` passés dans
400
+ * `headers` (ou déjà posés sur la réponse) l'emportent, et servent à la
401
+ * comparaison. `Cache-Control: private, no-cache` est posé si l'action n'en
402
+ * fixe aucun — en passer un dans `headers` le remplace. Un EXTRAIT
403
+ * (`start`/`end`) n'est pas la représentation entière : il ne reçoit ni
404
+ * validateur ni évaluation.
405
+ * `{ validators: false }` dans `options` coupe le tout — validateurs,
406
+ * évaluation et `Cache-Control` par défaut — pour des fichiers dont la date
407
+ * n'est pas fiable ; une `ETag` tirée du contenu garde, elle, le 304.
301
408
  *
302
409
  * @param file - chemin ou `FileClass` du fichier à servir.
303
410
  * @param headers - en-têtes ajoutés à la réponse (le type MIME est déduit si absent).
304
411
  * @param options - options de `createReadStream` (`start`/`end` pour un extrait).
305
- * @returns le flux de lecture, une fois la réponse écoulée.
306
- * @throws Si la réponse n'est pas disponible, ou si le fichier est illisible.
412
+ * @returns le flux de lecture, une fois la réponse écoulée — détruit, quelle
413
+ * que soit la fin ; `null` quand les préconditions ont répondu 304 ou 412 :
414
+ * aucun fichier n'a alors été ouvert.
415
+ * @throws HttpError 404 si le fichier n'existe pas ; 500 hors HTTP
416
+ * (WebSocket) ou quand la réponse est déjà partie.
307
417
  */
308
- streamFile(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: ReadStreamOptions | undefined): Promise<ReadStream>;
309
- renderMediaStream(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: ReadStreamOptions | undefined): Promise<HttpResponse | ReadStream | WebsocketResponse>;
418
+ streamFile(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: ReadStreamOptions): Promise<ReadStream | null>;
419
+ renderMediaStream(file: FileClass | string, headers?: OutgoingHttpHeaders, options?: ReadStreamOptions | undefined): Promise<HttpResponse | ReadStream | WebsocketResponse | null>;
310
420
  }
311
421
  export default Controller;