@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,967 @@
1
+ import Controller from "../src/Controller.js";
2
+ import router_default from "../service/router.js";
3
+ import { RequestContext } from "nodefony";
4
+ import "reflect-metadata";
5
+ //#region nodefony/decorators/routerDecorators.ts
6
+ const metadataKey = "routes:definitions";
7
+ /**
8
+ * Noms déjà portés par `Controller` — méthodes ET accesseurs, les siens comme
9
+ * ceux hérités de `Service`. Une action qui en reprend un est refusée à la
10
+ * déclaration (cf `assertActionNameFree`).
11
+ *
12
+ * Résolu PARESSEUSEMENT, et une seule fois : `Controller` participe à un cycle
13
+ * d'import (`Controller` → `Router` → ce module), le lire au chargement du
14
+ * module tomberait dans sa zone morte temporelle. Au premier décorateur d'un
15
+ * controller userland, la classe de base est forcément déjà initialisée.
16
+ *
17
+ * @returns Map `nom → classe qui le porte` (la plus DÉRIVÉE des deux gagne).
18
+ */
19
+ let reservedActionNames = null;
20
+ const getReservedActionNames = () => {
21
+ if (reservedActionNames) return reservedActionNames;
22
+ const reserved = /* @__PURE__ */ new Map();
23
+ let proto = Controller.prototype;
24
+ while (proto && proto !== Object.prototype) {
25
+ const owner = proto.constructor?.name ?? "Controller";
26
+ for (const key of Object.getOwnPropertyNames(proto)) if (key !== "constructor" && !reserved.has(key)) reserved.set(key, owner);
27
+ proto = Object.getPrototypeOf(proto);
28
+ }
29
+ reservedActionNames = reserved;
30
+ return reserved;
31
+ };
32
+ /**
33
+ * Refuse, AU MOMENT DE LA DÉCLARATION, une action dont le nom est déjà celui
34
+ * d'un membre de `Controller`.
35
+ *
36
+ * Le langage sanctionne déjà ce conflit, mais tard et sans le nommer : un
37
+ * `remove()` de controller produit un `TS2416` sur une incompatibilité de
38
+ * signature (`Service.remove(name): boolean`), et un `session()` ne produit
39
+ * rien du tout — l'accesseur de la classe de base masque simplement l'action à
40
+ * l'instanciation. La règle posée ici est volontairement plus stricte que
41
+ * TypeScript (elle refuse le nom, sans regarder les signatures) : une règle
42
+ * qu'on peut énoncer en une phrase vaut mieux qu'une règle exacte que
43
+ * personne ne peut anticiper.
44
+ *
45
+ * @param target - Le prototype de la classe qui déclare l'action.
46
+ * @param propertyKey - Le nom de la méthode décorée.
47
+ * @throws Error nommant le membre en conflit et la sortie (renommer l'action).
48
+ */
49
+ function assertActionNameFree(target, propertyKey) {
50
+ if (!Object.prototype.isPrototypeOf.call(Controller.prototype, target)) return;
51
+ const owner = getReservedActionNames().get(propertyKey);
52
+ if (!owner) return;
53
+ const className = target.constructor?.name ?? "(anonyme)";
54
+ throw new Error(`Action « ${propertyKey} » de ${className} : ce nom est RÉSERVÉ par le framework — ${owner} porte déjà un membre « ${propertyKey} », dont tout controller hérite. Le conflit casse la compilation (TS2416) ou masque silencieusement l'action. Renommez la méthode (« ${propertyKey}Action », « ${propertyKey}One »…) : l'URL vient du décorateur, pas du nom de la méthode — seul le nom généré de la route suit (« ${className}::${propertyKey} »).`);
55
+ }
56
+ /**
57
+ * Rattache un ou plusieurs contrôleurs à un module, dont ils suivent le cycle de vie.
58
+ *
59
+ * Décorateur de **classe de module**. L'enregistrement n'a pas lieu à
60
+ * l'évaluation du décorateur mais au hook `onBoot` du kernel : tant que le boot
61
+ * n'a pas eu lieu, les routes déclarées par `@route` sur ces classes n'existent
62
+ * pas encore dans le routeur — un test qui interroge le routeur sans booter ne
63
+ * verra rien. L'enregistrement est tagué au nom du module, de sorte qu'un échec
64
+ * désigne le module fautif au lieu d'un contrôleur anonyme.
65
+ *
66
+ * @param controller - Un contrôleur, ou un tableau de contrôleurs, à rattacher.
67
+ * @returns Le décorateur de classe, qui renvoie le module enrichi du hook.
68
+ * @example
69
+ * ```typescript
70
+ * @controllers([DefaultController, RestController])
71
+ * class TestModule extends Module {}
72
+ * ```
73
+ */
74
+ function controllers(targets) {
75
+ return function(constructor) {
76
+ class NewConstructorControllers extends constructor {
77
+ constructor(...args) {
78
+ super(...args);
79
+ this.hookKernel("onBoot", async () => {
80
+ return this.initDecoratorControllers();
81
+ });
82
+ }
83
+ async initDecoratorControllers() {
84
+ const log = (contr) => {
85
+ router_default.setController(contr, this);
86
+ this.log(`ADD CONTROLLER : ${contr.name}`, "DEBUG");
87
+ const declared = router_default.getRoutesForController(contr);
88
+ if (this.kernel?.debug) for (const r of declared) this.log(`route + ${r.toLogLine()}`, "DEBUG");
89
+ for (const r of declared) if (r.unreachableChars) this.log(`route INATTEIGNABLE : ${r.name} — le chemin « ${r.path} » contient ${r.unreachableChars.map((c) => `« ${c} »`).join(", ")}, qu'une requête ne porte jamais (encodé par l'analyseur d'URL, ou pris pour un délimiteur). Cette route ne répondra à rien.`, "WARNING");
90
+ };
91
+ if (Array.isArray(targets)) for (const contr of targets) log(contr);
92
+ else log(targets);
93
+ }
94
+ }
95
+ return NewConstructorControllers;
96
+ };
97
+ }
98
+ /**
99
+ * Declaration Controller
100
+ *
101
+ * @param prefix - prefixage du router du controller.
102
+ * @param options - Les options .
103
+ * @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
104
+ *
105
+ * @example
106
+ * \@controller("/openapi")
107
+ * \@UseSession()
108
+ * class OpenApiController extends Controller {
109
+ * constructor(context: Context) {
110
+ * super("OpenApiController", context);
111
+ * }
112
+ * \@route("index-openapi", { path: "" })
113
+ * index() {
114
+ * this.render({});
115
+ * }
116
+ * }
117
+ */
118
+ function controller(prefix) {
119
+ return function(mycontroller) {
120
+ mycontroller.prefix = prefix;
121
+ const metadata = Reflect.getMetadata(metadataKey, mycontroller) || {};
122
+ if (metadata && Object.keys(metadata).length !== 0) {
123
+ let hasMagic = false;
124
+ for (const name in metadata) {
125
+ const options = metadata[name];
126
+ options.prefix = prefix;
127
+ if (options.host === void 0) {
128
+ const methodDomain = Reflect.getMetadata(DOMAIN_METHOD_METADATA, mycontroller, options.classMethod);
129
+ const classDomain = Reflect.getMetadata(DOMAIN_CLASS_METADATA, mycontroller);
130
+ const domain = methodDomain ?? classDomain;
131
+ if (domain) options.host = domain;
132
+ }
133
+ if (options.bypassFirewall !== true) {
134
+ const methodBypass = Reflect.getMetadata(BYPASS_FIREWALL_METHOD_METADATA, mycontroller, options.classMethod);
135
+ const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
136
+ if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
137
+ }
138
+ if (options.path == "*") {
139
+ hasMagic = {
140
+ options,
141
+ name
142
+ };
143
+ continue;
144
+ }
145
+ router_default.createRoute(name, options);
146
+ }
147
+ if (hasMagic) router_default.createRoute(hasMagic.name, hasMagic.options);
148
+ }
149
+ Reflect.deleteMetadata(metadataKey, mycontroller);
150
+ return mycontroller;
151
+ };
152
+ }
153
+ /**
154
+ * Crée une route avec le nom et les options spécifiés.
155
+ *
156
+ * @param name - Le nom de la route.
157
+ * @param options - Les options de la route.
158
+ * @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
159
+ *
160
+ * @example
161
+ * \@route("myroute", {
162
+ * path: "/add/{name}",
163
+ * method: ["GET", "POST"],
164
+ * defaults: { name: "john" },
165
+ * })
166
+ * method(name: string) {
167
+ * return this.renderJson({ name });
168
+ * }
169
+ */
170
+ function route(name, options) {
171
+ return function(target, propertyKey, descriptor) {
172
+ assertActionNameFree(target, propertyKey);
173
+ const className = target.constructor.name;
174
+ const classMethod = propertyKey;
175
+ const prefix = options.prefix || null;
176
+ const path = options.path || "";
177
+ let filePath;
178
+ try {
179
+ const stackTrace = (/* @__PURE__ */ new Error()).stack?.split("\n").slice(2);
180
+ if (!stackTrace) throw new Error("Erreur lors de l'obtention de la pile d'appels.");
181
+ const controllerFilePath = extractControllerFilePath(stackTrace);
182
+ if (!controllerFilePath) throw new Error("Fichier de contrôleur non trouvé dans la pile d'appels.");
183
+ filePath = controllerFilePath;
184
+ } catch (error) {
185
+ filePath = error;
186
+ }
187
+ const metadata = Reflect.getMetadata(metadataKey, target.constructor) || {};
188
+ metadata[name] = {
189
+ path,
190
+ filePath,
191
+ constructor: target.constructor,
192
+ prefix,
193
+ className,
194
+ classMethod,
195
+ method: options.method,
196
+ host: options.host,
197
+ defaults: options.defaults,
198
+ requirements: options.requirements,
199
+ bypassFirewall: options.bypassFirewall
200
+ };
201
+ Reflect.defineMetadata(metadataKey, metadata, target.constructor);
202
+ return descriptor;
203
+ };
204
+ }
205
+ function extractControllerFilePath(stackTrace) {
206
+ for (const line of stackTrace) {
207
+ const match = line.match(/\s+at file:\/\/(.*\/controllers?\/.*\.js)/);
208
+ if (match && match[1]) return match[1];
209
+ }
210
+ }
211
+ const HTTP_CODE_METADATA = "route:httpCode";
212
+ const HEADERS_METADATA = "route:responseHeaders";
213
+ const REDIRECT_METADATA = "route:redirect";
214
+ const PARAM_ARGS_METADATA = "route:paramArgs";
215
+ const DOMAIN_CLASS_METADATA = "route:domainClass";
216
+ const DOMAIN_METHOD_METADATA = "route:domainMethod";
217
+ const BYPASS_FIREWALL_CLASS_METADATA = "route:bypassFirewallClass";
218
+ const BYPASS_FIREWALL_METHOD_METADATA = "route:bypassFirewallMethod";
219
+ const SECURITY_CLAUSES_METADATA = "nodefony:security:clauses";
220
+ const SECURITY_ANONYMOUS_METADATA = "nodefony:security:anonymous";
221
+ const SECURITY_SCOPES_METADATA = "nodefony:security:scopes";
222
+ const CSP_DIRECTIVES_METADATA = "nodefony:csp:directives";
223
+ const CSRF_PROTECT_METADATA = "nodefony:csrf:protect";
224
+ const CSRF_EXEMPT_METADATA = "nodefony:csrf:exempt";
225
+ const IDEMPOTENT_METADATA = "nodefony:idempotent";
226
+ const USE_SESSION_CLASS_METADATA = "session:useClass";
227
+ const USE_SESSION_METHOD_METADATA = "session:useMethod";
228
+ function httpMethodDecorator(methods) {
229
+ return function(path = "", options = {}) {
230
+ return function(target, propertyKey, descriptor) {
231
+ const name = `${target.constructor.name}::${propertyKey}`;
232
+ const declaredMethods = options.requirements?.methods;
233
+ const added = declaredMethods === void 0 ? [] : Array.isArray(declaredMethods) ? declaredMethods : [declaredMethods];
234
+ const fusion = added.length > 0 ? [.../* @__PURE__ */ new Set([...methods, ...added])] : methods;
235
+ return route(name, {
236
+ ...options,
237
+ path,
238
+ requirements: {
239
+ ...options.requirements,
240
+ methods: fusion
241
+ }
242
+ })(target, propertyKey, descriptor);
243
+ };
244
+ };
245
+ }
246
+ const Get = httpMethodDecorator(["GET"]);
247
+ const Post = httpMethodDecorator(["POST"]);
248
+ const Put = httpMethodDecorator(["PUT"]);
249
+ const Delete = httpMethodDecorator(["DELETE"]);
250
+ const Patch = httpMethodDecorator(["PATCH"]);
251
+ const Options = httpMethodDecorator(["OPTIONS"]);
252
+ const Head = httpMethodDecorator(["HEAD"]);
253
+ /**
254
+ * `@All` — route sans restriction de méthode : matche **toutes** les méthodes
255
+ * HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
256
+ * `Route.matchRequirements` ne lève jamais 405 sur la méthode.
257
+ */
258
+ function All(path = "", options = {}) {
259
+ return function(target, propertyKey, descriptor) {
260
+ return route(`${target.constructor.name}::${propertyKey}`, {
261
+ ...options,
262
+ path
263
+ })(target, propertyKey, descriptor);
264
+ };
265
+ }
266
+ /**
267
+ * Fixe le code de statut HTTP de la réponse d'une action.
268
+ *
269
+ * Décorateur de **méthode**. Le code est posé sur la réponse **avant** que le
270
+ * corps de l'action ne s'exécute (`Resolver._applyResponseMeta`) : l'action
271
+ * garde donc le dernier mot et peut encore le remplacer. Emploie-le pour le
272
+ * statut nominal d'une action — un 201 sur une création — et non pour un statut
273
+ * qui dépend du résultat. La métadonnée n'est lue qu'une fois par route, puis
274
+ * mémorisée : le décorateur ne coûte rien par requête.
275
+ *
276
+ * @param statusCode - Code HTTP appliqué à la réponse (201, 204, 202…).
277
+ * @returns Le décorateur de méthode.
278
+ * @example
279
+ * ```typescript
280
+ * @route("item-create", { path: "/items", method: "POST" })
281
+ * @HttpCode(201)
282
+ * async create() {
283
+ * return this.renderJson({ id: 42 });
284
+ * }
285
+ * ```
286
+ */
287
+ function HttpCode(statusCode) {
288
+ return function(target, propertyKey, descriptor) {
289
+ Reflect.defineMetadata(HTTP_CODE_METADATA, statusCode, target, propertyKey);
290
+ return descriptor;
291
+ };
292
+ }
293
+ /**
294
+ * Ajoute un en-tête à la réponse d'une action.
295
+ *
296
+ * Décorateur de **méthode**, empilable : chaque application ajoute une entrée,
297
+ * la dernière l'emportant sur un même nom d'en-tête. Les en-têtes sont posés
298
+ * avant l'exécution du corps de l'action, qui peut donc encore les modifier.
299
+ * Réserve-le aux en-têtes constants d'une action ; ce qui dépend de la requête
300
+ * s'écrit dans le corps.
301
+ *
302
+ * @param key - Nom de l'en-tête.
303
+ * @param value - Valeur de l'en-tête.
304
+ * @returns Le décorateur de méthode.
305
+ * @example
306
+ * ```typescript
307
+ * @route("feed", { path: "/feed", method: "GET" })
308
+ * @Header("Cache-Control", "public, max-age=3600")
309
+ * async feed() {
310
+ * return this.renderJson(items);
311
+ * }
312
+ * ```
313
+ */
314
+ function Header(key, value) {
315
+ return function(target, propertyKey, descriptor) {
316
+ const existing = Reflect.getMetadata("route:responseHeaders", target, propertyKey) || {};
317
+ existing[key] = value;
318
+ Reflect.defineMetadata(HEADERS_METADATA, existing, target, propertyKey);
319
+ return descriptor;
320
+ };
321
+ }
322
+ /**
323
+ * Redirige la réponse d'une action vers une autre URL.
324
+ *
325
+ * Décorateur de **méthode**. ⚠️ Le corps de l'action **est exécuté** : la
326
+ * redirection est portée à côté du résultat et appliquée après coup par le
327
+ * `Resolver`. Ce n'est donc pas un court-circuit — tout effet de bord écrit dans
328
+ * l'action a bien lieu. L'action peut d'ailleurs surcharger la cible ou le code
329
+ * en renvoyant sa propre redirection.
330
+ *
331
+ * @param url - URL cible, absolue ou relative à l'application.
332
+ * @param statusCode - Code HTTP de redirection. Défaut `302` (temporaire) ;
333
+ * `301` pour un déplacement permanent, `307` pour conserver la méthode.
334
+ * @returns Le décorateur de méthode.
335
+ * @example
336
+ * ```typescript
337
+ * @route("legacy", { path: "/old-path", method: "GET" })
338
+ * @Redirect("/new-path", 301)
339
+ * async oldPath() {}
340
+ * ```
341
+ */
342
+ function Redirect(url, statusCode = 302) {
343
+ return function(target, propertyKey, descriptor) {
344
+ const meta = {
345
+ url,
346
+ statusCode
347
+ };
348
+ Reflect.defineMetadata(REDIRECT_METADATA, meta, target, propertyKey);
349
+ return descriptor;
350
+ };
351
+ }
352
+ /**
353
+ * Restreint une route (décorateur de **méthode**) ou tout un contrôleur
354
+ * (décorateur de **classe**) à un ou plusieurs vhosts. Source de vérité du
355
+ * domaine de routing — le `host` posé ici alimente `Route.host`, compilé en
356
+ * RegExp ancrée/wildcard (matcher partagé `@nodefony/http`). Domaine non servi
357
+ * par la route → 403.
358
+ *
359
+ * Précédence : `@route({ host })` > `@Domain` méthode > `@Domain` classe.
360
+ *
361
+ * Pattern : exact (`"marseille.fr"`) ou wildcard un-label (`"*.cdn.nodefony.com"`).
362
+ *
363
+ * ⚠️ En décorateur de **classe**, placer `@Domain` SOUS `@controller` : les
364
+ * décorateurs de classe s'appliquent de bas en haut, et `@controller` construit
365
+ * les routes — il doit voir le domaine de classe déjà posé.
366
+ *
367
+ * @example
368
+ * \@controller("/")
369
+ * \@Domain("marseille.fr")
370
+ * class MarseilleController extends Controller {
371
+ * \@Get("/") home() {} // marseille.fr/ → OK ; nodefony.com/ → 403
372
+ * }
373
+ */
374
+ function Domain(patterns) {
375
+ const list = Array.isArray(patterns) ? patterns : [patterns];
376
+ return function(target, propertyKey, descriptor) {
377
+ if (propertyKey === void 0) {
378
+ Reflect.defineMetadata(DOMAIN_CLASS_METADATA, list, target);
379
+ return target;
380
+ }
381
+ Reflect.defineMetadata(DOMAIN_METHOD_METADATA, list, target.constructor, propertyKey);
382
+ return descriptor;
383
+ };
384
+ }
385
+ /**
386
+ * Déclare une route (décorateur de **méthode**) ou tout un contrôleur
387
+ * (décorateur de **classe**) comme **PUBLIQUE** : le firewall ne s'exécute pas
388
+ * (`Route.bypassFirewall`). Pour la **liveness** (`/health`, `/info` — sondes
389
+ * k8s/monitoring NON authentifiées, ping pré-login), les **webhooks signés**, ou
390
+ * un endpoint d'auth (login). Sucre déclaratif sur l'option
391
+ * `RouteOptions.bypassFirewall` (les deux coexistent ; l'option l'emporte).
392
+ *
393
+ * Précédence : `@Get({ bypassFirewall })` > `@BypassFirewall` méthode > classe.
394
+ * Lu au montage `@controller` (ordre des décorateurs indifférent). **Fail-closed** :
395
+ * un oubli laisse la route GATÉE (401), jamais ouverte par erreur.
396
+ *
397
+ * ⚠️ En décorateur de **classe**, placer `@BypassFirewall` SOUS `@controller`
398
+ * (décorateurs de classe appliqués de bas en haut). Préfigure `@Public`/
399
+ * `@Anonymous` (P6.8b) — sémantique « pas d'auth », qui s'appuiera sur ce primitif.
400
+ *
401
+ * Décorateur SANS argument → **simple, SANS parenthèses** (`@BypassFirewall`,
402
+ * pas `@BypassFirewall()`) : c'est un DRAPEAU, pas une option paramétrée. Une
403
+ * factory (`()`) ne se justifie que pour passer des arguments (cf `@Get("/x")`,
404
+ * `@Domain("host")`).
405
+ *
406
+ * @example
407
+ * \@controller("/nodefony")
408
+ * class StudioController extends Controller {
409
+ * \@BypassFirewall
410
+ * \@Get("/studio/api/health") health() {} // public (liveness)
411
+ * \@Get("/studio/api/stats") stats() {} // gaté par l'aire data plane
412
+ * }
413
+ */
414
+ function BypassFirewall(target, propertyKey, descriptor) {
415
+ if (propertyKey === void 0) {
416
+ Reflect.defineMetadata(BYPASS_FIREWALL_CLASS_METADATA, true, target);
417
+ return target;
418
+ }
419
+ Reflect.defineMetadata(BYPASS_FIREWALL_METHOD_METADATA, true, target.constructor, propertyKey);
420
+ return descriptor;
421
+ }
422
+ /**
423
+ * Déclare le scope d'instanciation d'un controller (V4.3) — pose le statique
424
+ * `scope` de la classe (hérité de `Controller`, défaut `"request"`). Lu par le
425
+ * constructor de `Controller` (`new.target`) et par le `Resolver` : 0 Reflect.
426
+ *
427
+ * `@Scope("singleton")` : UNE instance partagée par toutes les requêtes
428
+ * (cache kernel-scoped sur le Router, `initialize()` appelé 1× à la création).
429
+ * **Contrat stateless strict** : l'action ne lit/n'écrit AUCUN état par requête
430
+ * sur `this` — tout passe par les arguments décorés (`@Param`/`@Body`…) et les
431
+ * helpers, qui retrouvent la requête courante via l'ALS (V4.1). Un champ muté
432
+ * par requête sur un singleton = data race silencieuse entre deux requêtes
433
+ * concurrentes. Le défaut per-request reste inchangé (0 breaking legacy).
434
+ *
435
+ * ⚠️ Homonyme : le core `nodefony` exporte aussi `Scope` (le scope DI du
436
+ * `Container`) — celui-ci s'importe depuis `@nodefony/framework`.
437
+ *
438
+ * @example
439
+ * \@Scope("singleton")
440
+ * \@controller("/api/books")
441
+ * class BookController extends ResourceController { ... }
442
+ */
443
+ function Scope(scope) {
444
+ return function(target) {
445
+ target.scope = scope;
446
+ };
447
+ }
448
+ /**
449
+ * Déclare qu'une route (décorateur de **méthode**) ou tout un contrôleur
450
+ * (décorateur de **classe**) a besoin d'une **session serveur**. C'est l'unique
451
+ * façon d'activer une session (avec la reprise auto d'un cookie existant — L1) :
452
+ * il n'y a plus de `sessionAutoStart` global « démarre partout » (le moteur du
453
+ * ×23). Lazy par défaut — aucune session pour une route qui n'en déclare pas.
454
+ *
455
+ * Précédence : `@UseSession` méthode > `@UseSession` classe. La simple présence
456
+ * d'un paramètre `@Session` sur l'action suffit aussi (intent implicite).
457
+ *
458
+ * - `{ readOnly }` : session lue/reprise mais **jamais persistée** (0 write storage).
459
+ *
460
+ * ⚠️ En décorateur de **classe**, placer `@UseSession` SOUS `@controller`.
461
+ *
462
+ * @example
463
+ * \@controller("/account")
464
+ * \@UseSession()
465
+ * class AccountController extends Controller {
466
+ * \@Get("/me") @UseSession({ readOnly: true }) me() {} // lecture seule → 0 write
467
+ * }
468
+ */
469
+ function UseSession(options = {}) {
470
+ return function(target, propertyKey, descriptor) {
471
+ if (propertyKey === void 0) {
472
+ Reflect.defineMetadata(USE_SESSION_CLASS_METADATA, options, target);
473
+ return target;
474
+ }
475
+ Reflect.defineMetadata(USE_SESSION_METHOD_METADATA, options, target.constructor, propertyKey);
476
+ return descriptor;
477
+ };
478
+ }
479
+ /**
480
+ * Résout l'intent de session effectif d'une action — lu par le `Resolver` au
481
+ * match, posé sur `context.sessionIntent`, consommé au point d'activation unique
482
+ * (`HttpKernel.startSession`). Combine `@UseSession` classe + méthode (méthode
483
+ * prioritaire) ; à défaut, un paramètre `@Session` déclare un intent implicite.
484
+ *
485
+ * @returns l'intent, ou `null` si la route ne requiert aucune session.
486
+ */
487
+ function resolveSessionIntent(ctor, actionName) {
488
+ const classMeta = Reflect.getMetadata(USE_SESSION_CLASS_METADATA, ctor);
489
+ const methodMeta = Reflect.getMetadata(USE_SESSION_METHOD_METADATA, ctor, actionName);
490
+ if (classMeta || methodMeta) return {
491
+ ...classMeta,
492
+ ...methodMeta
493
+ };
494
+ if (Reflect.getMetadata("route:paramArgs", ctor.prototype, actionName)?.some((p) => p.source === "session")) return {};
495
+ return null;
496
+ }
497
+ /**
498
+ * Exige une autorisation pour l'action (décorateur de **méthode**) ou tout le
499
+ * contrôleur (décorateur de **classe**).
500
+ *
501
+ * - `@IsGranted("ROLE_ADMIN")` — un attribut (rôle `ROLE_*`, permission, ou
502
+ * attribut métier résolu par un voter).
503
+ * - `@IsGranted(["ROLE_ADMIN", "ROLE_AUDITOR"])` — **OR** : un seul suffit.
504
+ * - empiler plusieurs `@IsGranted` — **AND** : toutes les clauses doivent passer.
505
+ * - `@IsGranted("doc.edit", { subject: "id" })` — le paramètre de route `id` est
506
+ * passé comme `subject` au voter (ownership, multi-tenant).
507
+ *
508
+ * Classe + méthode fusionnent en AND. L'évaluation a lieu dans le `Resolver`
509
+ * AVANT l'instanciation du controller (403 court-circuite — Zero Trust). N'écrit
510
+ * QUE des métadonnées (zéro logique sécu ici → 0 import `@nodefony/security`,
511
+ * 0 cycle ; le moteur `authorization` est appelé par nom au runtime).
512
+ */
513
+ function IsGranted(attribute, options) {
514
+ const clause = {
515
+ anyOf: Array.isArray(attribute) ? [...attribute] : [attribute],
516
+ ...options?.subject !== void 0 ? { subjectParam: options.subject } : {}
517
+ };
518
+ return function(target, propertyKey, descriptor) {
519
+ if (propertyKey === void 0) {
520
+ const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) || [];
521
+ existing.push(clause);
522
+ Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target);
523
+ return target;
524
+ }
525
+ const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey) || [];
526
+ existing.push(clause);
527
+ Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target, propertyKey);
528
+ return descriptor;
529
+ };
530
+ }
531
+ /**
532
+ * Déclare une action (méthode) ou un contrôleur (classe) **publique** : skip
533
+ * l'autorisation (override un `@IsGranted` de classe sur cette méthode) ET skip
534
+ * l'authentification (réutilise le mécanisme `@BypassFirewall` → pas de 401 en
535
+ * zone protégée). L'alias lisible de « permitAll » (mental model Spring). Pour un
536
+ * login, une sonde de liveness, une page publique d'un contrôleur par ailleurs
537
+ * protégé.
538
+ */
539
+ function Anonymous() {
540
+ return function(target, propertyKey, descriptor) {
541
+ if (propertyKey === void 0) {
542
+ Reflect.defineMetadata(SECURITY_ANONYMOUS_METADATA, true, target);
543
+ Reflect.defineMetadata(BYPASS_FIREWALL_CLASS_METADATA, true, target);
544
+ return target;
545
+ }
546
+ Reflect.defineMetadata(SECURITY_ANONYMOUS_METADATA, true, target.constructor, propertyKey);
547
+ Reflect.defineMetadata(BYPASS_FIREWALL_METHOD_METADATA, true, target.constructor, propertyKey);
548
+ return descriptor;
549
+ };
550
+ }
551
+ /**
552
+ * Exige un **scope** (`api:action`) pour l'action (décorateur de **méthode**) ou
553
+ * tout le contrôleur (décorateur de **classe**). Axe d'autorisation **distinct des
554
+ * rôles** (`@IsGranted`) : un scope **downscope** un jeton MACHINE délégué (clé
555
+ * API, JWT d'agent, OAuth) — il est un **no-op** pour une session humaine, dont
556
+ * les droits sont portés par ses rôles (cf {@link ScopeVoter}).
557
+ *
558
+ * - `@RequireScope("orders:read")` — le jeton doit porter ce scope.
559
+ * - `@RequireScope(["orders:read", "orders:admin"])` — **OR** : un seul suffit.
560
+ * - empiler plusieurs `@RequireScope` — **AND** : tous les scopes requis.
561
+ *
562
+ * Convention d'espace **plat** `api:action` (modèle GitHub PAT classic) : le
563
+ * préfixe avant `:` EST l'API → la découverte au boot regroupe les scopes par API
564
+ * sans catalogue séparé. Classe + méthode fusionnent en AND, et fusionnent AUSSI
565
+ * avec les clauses `@IsGranted` dans le même `SecurityRequirement` (rôle ET scope).
566
+ *
567
+ * N'écrit QUE des métadonnées (zéro logique sécu ici → 0 import `@nodefony/security`,
568
+ * 0 cycle) : la décision est rendue par le `ScopeVoter` au runtime (par nom). La
569
+ * metadata est **dédiée** (≠ `@IsGranted`) pour que la découverte au boot puisse
570
+ * lister les scopes déclarés par route sans les confondre avec les rôles.
571
+ */
572
+ function RequireScope(scope) {
573
+ const clause = { anyOf: Array.isArray(scope) ? [...scope] : [scope] };
574
+ return function(target, propertyKey, descriptor) {
575
+ if (propertyKey === void 0) {
576
+ const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) || [];
577
+ existing.push(clause);
578
+ Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target);
579
+ return target;
580
+ }
581
+ const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey) || [];
582
+ existing.push(clause);
583
+ Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target, propertyKey);
584
+ return descriptor;
585
+ };
586
+ }
587
+ /**
588
+ * Fusion ADDITIVE de deux jeux de directives CSP : les sources d'une même
589
+ * directive sont concaténées (dédupliquées, ordre `a` puis `b`). Pure, sans
590
+ * mutation des entrées. Sert au stacking de `@Csp` et à la fusion classe+méthode.
591
+ */
592
+ function mergeCspDirectives(a, b) {
593
+ const out = {};
594
+ for (const src of [a, b]) {
595
+ if (!src) continue;
596
+ for (const name in src) {
597
+ const list = out[name] ??= [];
598
+ for (const v of src[name]) if (!list.includes(v)) list.push(v);
599
+ }
600
+ }
601
+ return out;
602
+ }
603
+ /**
604
+ * `@Csp({ "frame-src": [...] })` — déclare des directives CSP **additionnelles**
605
+ * pour l'action (méthode) ou tout le contrôleur (classe). Distinct de
606
+ * `registerCspOrigins` (besoins PERMANENTS d'un module, ex. Vite) : ici c'est le
607
+ * besoin ponctuel d'UNE réponse (embarquer une iframe YouTube, autoriser une CDN).
608
+ *
609
+ * Classe + méthode fusionnent additivement (sources concaténées par directive).
610
+ * Empiler plusieurs `@Csp` fusionne aussi. N'écrit QUE des métadonnées : le merge
611
+ * dans le CSP de la réponse est fait par le firewall, hors hot-path, UNIQUEMENT
612
+ * sur les routes décorées. Calque `@IsGranted` (0 import `@nodefony/security`).
613
+ */
614
+ function Csp(directives) {
615
+ return function(target, propertyKey, descriptor) {
616
+ if (propertyKey === void 0) {
617
+ const existing = Reflect.getMetadata(CSP_DIRECTIVES_METADATA, target);
618
+ Reflect.defineMetadata(CSP_DIRECTIVES_METADATA, mergeCspDirectives(existing, directives), target);
619
+ return target;
620
+ }
621
+ const existing = Reflect.getMetadata(CSP_DIRECTIVES_METADATA, target, propertyKey);
622
+ Reflect.defineMetadata(CSP_DIRECTIVES_METADATA, mergeCspDirectives(existing, directives), target, propertyKey);
623
+ return descriptor;
624
+ };
625
+ }
626
+ /**
627
+ * Fabrique d'un marqueur booléen dual classe+méthode (idiome `any` du module).
628
+ * Pose `true` sur le ctor (classe) ou le prototype keyé par nom (méthode).
629
+ */
630
+ function booleanMarkerDecorator(markerKey) {
631
+ return function() {
632
+ return function(target, propertyKey, descriptor) {
633
+ if (propertyKey === void 0) {
634
+ Reflect.defineMetadata(markerKey, true, target);
635
+ return target;
636
+ }
637
+ Reflect.defineMetadata(markerKey, true, target, propertyKey);
638
+ return descriptor;
639
+ };
640
+ };
641
+ }
642
+ /**
643
+ * `@CsrfProtect()` — opt-IN à la défense CSRF **synchronizer token** (double-submit
644
+ * signé HMAC) EN PLUS de la défense globale Fetch Metadata/Origin (étape 1, toujours
645
+ * active). Pour les mutations à haute valeur (changement de mot de passe, virement) :
646
+ * une requête sûre vers la route SÈME le cookie lisible `csrf-token` ; la mutation
647
+ * DOIT rejouer ce token dans l'en-tête `x-csrf-token` (sinon 403). Classe = toutes
648
+ * les actions. N'écrit qu'un marqueur (0 import `@nodefony/security`, 0 cycle).
649
+ */
650
+ const CsrfProtect = booleanMarkerDecorator(CSRF_PROTECT_METADATA);
651
+ /**
652
+ * `@CsrfExempt()` — opt-OUT de la défense CSRF pour une route, **en conservant
653
+ * l'authentification et l'autorisation** (≠ `@Anonymous`/`@BypassFirewall` qui
654
+ * coupent l'auth). Pour un webhook ou une API recevant un POST cross-origin
655
+ * légitime, dont la requête est authentifiée autrement (signature HMAC du provider,
656
+ * clé API). Classe = toutes les actions. Marqueur seul (0 logique sécu ici).
657
+ */
658
+ const CsrfExempt = booleanMarkerDecorator(CSRF_EXEMPT_METADATA);
659
+ /**
660
+ * `@Idempotent()` — protège une **mutation** (POST/PUT/PATCH/DELETE) d'un
661
+ * controller userland contre le double-effet d'un rejeu (double-clic, reconnexion
662
+ * socket, retry réseau), via une `Idempotency-Key` cliente (modèle Stripe, conforme
663
+ * `draft-ietf-httpapi-idempotency-key-header`). No-op sur les méthodes sûres (GET…).
664
+ *
665
+ * - **STRICT par défaut** : une mutation SANS clé est rejetée **400** (draft §2.7).
666
+ * - `@Idempotent({ required: false })` : mode **souple** — honore la clé si fournie,
667
+ * exécute sinon. (Une mutation par **socket** reste toujours strict : le WS rejoue.)
668
+ * - clé fournie → dédup complète : rejeu complété → réponse **mémorisée** ; rejeu
669
+ * concurrent → **409** ; même clé + payload différent → **422**.
670
+ *
671
+ * Décorateur de **méthode** (une action) ou de **classe** (toutes les mutations du
672
+ * controller). Précédence : méthode > classe (comme `@UseSession`). N'écrit QUE des
673
+ * métadonnées (0 import `@nodefony/security`, 0 cycle) ; la porte est appliquée par
674
+ * le `Resolver` (helper partagé `idempotency.ts`, le MÊME que le data plane admin),
675
+ * sur le `idempotencyStore` DI. Coût nul sur une route non décorée (`security: null`).
676
+ *
677
+ * @example
678
+ * \@controller("/api/orders")
679
+ * class OrderController extends Controller {
680
+ * \@Post("/") @Idempotent() create(@Body() dto: CreateOrder) { ... } // clé obligatoire
681
+ * \@Patch("/{id}") @Idempotent({ required: false }) update() { ... } // clé optionnelle (HTTP)
682
+ * }
683
+ */
684
+ function Idempotent(options) {
685
+ const meta = { required: options?.required ?? true };
686
+ return function(target, propertyKey, descriptor) {
687
+ if (propertyKey === void 0) {
688
+ Reflect.defineMetadata(IDEMPOTENT_METADATA, meta, target);
689
+ return target;
690
+ }
691
+ Reflect.defineMetadata(IDEMPOTENT_METADATA, meta, target, propertyKey);
692
+ return descriptor;
693
+ };
694
+ }
695
+ function paramDecoratorFactory(source) {
696
+ return function(key) {
697
+ return function(target, propertyKey, parameterIndex) {
698
+ const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey) || [];
699
+ existing.push({
700
+ source,
701
+ key,
702
+ index: parameterIndex
703
+ });
704
+ Reflect.defineMetadata(PARAM_ARGS_METADATA, existing, target, propertyKey);
705
+ };
706
+ };
707
+ }
708
+ const Param = paramDecoratorFactory("param");
709
+ const Query = paramDecoratorFactory("query");
710
+ /**
711
+ * Décorateur de paramètre `@Body` :
712
+ * - `@Body()` → body parsé entier · `@Body("field")` → un champ du body parsé.
713
+ * - `@Body({ stream: true })` → **flux brut** de la requête (`Readable`), sans
714
+ * parse en mémoire (P2.9 — gros uploads sans pic RAM ; le pipeline saute le
715
+ * parse busboy/JSON pour cette route).
716
+ *
717
+ * ## Le corps n'est PAS validé — le type écrit ici ne promet rien
718
+ *
719
+ * `@Body()` injecte le corps **tel qu'il a été parsé** : `@Body() dto: CreateOrder`
720
+ * compile, mais rien ne garantit qu'un `CreateOrder` soit arrivé. C'est un choix
721
+ * assumé, pas un oubli — valider ici ne garderait que la porte HTTP, et devrait
722
+ * rester **synchrone** ({@link resolveParamArg} l'est, et le rendre asynchrone
723
+ * taxerait toute requête à paramètres décorés).
724
+ *
725
+ * Où valider, donc :
726
+ *
727
+ * - **Une entité** → les hooks `beforeCreate` / `beforeUpdate` d'
728
+ * `AbstractCrudService` : ils sont `await`és — donc une règle asynchrone
729
+ * (unicité en base) y est possible — et ils gardent REST, WebSocket **et** la
730
+ * CLI d'un seul geste. C'est ce que génère `nodefony create entity`.
731
+ * - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
732
+ * modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
733
+ * devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
734
+ * quel message, quelle règle.
735
+ *
736
+ * Et typer le paramètre avec `z.infer<typeof createXSchema>` plutôt qu'avec
737
+ * `Partial<XRow>` : le premier décrit le contrat d'**entrée**, le second promet la
738
+ * ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * \@Post("/")
743
+ * async create(\@Body() payload: CreatePost) {
744
+ * return this.createResource(payload); // le service valide dans beforeCreate
745
+ * }
746
+ * ```
747
+ */
748
+ function Body(keyOrOptions) {
749
+ const isOptions = typeof keyOrOptions === "object" && keyOrOptions !== null;
750
+ const key = isOptions ? void 0 : keyOrOptions;
751
+ const stream = isOptions ? keyOrOptions.stream === true : false;
752
+ return function(target, propertyKey, parameterIndex) {
753
+ const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey) || [];
754
+ const meta = {
755
+ source: "body",
756
+ key,
757
+ index: parameterIndex
758
+ };
759
+ if (stream) meta.stream = true;
760
+ existing.push(meta);
761
+ Reflect.defineMetadata(PARAM_ARGS_METADATA, existing, target, propertyKey);
762
+ };
763
+ }
764
+ const Headers = paramDecoratorFactory("headers");
765
+ const Cookie = paramDecoratorFactory("cookie");
766
+ const Session = paramDecoratorFactory("session");
767
+ /** `@CurrentUser() user: IUser` — injecte l'utilisateur de l'ALS (jamais le credential). */
768
+ const CurrentUser = paramDecoratorFactory("user");
769
+ const Req = paramDecoratorFactory("req");
770
+ const Res = paramDecoratorFactory("res");
771
+ const UploadedFile = paramDecoratorFactory("file");
772
+ const UploadedFiles = paramDecoratorFactory("files");
773
+ /**
774
+ * Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
775
+ *
776
+ * @param meta - métadonnée posée par le décorateur (source + clé optionnelle)
777
+ * @param ctx - contexte de requête (forme structurelle minimale)
778
+ * @returns la valeur à injecter dans l'argument `meta.index` de l'action
779
+ */
780
+ function resolveParamArg(meta, ctx) {
781
+ switch (meta.source) {
782
+ case "param": return meta.key !== void 0 ? ctx.paramsMap[meta.key] : ctx.paramsMap;
783
+ case "query": {
784
+ const qg = ctx.queryOverride ?? ctx.request?.queryGet;
785
+ return meta.key !== void 0 ? qg?.[meta.key] : qg;
786
+ }
787
+ case "body": {
788
+ if (meta.stream) return ctx.request?.request;
789
+ const alsBody = RequestContext.get()?.body;
790
+ if (alsBody !== void 0) return meta.key !== void 0 ? alsBody?.[meta.key] : alsBody;
791
+ const qp = ctx.request?.queryPost;
792
+ return meta.key !== void 0 ? qp?.[meta.key] : qp;
793
+ }
794
+ case "headers": {
795
+ const h = ctx.request?.headers;
796
+ return meta.key !== void 0 ? h?.[meta.key.toLowerCase()] : h;
797
+ }
798
+ case "cookie": return ctx.getRequestCookies(meta.key);
799
+ case "session": return meta.key !== void 0 ? ctx.session?.get(meta.key) : ctx.session;
800
+ case "req": return ctx.request;
801
+ case "res": return ctx.response;
802
+ case "file": return ctx.request?.queryFile?.[0];
803
+ case "files": return ctx.request?.queryFile;
804
+ case "user": return RequestContext.getUser();
805
+ default: return;
806
+ }
807
+ }
808
+ /**
809
+ * Construit le tableau d'arguments d'une action à partir des métadonnées de
810
+ * paramètres décorés. Chaque valeur est placée à son `index` déclaré (les trous
811
+ * restent `undefined`). Fonction pure — aucun effet de bord, aucune I/O.
812
+ *
813
+ * @param metas - métadonnées de tous les paramètres décorés de l'action
814
+ * @param ctx - contexte de requête (forme structurelle minimale)
815
+ * @returns arguments positionnels à spread dans l'action
816
+ */
817
+ function buildParamArgs(metas, ctx) {
818
+ const result = [];
819
+ for (const meta of metas) result[meta.index] = resolveParamArg(meta, ctx);
820
+ return result;
821
+ }
822
+ /**
823
+ * P2.9 — Indique si l'action d'une route attend le **flux brut** du body
824
+ * (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
825
+ * `route.bodyStream` : lecture `Reflect` au 1er appel, O(1) ensuite → 0 coût
826
+ * hot-path. Lu **en amont** par `handleHttp` (avant le parse) pour décider de
827
+ * sauter le parse busboy/JSON. Typage structurel (pas d'import `Route` → 0 cycle).
828
+ *
829
+ * @param routeDef - route résolue (porte `controller` + `classMethod` à `onBoot`).
830
+ * @returns `true` si l'action déclare un `@Body({ stream:true })`.
831
+ */
832
+ function routeExpectsBodyStream(routeDef) {
833
+ if (routeDef.bodyStream === void 0) {
834
+ let flag = false;
835
+ const ctor = routeDef.controller;
836
+ const method = routeDef.classMethod;
837
+ if (ctor && method) flag = (Reflect.getMetadata("route:paramArgs", ctor.prototype, method) || []).some((m) => m.source === "body" && m.stream === true);
838
+ routeDef.bodyStream = flag;
839
+ }
840
+ return routeDef.bodyStream;
841
+ }
842
+ const EMPTY_ACTION_META = {
843
+ paramsMeta: null,
844
+ redirectMeta: null,
845
+ httpCode: null,
846
+ headerEntries: null,
847
+ sessionIntent: null,
848
+ security: null,
849
+ cspDirectives: null,
850
+ csrfProtect: false,
851
+ csrfExempt: false,
852
+ idempotent: null
853
+ };
854
+ /**
855
+ * P5 — Calcule le {@link RouteActionMeta} d'un couple (controller, action) par
856
+ * lecture `Reflect`. Fonction PURE (pas de memo) : utilisée par
857
+ * {@link resolveActionMeta} (routes, mémoïsé) et par le `Resolver` pour le
858
+ * chemin froid du forward (`parsePathernController`, pas de route).
859
+ */
860
+ /** Lit un tableau de clauses (`@IsGranted`/`@RequireScope`) posé sur une cible Reflect — `[]` si absent. */
861
+ function readClauses(target, propertyKey) {
862
+ return (propertyKey === void 0 ? Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) : Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey)) ?? [];
863
+ }
864
+ /** Idem pour les clauses de scope (`@RequireScope`, metadata dédiée) — `[]` si absent. */
865
+ function readScopeClauses(target, propertyKey) {
866
+ return (propertyKey === void 0 ? Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) : Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey)) ?? [];
867
+ }
868
+ /**
869
+ * P6.8 — Liste PLATE des scopes `api:action` déclarés par une action
870
+ * (`@RequireScope`, classe + méthode), **dédupliqués**. Source de la **découverte
871
+ * au boot** : le catalogue de scopes du formulaire de clés API se construit en
872
+ * scannant les routes (cf `collectDeclaredApiScopes`), au lieu d'une config plate
873
+ * qui dérive du code. Lecture `Reflect` directe — **cold path** (introspection à la
874
+ * demande, jamais sur le hot path requête). `[]` si l'action ne déclare aucun scope.
875
+ */
876
+ function extractActionScopes(ctor, method) {
877
+ const out = /* @__PURE__ */ new Set();
878
+ for (const clause of readScopeClauses(ctor.prototype, method)) for (const s of clause.anyOf) out.add(s);
879
+ for (const clause of readScopeClauses(ctor)) for (const s of clause.anyOf) out.add(s);
880
+ return [...out];
881
+ }
882
+ /**
883
+ * P6 J7 / P6.8 — Fusionne les clauses `@IsGranted` (rôles) ET `@RequireScope`
884
+ * (scopes) — classe + méthode — en une exigence d'autorisation **figée**, ou
885
+ * `null` si l'action n'est pas gardée. Les deux axes cohabitent dans le même
886
+ * `SecurityRequirement` (clauses en **AND**) → un seul chemin d'enforcement dans
887
+ * le `Resolver`, le bon voter (`RoleVoter`/`ScopeVoter`) répond par attribut.
888
+ * `@Anonymous` (méthode) rend l'action publique (override `@IsGranted`/
889
+ * `@RequireScope` de classe → `null`). Lecture `Reflect` faite UNE fois (via
890
+ * {@link resolveActionMeta} mémoïsé).
891
+ */
892
+ function computeSecurityRequirement(ctor, method) {
893
+ if (Reflect.getMetadata(SECURITY_ANONYMOUS_METADATA, ctor, method) === true) return null;
894
+ const proto = ctor.prototype;
895
+ const methodClauses = readClauses(proto, method);
896
+ const methodScopes = readScopeClauses(proto, method);
897
+ const classAnon = Reflect.getMetadata(SECURITY_ANONYMOUS_METADATA, ctor) === true;
898
+ const classClauses = classAnon ? [] : readClauses(ctor);
899
+ const classScopes = classAnon ? [] : readScopeClauses(ctor);
900
+ const all = [
901
+ ...classClauses,
902
+ ...methodClauses,
903
+ ...classScopes,
904
+ ...methodScopes
905
+ ];
906
+ if (all.length === 0) return null;
907
+ return Object.freeze({ clauses: Object.freeze(all) });
908
+ }
909
+ /**
910
+ * P6 — Fusionne les directives `@Csp` (classe + méthode) en un jeu figé, ou
911
+ * `null` si l'action n'en déclare aucune (cas courant → 0 alloc/composition).
912
+ * Lecture `Reflect` faite UNE fois (via {@link resolveActionMeta} mémoïsé).
913
+ */
914
+ function computeCspDirectives(ctor, method) {
915
+ const methodDirectives = Reflect.getMetadata(CSP_DIRECTIVES_METADATA, ctor.prototype, method);
916
+ const classDirectives = Reflect.getMetadata(CSP_DIRECTIVES_METADATA, ctor);
917
+ if (!classDirectives && !methodDirectives) return null;
918
+ return Object.freeze(mergeCspDirectives(classDirectives, methodDirectives));
919
+ }
920
+ /**
921
+ * P6.8 — Résout la config `@Idempotent` figée d'une action (méthode prime sur
922
+ * classe, comme `@UseSession`), ou `null` si non décorée. `@Idempotent()` pose
923
+ * `{ required: true }` explicite → la précédence méthode > classe est non ambiguë
924
+ * (une méthode `@Idempotent()` force le mode strict même sous une classe souple).
925
+ * Lecture `Reflect` faite UNE fois (via {@link resolveActionMeta} mémoïsé).
926
+ */
927
+ function computeIdempotent(ctor, method) {
928
+ const methodMeta = Reflect.getMetadata(IDEMPOTENT_METADATA, ctor.prototype, method);
929
+ const classMeta = Reflect.getMetadata(IDEMPOTENT_METADATA, ctor);
930
+ if (methodMeta === void 0 && classMeta === void 0) return null;
931
+ return Object.freeze({ required: methodMeta?.required ?? classMeta?.required ?? true });
932
+ }
933
+ function computeActionMeta(ctor, method) {
934
+ if (!ctor || !method) return EMPTY_ACTION_META;
935
+ const proto = ctor.prototype;
936
+ const params = Reflect.getMetadata(PARAM_ARGS_METADATA, proto, method);
937
+ const redirect = Reflect.getMetadata(REDIRECT_METADATA, proto, method);
938
+ const httpCode = Reflect.getMetadata(HTTP_CODE_METADATA, proto, method);
939
+ const headers = Reflect.getMetadata(HEADERS_METADATA, proto, method);
940
+ return {
941
+ paramsMeta: params && params.length > 0 ? params : null,
942
+ redirectMeta: redirect ?? null,
943
+ httpCode: httpCode ?? null,
944
+ headerEntries: headers ? Object.entries(headers) : null,
945
+ sessionIntent: resolveSessionIntent(ctor, method),
946
+ security: computeSecurityRequirement(ctor, method),
947
+ cspDirectives: computeCspDirectives(ctor, method),
948
+ csrfProtect: Reflect.getMetadata(CSRF_PROTECT_METADATA, proto, method) === true || Reflect.getMetadata(CSRF_PROTECT_METADATA, ctor) === true,
949
+ csrfExempt: Reflect.getMetadata(CSRF_EXEMPT_METADATA, proto, method) === true || Reflect.getMetadata(CSRF_EXEMPT_METADATA, ctor) === true,
950
+ idempotent: computeIdempotent(ctor, method)
951
+ };
952
+ }
953
+ /**
954
+ * P5 — Metadata d'action d'une route, **mémoïsées au 1er hit** sur
955
+ * `route.actionMeta` (pattern frère de {@link routeExpectsBodyStream}) :
956
+ * `undefined` = pas encore résolu → 1 lecture `Reflect` par route pour la vie
957
+ * du process, O(1) ensuite. Sort `Reflect.getMetadata` (~6 appels/req) du hot
958
+ * path `match`/`executeAction`. Posé APRÈS `generateId()` (1ʳᵉ requête) → le
959
+ * hash de route et l'introspection Studio restent stables. Typage structurel
960
+ * (pas d'import `Route` → 0 cycle).
961
+ */
962
+ function resolveActionMeta(routeDef) {
963
+ if (routeDef.actionMeta === void 0) routeDef.actionMeta = computeActionMeta(routeDef.controller, routeDef.classMethod);
964
+ return routeDef.actionMeta;
965
+ }
966
+ //#endregion
967
+ export { All, Anonymous, BYPASS_FIREWALL_CLASS_METADATA, BYPASS_FIREWALL_METHOD_METADATA, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, DOMAIN_CLASS_METADATA, DOMAIN_METHOD_METADATA, Delete, Domain, Get, HEADERS_METADATA, HTTP_CODE_METADATA, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, PARAM_ARGS_METADATA, Param, Patch, Post, Put, Query, REDIRECT_METADATA, Redirect, Req, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };