@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js +7 -0
- package/dist/index.js +14 -10
- package/dist/nodefony/config/config.js +16 -2
- package/dist/nodefony/controller/AdminApiController.js +18 -5
- package/dist/nodefony/controller/BenchController.js +1 -1
- package/dist/nodefony/controller/CspReportController.js +70 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +3 -3
- package/dist/nodefony/controller/LoginPageController.js +5 -7
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +75 -10
- package/dist/nodefony/controller/SessionAuthController.js +6 -7
- package/dist/nodefony/controller/TokenAuthController.js +3 -3
- package/dist/nodefony/decorators/routerDecorators.js +103 -15
- package/dist/nodefony/grpc/GrpcController.js +138 -0
- package/dist/nodefony/grpc/GrpcService.js +75 -0
- package/dist/nodefony/grpc/declarations.js +33 -0
- package/dist/nodefony/grpc/index.js +3 -0
- package/dist/nodefony/service/AdminBroker.js +1 -1
- package/dist/nodefony/service/IdempotencyStore.js +1 -1
- package/dist/nodefony/service/router.js +15 -12
- package/dist/nodefony/src/Controller.js +330 -115
- package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
- package/dist/nodefony/src/Resolver.js +85 -51
- package/dist/nodefony/src/Route.js +45 -45
- package/dist/nodefony/src/SecurityApiController.js +30 -7
- package/dist/nodefony/src/conditionalRequest.js +103 -0
- package/dist/nodefony/src/docsReader.js +1 -1
- package/dist/nodefony/src/idempotency.js +1 -1
- package/dist/nodefony/src/loginPage.js +3 -2
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/viewNonce.js +58 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/nodefony/config/config.d.ts +20 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
- package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
- package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
- package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
- package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
- package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
- package/dist/types/nodefony/grpc/index.d.ts +13 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +4 -4
- package/dist/types/nodefony/service/router.d.ts +8 -6
- package/dist/types/nodefony/src/Controller.d.ts +124 -14
- package/dist/types/nodefony/src/Resolver.d.ts +35 -8
- package/dist/types/nodefony/src/Route.d.ts +35 -0
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
- package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
- package/docs/controller.md +234 -35
- package/docs/decorateurs.md +1 -1
- package/docs/grpc.md +690 -0
- package/docs/idempotence.md +13 -8
- package/docs/index.md +16 -10
- package/docs/routing.md +30 -2
- package/docs/templates.md +15 -0
- 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** :
|
|
42
|
-
*
|
|
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`)
|
|
10
|
-
*
|
|
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
|
|
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
|
|
66
|
-
*
|
|
67
|
-
* session, et un
|
|
68
|
-
* navigateur
|
|
69
|
-
*
|
|
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
|
|
37
|
-
*
|
|
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
|
|
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**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* `undefined` (
|
|
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:
|
|
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
|
|
129
|
-
set files(value: readonly
|
|
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
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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;
|