@nodefony/framework 10.0.0-alpha.1

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 (96) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +50 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +211 -0
  6. package/dist/nodefony/config/config.js +61 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  8. package/dist/nodefony/controller/AdminApiController.js +163 -0
  9. package/dist/nodefony/controller/ApiKeyController.js +151 -0
  10. package/dist/nodefony/controller/BenchController.js +132 -0
  11. package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
  12. package/dist/nodefony/controller/OAuth2Controller.js +133 -0
  13. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
  14. package/dist/nodefony/controller/SessionAuthController.js +141 -0
  15. package/dist/nodefony/controller/TokenAuthController.js +113 -0
  16. package/dist/nodefony/controller/TotpController.js +129 -0
  17. package/dist/nodefony/controller/WebAuthnController.js +242 -0
  18. package/dist/nodefony/controller/oauthAuthority.js +74 -0
  19. package/dist/nodefony/decorators/routerDecorators.js +967 -0
  20. package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
  21. package/dist/nodefony/interfaces/IController.js +1 -0
  22. package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
  23. package/dist/nodefony/interfaces/IResolver.js +1 -0
  24. package/dist/nodefony/interfaces/IRoute.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/service/AdminBroker.js +106 -0
  27. package/dist/nodefony/service/Eta.js +68 -0
  28. package/dist/nodefony/service/IdempotencyStore.js +136 -0
  29. package/dist/nodefony/service/router.js +243 -0
  30. package/dist/nodefony/src/Controller.js +515 -0
  31. package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
  32. package/dist/nodefony/src/KernelAdminApi.js +1243 -0
  33. package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
  34. package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
  35. package/dist/nodefony/src/Resolver.js +416 -0
  36. package/dist/nodefony/src/ResourceController.js +148 -0
  37. package/dist/nodefony/src/Route.js +476 -0
  38. package/dist/nodefony/src/SyslogAdminApi.js +466 -0
  39. package/dist/nodefony/src/Template.js +15 -0
  40. package/dist/nodefony/src/configMutation.js +186 -0
  41. package/dist/nodefony/src/docsReader.js +929 -0
  42. package/dist/nodefony/src/idempotency.js +137 -0
  43. package/dist/nodefony/src/idempotencyGc.js +32 -0
  44. package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
  45. package/dist/nodefony/src/scopeCatalog.js +40 -0
  46. package/dist/nodefony/src/syslogFilters.js +51 -0
  47. package/dist/types/index.d.ts +96 -0
  48. package/dist/types/nodefony/config/config.d.ts +41 -0
  49. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  50. package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
  51. package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
  52. package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
  53. package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
  54. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
  55. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
  56. package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
  57. package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
  58. package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
  59. package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
  60. package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
  61. package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
  62. package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
  63. package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
  64. package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
  65. package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
  66. package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
  67. package/dist/types/nodefony/interfaces/index.d.ts +5 -0
  68. package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
  69. package/dist/types/nodefony/service/Eta.d.ts +25 -0
  70. package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
  71. package/dist/types/nodefony/service/router.d.ts +53 -0
  72. package/dist/types/nodefony/src/Controller.d.ts +193 -0
  73. package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
  74. package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
  75. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
  76. package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
  77. package/dist/types/nodefony/src/Resolver.d.ts +165 -0
  78. package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
  79. package/dist/types/nodefony/src/Route.d.ts +192 -0
  80. package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
  81. package/dist/types/nodefony/src/Template.d.ts +8 -0
  82. package/dist/types/nodefony/src/configMutation.d.ts +109 -0
  83. package/dist/types/nodefony/src/docsReader.d.ts +369 -0
  84. package/dist/types/nodefony/src/idempotency.d.ts +96 -0
  85. package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
  86. package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
  87. package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
  88. package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
  89. package/docs/admin.md +451 -0
  90. package/docs/controller.md +645 -0
  91. package/docs/decorateurs.md +845 -0
  92. package/docs/idempotence.md +741 -0
  93. package/docs/index.md +151 -0
  94. package/docs/routing.md +648 -0
  95. package/docs/templates.md +380 -0
  96. package/package.json +83 -0
@@ -0,0 +1,632 @@
1
+ import "reflect-metadata";
2
+ import { TypeController } from "../service/router.js";
3
+ import { RouteOptions } from "../src/Route.js";
4
+ import Controller from "../src/Controller.js";
5
+ import type { ControllerScope } from "../src/Controller.js";
6
+ import { Module } from "nodefony";
7
+ import { ControllerConstructor } from "../src/Route.js";
8
+ import type { SessionIntent } from "@nodefony/http";
9
+ type Constructor<T = {}> = new (...args: any[]) => T;
10
+ /**
11
+ * Rattache un ou plusieurs contrôleurs à un module, dont ils suivent le cycle de vie.
12
+ *
13
+ * Décorateur de **classe de module**. L'enregistrement n'a pas lieu à
14
+ * l'évaluation du décorateur mais au hook `onBoot` du kernel : tant que le boot
15
+ * n'a pas eu lieu, les routes déclarées par `@route` sur ces classes n'existent
16
+ * pas encore dans le routeur — un test qui interroge le routeur sans booter ne
17
+ * verra rien. L'enregistrement est tagué au nom du module, de sorte qu'un échec
18
+ * désigne le module fautif au lieu d'un contrôleur anonyme.
19
+ *
20
+ * @param controller - Un contrôleur, ou un tableau de contrôleurs, à rattacher.
21
+ * @returns Le décorateur de classe, qui renvoie le module enrichi du hook.
22
+ * @example
23
+ * ```typescript
24
+ * @controllers([DefaultController, RestController])
25
+ * class TestModule extends Module {}
26
+ * ```
27
+ */
28
+ declare function controllers(targets: TypeController<Controller>[] | TypeController<Controller>): <T extends Constructor<Module>>(constructor: T) => T;
29
+ /**
30
+ * Declaration Controller
31
+ *
32
+ * @param prefix - prefixage du router du controller.
33
+ * @param options - Les options .
34
+ * @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
35
+ *
36
+ * @example
37
+ * \@controller("/openapi")
38
+ * \@UseSession()
39
+ * class OpenApiController extends Controller {
40
+ * constructor(context: Context) {
41
+ * super("OpenApiController", context);
42
+ * }
43
+ * \@route("index-openapi", { path: "" })
44
+ * index() {
45
+ * this.render({});
46
+ * }
47
+ * }
48
+ */
49
+ declare function controller(prefix: string): <T extends ControllerConstructor & {
50
+ prefix?: string;
51
+ }>(mycontroller: T) => T;
52
+ /**
53
+ * Crée une route avec le nom et les options spécifiés.
54
+ *
55
+ * @param name - Le nom de la route.
56
+ * @param options - Les options de la route.
57
+ * @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
58
+ *
59
+ * @example
60
+ * \@route("myroute", {
61
+ * path: "/add/{name}",
62
+ * method: ["GET", "POST"],
63
+ * defaults: { name: "john" },
64
+ * })
65
+ * method(name: string) {
66
+ * return this.renderJson({ name });
67
+ * }
68
+ */
69
+ declare function route(name: string, options: RouteOptions): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
70
+ export declare const HTTP_CODE_METADATA = "route:httpCode";
71
+ export declare const HEADERS_METADATA = "route:responseHeaders";
72
+ export declare const REDIRECT_METADATA = "route:redirect";
73
+ export declare const PARAM_ARGS_METADATA = "route:paramArgs";
74
+ export declare const DOMAIN_CLASS_METADATA = "route:domainClass";
75
+ export declare const DOMAIN_METHOD_METADATA = "route:domainMethod";
76
+ export declare const BYPASS_FIREWALL_CLASS_METADATA = "route:bypassFirewallClass";
77
+ export declare const BYPASS_FIREWALL_METHOD_METADATA = "route:bypassFirewallMethod";
78
+ export declare const USE_SESSION_CLASS_METADATA = "session:useClass";
79
+ export declare const USE_SESSION_METHOD_METADATA = "session:useMethod";
80
+ export type ParamSource = "param" | "body" | "query" | "headers" | "cookie" | "session" | "req" | "res" | "file" | "files" | "user";
81
+ export interface ParamMeta {
82
+ source: ParamSource;
83
+ key?: string;
84
+ index: number;
85
+ /**
86
+ * P2.9 — `@Body({ stream: true })` : injecte le **flux brut** de la requête
87
+ * (`IncomingMessage`, un `Readable`) au lieu du body parsé en mémoire. Permet
88
+ * de piper un gros upload (vidéo, backup) vers disque/S3 sans pic RAM. Le
89
+ * pipeline saute le parse busboy/JSON pour la route concernée (cf
90
+ * `routeExpectsBodyStream` + `handleHttp`).
91
+ */
92
+ stream?: boolean;
93
+ }
94
+ export interface RedirectMeta {
95
+ url: string;
96
+ statusCode: number;
97
+ }
98
+ /**
99
+ * Une clause `@IsGranted` : un OU plusieurs attributs (OR interne), sujet
100
+ * optionnel. `@IsGranted("ROLE_ADMIN")` → `{ anyOf: ["ROLE_ADMIN"] }` ;
101
+ * `@IsGranted(["A","B"])` → `{ anyOf: ["A","B"] }` (un seul suffit) ;
102
+ * `@IsGranted("doc.edit", { subject: "id" })` → le param de route `id` est passé
103
+ * au voter.
104
+ */
105
+ export interface SecurityClause {
106
+ /** Attributs en OR — un seul accordé suffit pour valider la clause. */
107
+ readonly anyOf: readonly string[];
108
+ /** Nom du paramètre de route passé comme `subject` au voter (optionnel). */
109
+ readonly subjectParam?: string;
110
+ }
111
+ /**
112
+ * Exigence d'autorisation **figée** d'une action — calculée UNE fois par route
113
+ * (fusion classe + méthode) puis gelée sur `RouteActionMeta.security`. Objet
114
+ * PARTAGÉ entre toutes les requêtes : ne jamais muter. `null` (hors de ce type)
115
+ * = aucune garde → coût nul sur le hot path.
116
+ *
117
+ * Plusieurs `@IsGranted` empilés = `clauses` multiples en **AND** (toutes doivent
118
+ * passer). `@Anonymous()` ne produit jamais de `SecurityRequirement` (l'action
119
+ * devient `security: null` = publique).
120
+ */
121
+ export interface SecurityRequirement {
122
+ /** Clauses en AND — toutes doivent être accordées (chacune est un OR interne). */
123
+ readonly clauses: readonly SecurityClause[];
124
+ }
125
+ /**
126
+ * Directives CSP additionnelles déclarées par une action (`@Csp`) :
127
+ * `directive → sources` (ex. `{ "frame-src": ["https://youtube.com"] }`).
128
+ * Structurellement compatible avec `CspFragment` (@nodefony/security) — aucun
129
+ * import cross-module (0 cycle). Mergé additivement dans le CSP de la réponse
130
+ * par le firewall, UNIQUEMENT sur les routes qui en déclarent (cold path).
131
+ */
132
+ export type CspDirectives = Record<string, readonly string[]>;
133
+ /**
134
+ * Configuration d'idempotence figée d'une action (`@Idempotent`) — calculée une
135
+ * fois par route (fusion classe + méthode) puis gelée sur `RouteActionMeta.idempotent`.
136
+ * `null` (hors de ce type) = action non idempotentée → 0 coût hot path.
137
+ */
138
+ export interface IdempotentMeta {
139
+ /**
140
+ * Mode STRICT : une mutation sans `Idempotency-Key` est rejetée (400). Défaut
141
+ * `true` (`@Idempotent()`). `@Idempotent({ required: false })` = mode souple
142
+ * (honore la clé si présente, exécute sinon) — sauf en WS, toujours strict.
143
+ */
144
+ readonly required: boolean;
145
+ }
146
+ type MethodDecoratorOptions = Omit<RouteOptions, "path" | "method">;
147
+ declare const Get: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
148
+ declare const Post: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
149
+ declare const Put: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
150
+ declare const Delete: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
151
+ declare const Patch: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
152
+ declare const Options: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
153
+ declare const Head: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
154
+ /**
155
+ * `@All` — route sans restriction de méthode : matche **toutes** les méthodes
156
+ * HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
157
+ * `Route.matchRequirements` ne lève jamais 405 sur la méthode.
158
+ */
159
+ declare function All(path?: string, options?: MethodDecoratorOptions): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
160
+ /**
161
+ * Fixe le code de statut HTTP de la réponse d'une action.
162
+ *
163
+ * Décorateur de **méthode**. Le code est posé sur la réponse **avant** que le
164
+ * corps de l'action ne s'exécute (`Resolver._applyResponseMeta`) : l'action
165
+ * garde donc le dernier mot et peut encore le remplacer. Emploie-le pour le
166
+ * statut nominal d'une action — un 201 sur une création — et non pour un statut
167
+ * qui dépend du résultat. La métadonnée n'est lue qu'une fois par route, puis
168
+ * mémorisée : le décorateur ne coûte rien par requête.
169
+ *
170
+ * @param statusCode - Code HTTP appliqué à la réponse (201, 204, 202…).
171
+ * @returns Le décorateur de méthode.
172
+ * @example
173
+ * ```typescript
174
+ * @route("item-create", { path: "/items", method: "POST" })
175
+ * @HttpCode(201)
176
+ * async create() {
177
+ * return this.renderJson({ id: 42 });
178
+ * }
179
+ * ```
180
+ */
181
+ declare function HttpCode(statusCode: number): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
182
+ /**
183
+ * Ajoute un en-tête à la réponse d'une action.
184
+ *
185
+ * Décorateur de **méthode**, empilable : chaque application ajoute une entrée,
186
+ * la dernière l'emportant sur un même nom d'en-tête. Les en-têtes sont posés
187
+ * avant l'exécution du corps de l'action, qui peut donc encore les modifier.
188
+ * Réserve-le aux en-têtes constants d'une action ; ce qui dépend de la requête
189
+ * s'écrit dans le corps.
190
+ *
191
+ * @param key - Nom de l'en-tête.
192
+ * @param value - Valeur de l'en-tête.
193
+ * @returns Le décorateur de méthode.
194
+ * @example
195
+ * ```typescript
196
+ * @route("feed", { path: "/feed", method: "GET" })
197
+ * @Header("Cache-Control", "public, max-age=3600")
198
+ * async feed() {
199
+ * return this.renderJson(items);
200
+ * }
201
+ * ```
202
+ */
203
+ declare function Header(key: string, value: string): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
204
+ /**
205
+ * Redirige la réponse d'une action vers une autre URL.
206
+ *
207
+ * Décorateur de **méthode**. ⚠️ Le corps de l'action **est exécuté** : la
208
+ * redirection est portée à côté du résultat et appliquée après coup par le
209
+ * `Resolver`. Ce n'est donc pas un court-circuit — tout effet de bord écrit dans
210
+ * l'action a bien lieu. L'action peut d'ailleurs surcharger la cible ou le code
211
+ * en renvoyant sa propre redirection.
212
+ *
213
+ * @param url - URL cible, absolue ou relative à l'application.
214
+ * @param statusCode - Code HTTP de redirection. Défaut `302` (temporaire) ;
215
+ * `301` pour un déplacement permanent, `307` pour conserver la méthode.
216
+ * @returns Le décorateur de méthode.
217
+ * @example
218
+ * ```typescript
219
+ * @route("legacy", { path: "/old-path", method: "GET" })
220
+ * @Redirect("/new-path", 301)
221
+ * async oldPath() {}
222
+ * ```
223
+ */
224
+ declare function Redirect(url: string, statusCode?: number): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
225
+ /**
226
+ * Restreint une route (décorateur de **méthode**) ou tout un contrôleur
227
+ * (décorateur de **classe**) à un ou plusieurs vhosts. Source de vérité du
228
+ * domaine de routing — le `host` posé ici alimente `Route.host`, compilé en
229
+ * RegExp ancrée/wildcard (matcher partagé `@nodefony/http`). Domaine non servi
230
+ * par la route → 403.
231
+ *
232
+ * Précédence : `@route({ host })` > `@Domain` méthode > `@Domain` classe.
233
+ *
234
+ * Pattern : exact (`"marseille.fr"`) ou wildcard un-label (`"*.cdn.nodefony.com"`).
235
+ *
236
+ * ⚠️ En décorateur de **classe**, placer `@Domain` SOUS `@controller` : les
237
+ * décorateurs de classe s'appliquent de bas en haut, et `@controller` construit
238
+ * les routes — il doit voir le domaine de classe déjà posé.
239
+ *
240
+ * @example
241
+ * \@controller("/")
242
+ * \@Domain("marseille.fr")
243
+ * class MarseilleController extends Controller {
244
+ * \@Get("/") home() {} // marseille.fr/ → OK ; nodefony.com/ → 403
245
+ * }
246
+ */
247
+ declare function Domain(patterns: string | string[]): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
248
+ /**
249
+ * Déclare une route (décorateur de **méthode**) ou tout un contrôleur
250
+ * (décorateur de **classe**) comme **PUBLIQUE** : le firewall ne s'exécute pas
251
+ * (`Route.bypassFirewall`). Pour la **liveness** (`/health`, `/info` — sondes
252
+ * k8s/monitoring NON authentifiées, ping pré-login), les **webhooks signés**, ou
253
+ * un endpoint d'auth (login). Sucre déclaratif sur l'option
254
+ * `RouteOptions.bypassFirewall` (les deux coexistent ; l'option l'emporte).
255
+ *
256
+ * Précédence : `@Get({ bypassFirewall })` > `@BypassFirewall` méthode > classe.
257
+ * Lu au montage `@controller` (ordre des décorateurs indifférent). **Fail-closed** :
258
+ * un oubli laisse la route GATÉE (401), jamais ouverte par erreur.
259
+ *
260
+ * ⚠️ En décorateur de **classe**, placer `@BypassFirewall` SOUS `@controller`
261
+ * (décorateurs de classe appliqués de bas en haut). Préfigure `@Public`/
262
+ * `@Anonymous` (P6.8b) — sémantique « pas d'auth », qui s'appuiera sur ce primitif.
263
+ *
264
+ * Décorateur SANS argument → **simple, SANS parenthèses** (`@BypassFirewall`,
265
+ * pas `@BypassFirewall()`) : c'est un DRAPEAU, pas une option paramétrée. Une
266
+ * factory (`()`) ne se justifie que pour passer des arguments (cf `@Get("/x")`,
267
+ * `@Domain("host")`).
268
+ *
269
+ * @example
270
+ * \@controller("/nodefony")
271
+ * class StudioController extends Controller {
272
+ * \@BypassFirewall
273
+ * \@Get("/studio/api/health") health() {} // public (liveness)
274
+ * \@Get("/studio/api/stats") stats() {} // gaté par l'aire data plane
275
+ * }
276
+ */
277
+ declare function BypassFirewall(target: any, propertyKey?: string, descriptor?: PropertyDescriptor): any;
278
+ /**
279
+ * Déclare le scope d'instanciation d'un controller (V4.3) — pose le statique
280
+ * `scope` de la classe (hérité de `Controller`, défaut `"request"`). Lu par le
281
+ * constructor de `Controller` (`new.target`) et par le `Resolver` : 0 Reflect.
282
+ *
283
+ * `@Scope("singleton")` : UNE instance partagée par toutes les requêtes
284
+ * (cache kernel-scoped sur le Router, `initialize()` appelé 1× à la création).
285
+ * **Contrat stateless strict** : l'action ne lit/n'écrit AUCUN état par requête
286
+ * sur `this` — tout passe par les arguments décorés (`@Param`/`@Body`…) et les
287
+ * helpers, qui retrouvent la requête courante via l'ALS (V4.1). Un champ muté
288
+ * par requête sur un singleton = data race silencieuse entre deux requêtes
289
+ * concurrentes. Le défaut per-request reste inchangé (0 breaking legacy).
290
+ *
291
+ * ⚠️ Homonyme : le core `nodefony` exporte aussi `Scope` (le scope DI du
292
+ * `Container`) — celui-ci s'importe depuis `@nodefony/framework`.
293
+ *
294
+ * @example
295
+ * \@Scope("singleton")
296
+ * \@controller("/api/books")
297
+ * class BookController extends ResourceController { ... }
298
+ */
299
+ declare function Scope(scope: ControllerScope): <T extends {
300
+ scope?: ControllerScope;
301
+ }>(target: T) => void;
302
+ /** Options déclaratives de `@UseSession` (= forme de l'intent runtime). */
303
+ export type UseSessionOptions = SessionIntent;
304
+ /**
305
+ * Déclare qu'une route (décorateur de **méthode**) ou tout un contrôleur
306
+ * (décorateur de **classe**) a besoin d'une **session serveur**. C'est l'unique
307
+ * façon d'activer une session (avec la reprise auto d'un cookie existant — L1) :
308
+ * il n'y a plus de `sessionAutoStart` global « démarre partout » (le moteur du
309
+ * ×23). Lazy par défaut — aucune session pour une route qui n'en déclare pas.
310
+ *
311
+ * Précédence : `@UseSession` méthode > `@UseSession` classe. La simple présence
312
+ * d'un paramètre `@Session` sur l'action suffit aussi (intent implicite).
313
+ *
314
+ * - `{ readOnly }` : session lue/reprise mais **jamais persistée** (0 write storage).
315
+ *
316
+ * ⚠️ En décorateur de **classe**, placer `@UseSession` SOUS `@controller`.
317
+ *
318
+ * @example
319
+ * \@controller("/account")
320
+ * \@UseSession()
321
+ * class AccountController extends Controller {
322
+ * \@Get("/me") @UseSession({ readOnly: true }) me() {} // lecture seule → 0 write
323
+ * }
324
+ */
325
+ declare function UseSession(options?: UseSessionOptions): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
326
+ /**
327
+ * Résout l'intent de session effectif d'une action — lu par le `Resolver` au
328
+ * match, posé sur `context.sessionIntent`, consommé au point d'activation unique
329
+ * (`HttpKernel.startSession`). Combine `@UseSession` classe + méthode (méthode
330
+ * prioritaire) ; à défaut, un paramètre `@Session` déclare un intent implicite.
331
+ *
332
+ * @returns l'intent, ou `null` si la route ne requiert aucune session.
333
+ */
334
+ declare function resolveSessionIntent(ctor: ControllerConstructor, actionName: string): SessionIntent | null;
335
+ /**
336
+ * Exige une autorisation pour l'action (décorateur de **méthode**) ou tout le
337
+ * contrôleur (décorateur de **classe**).
338
+ *
339
+ * - `@IsGranted("ROLE_ADMIN")` — un attribut (rôle `ROLE_*`, permission, ou
340
+ * attribut métier résolu par un voter).
341
+ * - `@IsGranted(["ROLE_ADMIN", "ROLE_AUDITOR"])` — **OR** : un seul suffit.
342
+ * - empiler plusieurs `@IsGranted` — **AND** : toutes les clauses doivent passer.
343
+ * - `@IsGranted("doc.edit", { subject: "id" })` — le paramètre de route `id` est
344
+ * passé comme `subject` au voter (ownership, multi-tenant).
345
+ *
346
+ * Classe + méthode fusionnent en AND. L'évaluation a lieu dans le `Resolver`
347
+ * AVANT l'instanciation du controller (403 court-circuite — Zero Trust). N'écrit
348
+ * QUE des métadonnées (zéro logique sécu ici → 0 import `@nodefony/security`,
349
+ * 0 cycle ; le moteur `authorization` est appelé par nom au runtime).
350
+ */
351
+ declare function IsGranted(attribute: string | readonly string[], options?: {
352
+ subject?: string;
353
+ }): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
354
+ /**
355
+ * Déclare une action (méthode) ou un contrôleur (classe) **publique** : skip
356
+ * l'autorisation (override un `@IsGranted` de classe sur cette méthode) ET skip
357
+ * l'authentification (réutilise le mécanisme `@BypassFirewall` → pas de 401 en
358
+ * zone protégée). L'alias lisible de « permitAll » (mental model Spring). Pour un
359
+ * login, une sonde de liveness, une page publique d'un contrôleur par ailleurs
360
+ * protégé.
361
+ */
362
+ declare function Anonymous(): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
363
+ /**
364
+ * Exige un **scope** (`api:action`) pour l'action (décorateur de **méthode**) ou
365
+ * tout le contrôleur (décorateur de **classe**). Axe d'autorisation **distinct des
366
+ * rôles** (`@IsGranted`) : un scope **downscope** un jeton MACHINE délégué (clé
367
+ * API, JWT d'agent, OAuth) — il est un **no-op** pour une session humaine, dont
368
+ * les droits sont portés par ses rôles (cf {@link ScopeVoter}).
369
+ *
370
+ * - `@RequireScope("orders:read")` — le jeton doit porter ce scope.
371
+ * - `@RequireScope(["orders:read", "orders:admin"])` — **OR** : un seul suffit.
372
+ * - empiler plusieurs `@RequireScope` — **AND** : tous les scopes requis.
373
+ *
374
+ * Convention d'espace **plat** `api:action` (modèle GitHub PAT classic) : le
375
+ * préfixe avant `:` EST l'API → la découverte au boot regroupe les scopes par API
376
+ * sans catalogue séparé. Classe + méthode fusionnent en AND, et fusionnent AUSSI
377
+ * avec les clauses `@IsGranted` dans le même `SecurityRequirement` (rôle ET scope).
378
+ *
379
+ * N'écrit QUE des métadonnées (zéro logique sécu ici → 0 import `@nodefony/security`,
380
+ * 0 cycle) : la décision est rendue par le `ScopeVoter` au runtime (par nom). La
381
+ * metadata est **dédiée** (≠ `@IsGranted`) pour que la découverte au boot puisse
382
+ * lister les scopes déclarés par route sans les confondre avec les rôles.
383
+ */
384
+ declare function RequireScope(scope: string | readonly string[]): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
385
+ /**
386
+ * `@Csp({ "frame-src": [...] })` — déclare des directives CSP **additionnelles**
387
+ * pour l'action (méthode) ou tout le contrôleur (classe). Distinct de
388
+ * `registerCspOrigins` (besoins PERMANENTS d'un module, ex. Vite) : ici c'est le
389
+ * besoin ponctuel d'UNE réponse (embarquer une iframe YouTube, autoriser une CDN).
390
+ *
391
+ * Classe + méthode fusionnent additivement (sources concaténées par directive).
392
+ * Empiler plusieurs `@Csp` fusionne aussi. N'écrit QUE des métadonnées : le merge
393
+ * dans le CSP de la réponse est fait par le firewall, hors hot-path, UNIQUEMENT
394
+ * sur les routes décorées. Calque `@IsGranted` (0 import `@nodefony/security`).
395
+ */
396
+ declare function Csp(directives: CspDirectives): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
397
+ /**
398
+ * `@CsrfProtect()` — opt-IN à la défense CSRF **synchronizer token** (double-submit
399
+ * signé HMAC) EN PLUS de la défense globale Fetch Metadata/Origin (étape 1, toujours
400
+ * active). Pour les mutations à haute valeur (changement de mot de passe, virement) :
401
+ * une requête sûre vers la route SÈME le cookie lisible `csrf-token` ; la mutation
402
+ * DOIT rejouer ce token dans l'en-tête `x-csrf-token` (sinon 403). Classe = toutes
403
+ * les actions. N'écrit qu'un marqueur (0 import `@nodefony/security`, 0 cycle).
404
+ */
405
+ declare const CsrfProtect: () => (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
406
+ /**
407
+ * `@CsrfExempt()` — opt-OUT de la défense CSRF pour une route, **en conservant
408
+ * l'authentification et l'autorisation** (≠ `@Anonymous`/`@BypassFirewall` qui
409
+ * coupent l'auth). Pour un webhook ou une API recevant un POST cross-origin
410
+ * légitime, dont la requête est authentifiée autrement (signature HMAC du provider,
411
+ * clé API). Classe = toutes les actions. Marqueur seul (0 logique sécu ici).
412
+ */
413
+ declare const CsrfExempt: () => (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
414
+ /**
415
+ * `@Idempotent()` — protège une **mutation** (POST/PUT/PATCH/DELETE) d'un
416
+ * controller userland contre le double-effet d'un rejeu (double-clic, reconnexion
417
+ * socket, retry réseau), via une `Idempotency-Key` cliente (modèle Stripe, conforme
418
+ * `draft-ietf-httpapi-idempotency-key-header`). No-op sur les méthodes sûres (GET…).
419
+ *
420
+ * - **STRICT par défaut** : une mutation SANS clé est rejetée **400** (draft §2.7).
421
+ * - `@Idempotent({ required: false })` : mode **souple** — honore la clé si fournie,
422
+ * exécute sinon. (Une mutation par **socket** reste toujours strict : le WS rejoue.)
423
+ * - clé fournie → dédup complète : rejeu complété → réponse **mémorisée** ; rejeu
424
+ * concurrent → **409** ; même clé + payload différent → **422**.
425
+ *
426
+ * Décorateur de **méthode** (une action) ou de **classe** (toutes les mutations du
427
+ * controller). Précédence : méthode > classe (comme `@UseSession`). N'écrit QUE des
428
+ * métadonnées (0 import `@nodefony/security`, 0 cycle) ; la porte est appliquée par
429
+ * le `Resolver` (helper partagé `idempotency.ts`, le MÊME que le data plane admin),
430
+ * sur le `idempotencyStore` DI. Coût nul sur une route non décorée (`security: null`).
431
+ *
432
+ * @example
433
+ * \@controller("/api/orders")
434
+ * class OrderController extends Controller {
435
+ * \@Post("/") @Idempotent() create(@Body() dto: CreateOrder) { ... } // clé obligatoire
436
+ * \@Patch("/{id}") @Idempotent({ required: false }) update() { ... } // clé optionnelle (HTTP)
437
+ * }
438
+ */
439
+ declare function Idempotent(options?: {
440
+ required?: boolean;
441
+ }): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
442
+ declare const Param: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
443
+ declare const Query: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
444
+ /**
445
+ * Décorateur de paramètre `@Body` :
446
+ * - `@Body()` → body parsé entier · `@Body("field")` → un champ du body parsé.
447
+ * - `@Body({ stream: true })` → **flux brut** de la requête (`Readable`), sans
448
+ * parse en mémoire (P2.9 — gros uploads sans pic RAM ; le pipeline saute le
449
+ * parse busboy/JSON pour cette route).
450
+ *
451
+ * ## Le corps n'est PAS validé — le type écrit ici ne promet rien
452
+ *
453
+ * `@Body()` injecte le corps **tel qu'il a été parsé** : `@Body() dto: CreateOrder`
454
+ * compile, mais rien ne garantit qu'un `CreateOrder` soit arrivé. C'est un choix
455
+ * assumé, pas un oubli — valider ici ne garderait que la porte HTTP, et devrait
456
+ * rester **synchrone** ({@link resolveParamArg} l'est, et le rendre asynchrone
457
+ * taxerait toute requête à paramètres décorés).
458
+ *
459
+ * Où valider, donc :
460
+ *
461
+ * - **Une entité** → les hooks `beforeCreate` / `beforeUpdate` d'
462
+ * `AbstractCrudService` : ils sont `await`és — donc une règle asynchrone
463
+ * (unicité en base) y est possible — et ils gardent REST, WebSocket **et** la
464
+ * CLI d'un seul geste. C'est ce que génère `nodefony create entity`.
465
+ * - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
466
+ * modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
467
+ * devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
468
+ * quel message, quelle règle.
469
+ *
470
+ * Et typer le paramètre avec `z.infer<typeof createXSchema>` plutôt qu'avec
471
+ * `Partial<XRow>` : le premier décrit le contrat d'**entrée**, le second promet la
472
+ * ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * \@Post("/")
477
+ * async create(\@Body() payload: CreatePost) {
478
+ * return this.createResource(payload); // le service valide dans beforeCreate
479
+ * }
480
+ * ```
481
+ */
482
+ declare function Body(keyOrOptions?: string | {
483
+ stream?: boolean;
484
+ }): (target: object, propertyKey: string, parameterIndex: number) => void;
485
+ declare const Headers: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
486
+ declare const Cookie: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
487
+ declare const Session: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
488
+ /** `@CurrentUser() user: IUser` — injecte l'utilisateur de l'ALS (jamais le credential). */
489
+ declare const CurrentUser: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
490
+ declare const Req: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
491
+ declare const Res: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
492
+ declare const UploadedFile: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
493
+ declare const UploadedFiles: (key?: string) => (target: object, propertyKey: string, parameterIndex: number) => void;
494
+ /**
495
+ * Contexte structurel minimal nécessaire pour résoudre les arguments injectés
496
+ * par les décorateurs de paramètre. Volontairement découplé de `HttpContext`
497
+ * (typage par forme) → la résolution est une fonction pure, testable en unit
498
+ * avec un faux contexte, sans démarrer de serveur. Le `Resolver` lui passe le
499
+ * vrai `Context`, qui satisfait cette forme.
500
+ */
501
+ export interface IParamArgContext {
502
+ /** Variables de route extraites du path (`{name}` → valeur). */
503
+ paramsMap: Record<string, unknown>;
504
+ /**
505
+ * Query de l'invocation courante quand elle ne vient PAS de l'URL du
506
+ * transport — pont WS-RPC `api.request` (`Resolver.queryOverride`). Prime
507
+ * sur `request.queryGet` pour `@Query` uniquement (`@Req` reste le brut).
508
+ */
509
+ queryOverride?: Record<string, unknown>;
510
+ request?: {
511
+ queryGet?: Record<string, unknown>;
512
+ queryPost?: Record<string, unknown>;
513
+ queryFile?: unknown[];
514
+ headers?: Record<string, unknown>;
515
+ /**
516
+ * P2.9 — `IncomingMessage` brut (un `Readable`) sous-jacent au wrapper
517
+ * `HttpRequest`. Injecté tel quel par `@Body({ stream: true })` (le pipeline
518
+ * n'a pas consommé/parsé ce flux). `undefined` pour les contextes WS.
519
+ */
520
+ request?: NodeJS.ReadableStream;
521
+ } | null;
522
+ response?: unknown;
523
+ session?: {
524
+ get(key: string): unknown;
525
+ } | null;
526
+ getRequestCookies(name?: string): unknown;
527
+ }
528
+ /**
529
+ * Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
530
+ *
531
+ * @param meta - métadonnée posée par le décorateur (source + clé optionnelle)
532
+ * @param ctx - contexte de requête (forme structurelle minimale)
533
+ * @returns la valeur à injecter dans l'argument `meta.index` de l'action
534
+ */
535
+ declare function resolveParamArg(meta: ParamMeta, ctx: IParamArgContext): unknown;
536
+ /**
537
+ * Construit le tableau d'arguments d'une action à partir des métadonnées de
538
+ * paramètres décorés. Chaque valeur est placée à son `index` déclaré (les trous
539
+ * restent `undefined`). Fonction pure — aucun effet de bord, aucune I/O.
540
+ *
541
+ * @param metas - métadonnées de tous les paramètres décorés de l'action
542
+ * @param ctx - contexte de requête (forme structurelle minimale)
543
+ * @returns arguments positionnels à spread dans l'action
544
+ */
545
+ declare function buildParamArgs(metas: ParamMeta[], ctx: IParamArgContext): unknown[];
546
+ /**
547
+ * P2.9 — Indique si l'action d'une route attend le **flux brut** du body
548
+ * (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
549
+ * `route.bodyStream` : lecture `Reflect` au 1er appel, O(1) ensuite → 0 coût
550
+ * hot-path. Lu **en amont** par `handleHttp` (avant le parse) pour décider de
551
+ * sauter le parse busboy/JSON. Typage structurel (pas d'import `Route` → 0 cycle).
552
+ *
553
+ * @param routeDef - route résolue (porte `controller` + `classMethod` à `onBoot`).
554
+ * @returns `true` si l'action déclare un `@Body({ stream:true })`.
555
+ */
556
+ declare function routeExpectsBodyStream(routeDef: {
557
+ controller?: {
558
+ prototype: object;
559
+ } | null;
560
+ classMethod?: string;
561
+ bodyStream?: boolean;
562
+ }): boolean;
563
+ /**
564
+ * Snapshot des métadonnées d'action d'une route (`@HttpCode`/`@Header`/
565
+ * `@Redirect`/paramètres décorés/intent de session), résolu UNE fois par route
566
+ * puis figé sur `route.actionMeta` (cf {@link resolveActionMeta}). Objet
567
+ * **PARTAGÉ entre toutes les requêtes** de la route — ne jamais muter.
568
+ */
569
+ export interface RouteActionMeta {
570
+ /** Paramètres décorés (`@Param`/`@Body`/`@Query`…) — `null` si aucun. */
571
+ paramsMeta: ParamMeta[] | null;
572
+ /** `@Redirect` de l'action — `null` si absent. */
573
+ redirectMeta: RedirectMeta | null;
574
+ /** `@HttpCode` de l'action — `null` si absent. */
575
+ httpCode: number | null;
576
+ /** Entrées `@Header` pré-dépliées (`Object.entries` fait 1×) — `null` si aucune. */
577
+ headerEntries: [string, string][] | null;
578
+ /** Intent `@UseSession`/`@Session` — `null` si la route n'en déclare pas. */
579
+ sessionIntent: SessionIntent | null;
580
+ /**
581
+ * Exigence d'autorisation (`@IsGranted`, fusion classe+méthode) — `null` si
582
+ * l'action n'est pas gardée (ou `@Anonymous`). `null` = **0 coût** sur le hot
583
+ * path (ni résolution de service, ni `decide`, ni await). Frozen, partagé.
584
+ */
585
+ security: SecurityRequirement | null;
586
+ /**
587
+ * Directives CSP additionnelles (`@Csp`, fusion classe+méthode) — `null` si
588
+ * l'action n'en déclare pas (cas courant → 0 composition CSP). Frozen, partagé.
589
+ */
590
+ cspDirectives: CspDirectives | null;
591
+ /** `@CsrfProtect` (classe ou méthode) → exige le synchronizer token sur la mutation. */
592
+ csrfProtect: boolean;
593
+ /** `@CsrfExempt` (classe ou méthode) → la route est hors défense CSRF (auth conservée). */
594
+ csrfExempt: boolean;
595
+ /**
596
+ * Idempotence (`@Idempotent`, fusion classe + méthode) — `null` si l'action
597
+ * n'est pas décorée (cas courant → **0 coût** sur le hot path : ni résolution de
598
+ * store, ni `begin`). Frozen, partagé entre requêtes.
599
+ */
600
+ idempotent: IdempotentMeta | null;
601
+ }
602
+ /**
603
+ * P6.8 — Liste PLATE des scopes `api:action` déclarés par une action
604
+ * (`@RequireScope`, classe + méthode), **dédupliqués**. Source de la **découverte
605
+ * au boot** : le catalogue de scopes du formulaire de clés API se construit en
606
+ * scannant les routes (cf `collectDeclaredApiScopes`), au lieu d'une config plate
607
+ * qui dérive du code. Lecture `Reflect` directe — **cold path** (introspection à la
608
+ * demande, jamais sur le hot path requête). `[]` si l'action ne déclare aucun scope.
609
+ */
610
+ declare function extractActionScopes(ctor: {
611
+ prototype: object;
612
+ }, method: string): string[];
613
+ declare function computeActionMeta(ctor?: {
614
+ prototype: object;
615
+ } | null, method?: string): RouteActionMeta;
616
+ /**
617
+ * P5 — Metadata d'action d'une route, **mémoïsées au 1er hit** sur
618
+ * `route.actionMeta` (pattern frère de {@link routeExpectsBodyStream}) :
619
+ * `undefined` = pas encore résolu → 1 lecture `Reflect` par route pour la vie
620
+ * du process, O(1) ensuite. Sort `Reflect.getMetadata` (~6 appels/req) du hot
621
+ * path `match`/`executeAction`. Posé APRÈS `generateId()` (1ʳᵉ requête) → le
622
+ * hash de route et l'introspection Studio restent stables. Typage structurel
623
+ * (pas d'import `Route` → 0 cycle).
624
+ */
625
+ declare function resolveActionMeta(routeDef: {
626
+ controller?: {
627
+ prototype: object;
628
+ } | null;
629
+ classMethod?: string;
630
+ actionMeta?: RouteActionMeta;
631
+ }): RouteActionMeta;
632
+ export { route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, 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, };