@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.4

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 (76) hide show
  1. package/README.md +4 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +15 -20
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  6. package/dist/nodefony/controller/AdminApiController.js +45 -11
  7. package/dist/nodefony/controller/ApiKeyController.js +18 -10
  8. package/dist/nodefony/controller/BenchController.js +1 -2
  9. package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
  10. package/dist/nodefony/controller/LoginPageController.js +117 -0
  11. package/dist/nodefony/controller/OAuth2Controller.js +133 -17
  12. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
  13. package/dist/nodefony/controller/SessionAuthController.js +50 -13
  14. package/dist/nodefony/controller/TokenAuthController.js +14 -8
  15. package/dist/nodefony/controller/TotpController.js +7 -8
  16. package/dist/nodefony/controller/WebAuthnController.js +28 -12
  17. package/dist/nodefony/controller/oauthAuthority.js +1 -2
  18. package/dist/nodefony/decorators/routerDecorators.js +268 -65
  19. package/dist/nodefony/service/AdminBroker.js +4 -6
  20. package/dist/nodefony/service/Eta.js +1 -2
  21. package/dist/nodefony/service/IdempotencyStore.js +4 -6
  22. package/dist/nodefony/service/router.js +97 -34
  23. package/dist/nodefony/src/Controller.js +71 -43
  24. package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
  25. package/dist/nodefony/src/KernelAdminApi.js +82 -18
  26. package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
  27. package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
  28. package/dist/nodefony/src/Resolver.js +105 -36
  29. package/dist/nodefony/src/ResourceController.js +2 -3
  30. package/dist/nodefony/src/Route.js +54 -8
  31. package/dist/nodefony/src/SecurityApiController.js +23 -0
  32. package/dist/nodefony/src/SyslogAdminApi.js +1 -2
  33. package/dist/nodefony/src/Template.js +1 -2
  34. package/dist/nodefony/src/configMutation.js +1 -2
  35. package/dist/nodefony/src/docsReader.js +39 -23
  36. package/dist/nodefony/src/idempotency.js +14 -22
  37. package/dist/nodefony/src/idempotencyGc.js +1 -2
  38. package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
  39. package/dist/nodefony/src/loginPage.js +430 -0
  40. package/dist/nodefony/src/requireAuthentication.js +35 -0
  41. package/dist/nodefony/src/routeSchema.js +81 -0
  42. package/dist/nodefony/src/scopeCatalog.js +1 -2
  43. package/dist/nodefony/src/singletonGuard.js +1 -2
  44. package/dist/nodefony/src/syslogFilters.js +1 -2
  45. package/dist/types/index.d.ts +8 -12
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
  47. package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
  48. package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
  49. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
  50. package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
  51. package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
  52. package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
  53. package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
  54. package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
  55. package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
  56. package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
  57. package/dist/types/nodefony/service/router.d.ts +40 -1
  58. package/dist/types/nodefony/src/Controller.d.ts +54 -17
  59. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  60. package/dist/types/nodefony/src/Resolver.d.ts +44 -1
  61. package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
  62. package/dist/types/nodefony/src/Route.d.ts +21 -1
  63. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
  64. package/dist/types/nodefony/src/docsReader.d.ts +10 -6
  65. package/dist/types/nodefony/src/idempotency.d.ts +12 -10
  66. package/dist/types/nodefony/src/loginPage.d.ts +144 -0
  67. package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
  68. package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
  69. package/docs/admin.md +83 -78
  70. package/docs/controller.md +112 -111
  71. package/docs/decorateurs.md +212 -163
  72. package/docs/idempotence.md +137 -135
  73. package/docs/routing.md +96 -95
  74. package/docs/templates.md +55 -55
  75. package/package.json +10 -13
  76. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,8 +1,9 @@
1
+ import { assertRouteSchemaShape } from "../src/routeSchema.js";
1
2
  import Controller from "../src/Controller.js";
2
3
  import router_default from "../service/router.js";
3
- import { RequestContext } from "nodefony";
4
+ import { BootConfigurationError, RequestContext } from "nodefony";
4
5
  import "reflect-metadata";
5
- //#region nodefony/decorators/routerDecorators.ts
6
+ // nodefony/decorators/routerDecorators.ts
6
7
  const metadataKey = "routes:definitions";
7
8
  /**
8
9
  * Noms déjà portés par `Controller` — méthodes ET accesseurs, les siens comme
@@ -134,7 +135,7 @@ function controller(prefix) {
134
135
  const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
135
136
  if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
136
137
  }
137
- if (options.path == "*") {
138
+ if (options.path === "*") {
138
139
  hasMagic = {
139
140
  options,
140
141
  name
@@ -149,26 +150,19 @@ function controller(prefix) {
149
150
  return mycontroller;
150
151
  };
151
152
  }
152
- /**
153
- * Crée une route avec le nom et les options spécifiés.
154
- *
155
- * @param name - Le nom de la route.
156
- * @param options - Les options de la route.
157
- * @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
158
- *
159
- * @example
160
- * \@route("myroute", {
161
- * path: "/add/{name}",
162
- * method: ["GET", "POST"],
163
- * defaults: { name: "john" },
164
- * })
165
- * method(name: string) {
166
- * return this.renderJson({ name });
167
- * }
168
- */
169
153
  function route(name, options) {
154
+ return defineRoute(name, options);
155
+ }
156
+ /** L'implémentation de {@link route}, sans les surcharges de typage. */
157
+ function defineRoute(name, options) {
170
158
  return function(target, propertyKey, descriptor) {
171
159
  assertActionNameFree(target, propertyKey);
160
+ if (options.schema !== void 0) {
161
+ const previous = Reflect.getOwnMetadata(ROUTE_SCHEMA_METADATA, target, propertyKey);
162
+ if (previous !== void 0 && previous !== options.schema) throw new BootConfigurationError(`${target.constructor.name}.${propertyKey} : deux routes de la même action déclarent des schémas différents — une action n'a qu'une entrée, déclarer le schéma une seule fois.`);
163
+ Reflect.defineMetadata(ROUTE_SCHEMA_METADATA, options.schema, target, propertyKey);
164
+ }
165
+ if (options.bodyStream === true) Reflect.defineMetadata(BODY_STREAM_METADATA, true, target, propertyKey);
172
166
  const className = target.constructor.name;
173
167
  const classMethod = propertyKey;
174
168
  const prefix = options.prefix || null;
@@ -183,7 +177,7 @@ function route(name, options) {
183
177
  } catch (error) {
184
178
  filePath = error;
185
179
  }
186
- const metadata = Reflect.getMetadata(metadataKey, target.constructor) ?? {};
180
+ const metadata = { ...Reflect.getMetadata(metadataKey, target.constructor) };
187
181
  metadata[name] = {
188
182
  path,
189
183
  filePath,
@@ -195,7 +189,8 @@ function route(name, options) {
195
189
  host: options.host,
196
190
  defaults: options.defaults,
197
191
  requirements: options.requirements,
198
- bypassFirewall: options.bypassFirewall
192
+ bypassFirewall: options.bypassFirewall,
193
+ fallback: options.fallback
199
194
  };
200
195
  Reflect.defineMetadata(metadataKey, metadata, target.constructor);
201
196
  return descriptor;
@@ -210,6 +205,10 @@ function extractControllerFilePath(stackTrace) {
210
205
  const HTTP_CODE_METADATA = "route:httpCode";
211
206
  const HEADERS_METADATA = "route:responseHeaders";
212
207
  const REDIRECT_METADATA = "route:redirect";
208
+ /** Schéma d'entrée d'une action (`schema` d'une option de route) — par méthode. */
209
+ const ROUTE_SCHEMA_METADATA = "route:schema";
210
+ /** `bodyStream: true` d'une option de route — par méthode. */
211
+ const BODY_STREAM_METADATA = "route:bodyStream";
213
212
  const PARAM_ARGS_METADATA = "route:paramArgs";
214
213
  const DOMAIN_CLASS_METADATA = "route:domainClass";
215
214
  const DOMAIN_METHOD_METADATA = "route:domainMethod";
@@ -231,7 +230,7 @@ function httpMethodDecorator(methods) {
231
230
  const declaredMethods = options.requirements?.methods;
232
231
  const added = declaredMethods === void 0 ? [] : Array.isArray(declaredMethods) ? declaredMethods : [declaredMethods];
233
232
  const fusion = added.length > 0 ? [.../* @__PURE__ */ new Set([...methods, ...added])] : methods;
234
- return route(name, {
233
+ return defineRoute(name, {
235
234
  ...options,
236
235
  path,
237
236
  requirements: {
@@ -254,14 +253,14 @@ const Head = httpMethodDecorator(["HEAD"]);
254
253
  * HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
255
254
  * `Route.matchRequirements` ne lève jamais 405 sur la méthode.
256
255
  */
257
- function All(path = "", options = {}) {
256
+ const All = function(path = "", options = {}) {
258
257
  return function(target, propertyKey, descriptor) {
259
- return route(`${target.constructor.name}::${propertyKey}`, {
258
+ return defineRoute(`${target.constructor.name}::${propertyKey}`, {
260
259
  ...options,
261
260
  path
262
261
  })(target, propertyKey, descriptor);
263
262
  };
264
- }
263
+ };
265
264
  /**
266
265
  * Fixe le code de statut HTTP de la réponse d'une action.
267
266
  *
@@ -312,7 +311,7 @@ function HttpCode(statusCode) {
312
311
  */
313
312
  function Header(key, value) {
314
313
  return function(target, propertyKey, descriptor) {
315
- const existing = Reflect.getMetadata("route:responseHeaders", target, propertyKey) ?? {};
314
+ const existing = { ...Reflect.getMetadata(HEADERS_METADATA, target, propertyKey) };
316
315
  existing[key] = value;
317
316
  Reflect.defineMetadata(HEADERS_METADATA, existing, target, propertyKey);
318
317
  return descriptor;
@@ -522,12 +521,12 @@ function IsGranted(attribute, options) {
522
521
  };
523
522
  return function(target, propertyKey, descriptor) {
524
523
  if (propertyKey === void 0) {
525
- const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) ?? [];
524
+ const existing = [...Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target) ?? []];
526
525
  existing.push(clause);
527
526
  Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target);
528
527
  return target;
529
528
  }
530
- const existing = Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey) ?? [];
529
+ const existing = [...Reflect.getMetadata(SECURITY_CLAUSES_METADATA, target, propertyKey) ?? []];
531
530
  existing.push(clause);
532
531
  Reflect.defineMetadata(SECURITY_CLAUSES_METADATA, existing, target, propertyKey);
533
532
  return descriptor;
@@ -578,12 +577,12 @@ function RequireScope(scope) {
578
577
  const clause = { anyOf: Array.isArray(scope) ? [...scope] : [scope] };
579
578
  return function(target, propertyKey, descriptor) {
580
579
  if (propertyKey === void 0) {
581
- const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) ?? [];
580
+ const existing = [...Reflect.getMetadata(SECURITY_SCOPES_METADATA, target) ?? []];
582
581
  existing.push(clause);
583
582
  Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target);
584
583
  return target;
585
584
  }
586
- const existing = Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey) ?? [];
585
+ const existing = [...Reflect.getMetadata(SECURITY_SCOPES_METADATA, target, propertyKey) ?? []];
587
586
  existing.push(clause);
588
587
  Reflect.defineMetadata(SECURITY_SCOPES_METADATA, existing, target, propertyKey);
589
588
  return descriptor;
@@ -700,7 +699,7 @@ function Idempotent(options) {
700
699
  function paramDecoratorFactory(source) {
701
700
  return function(key) {
702
701
  return function(target, propertyKey, parameterIndex) {
703
- const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey) ?? [];
702
+ const existing = Reflect.getOwnMetadata("route:paramArgs", target, propertyKey) ?? [];
704
703
  existing.push({
705
704
  source,
706
705
  key,
@@ -721,26 +720,18 @@ const Query = paramDecoratorFactory("query");
721
720
  *
722
721
  * ## Le corps n'est PAS validé — le type écrit ici ne promet rien
723
722
  *
724
- * `@Body()` injecte le corps **tel qu'il a été parsé** : `@Body() dto: CreateOrder`
725
- * compile, mais rien ne garantit qu'un `CreateOrder` soit arrivé. C'est un choix
726
- * assumé, pas un oubli — valider ici ne garderait que la porte HTTP, et devrait
727
- * rester **synchrone** ({@link resolveParamArg} l'est, et le rendre asynchrone
728
- * taxerait toute requête à paramètres décorés).
729
- *
730
- * Où valider, donc :
723
+ * `@Body()` injecte le corps **tel qu'il a été décodé** (objet JSON ou de
724
+ * formulaire, chaîne pour `text/*`, octets `Uint8Array` pour tout autre type) :
725
+ * `@Body() dto: CreateOrder` compile, mais rien ne garantit qu'un `CreateOrder`
726
+ * soit arrivé.
731
727
  *
732
- * - **Une entité** → les hooks `beforeCreate` / `beforeUpdate` d'
733
- * `AbstractCrudService` : ils sont `await`és — donc une règle asynchrone
734
- * (unicité en base) y est possible — et ils gardent REST, WebSocket **et** la
735
- * CLI d'un seul geste. C'est ce que génère `nodefony create entity`.
736
- * - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
737
- * modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
738
- * devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
739
- * quel message, quelle règle.
728
+ * Pour un contrat vérifié, déclarer l'option de route `schema` (ADR-0018) : le
729
+ * corps est validé AVANT l'action — 422 qui nomme chaque champ —, sur HTTP comme
730
+ * sur le pont WebSocket, et l'action le reçoit typé dans `input.body`. Une action
731
+ * à schéma ne prend aucun décorateur de paramètre.
740
732
  *
741
- * Et typer le paramètre avec `z.infer<typeof createXSchema>` plutôt qu'avec
742
- * `Partial<XRow>` : le premier décrit le contrat d'**entrée**, le second promet la
743
- * ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
733
+ * Sans schéma, `schema.parse(body)` en première ligne de l'action reste le geste
734
+ * d'un cas isolé : une `ZodError` qui remonte devient le même **422**.
744
735
  *
745
736
  * @example
746
737
  * ```ts
@@ -756,7 +747,7 @@ function Body(keyOrOptions) {
756
747
  const key = isOptions ? void 0 : raw ?? void 0;
757
748
  const stream = isOptions ? raw.stream === true : false;
758
749
  return function(target, propertyKey, parameterIndex) {
759
- const existing = Reflect.getMetadata("route:paramArgs", target, propertyKey) ?? [];
750
+ const existing = Reflect.getOwnMetadata("route:paramArgs", target, propertyKey) ?? [];
760
751
  const meta = {
761
752
  source: "body",
762
753
  key,
@@ -777,6 +768,75 @@ const Res = paramDecoratorFactory("res");
777
768
  const UploadedFile = paramDecoratorFactory("file");
778
769
  const UploadedFiles = paramDecoratorFactory("files");
779
770
  /**
771
+ * La query de l'invocation : celle du chemin INVOQUÉ par le pont WebSocket
772
+ * (`queryOverride`), sinon celle de l'URL. Source UNIQUE de `@Query` et de
773
+ * `input.query` d'une route à schéma.
774
+ */
775
+ function readQuery(ctx) {
776
+ return readQueryOf(ctx.queryOverride, ctx.request);
777
+ }
778
+ /** {@link readQuery}, depuis ses deux sources. */
779
+ function readQueryOf(queryOverride, request) {
780
+ return queryOverride ?? request?.query;
781
+ }
782
+ /**
783
+ * Le corps DÉCODÉ de l'invocation — source UNIQUE de `@Body` et de
784
+ * `input.body` d'une route à schéma, pour qu'un corps ne puisse pas prendre
785
+ * deux sens selon la forme de l'action.
786
+ *
787
+ * Pont `api.request` (WS) : le corps d'une mutation voyage dans l'ALS (aucun
788
+ * corps HTTP parsé sur une frame — posé par appel par
789
+ * `RealtimeController.invokeApiRequest`, zéro fuite entre appels) et PRIME sur
790
+ * `request.body` (vide en WS) — même priorité que
791
+ * `AdminApiController.buildRequest`. En HTTP, le magasin ALS ne porte jamais
792
+ * `body`. Aucune forme n'est supposée : un codec binaire y posera des octets.
793
+ */
794
+ function readBody(ctx) {
795
+ return readInvocationBody(ctx.request);
796
+ }
797
+ /**
798
+ * Le corps DÉCODÉ d'une invocation, depuis la requête seule — la règle de
799
+ * {@link readBody}, pour qui n'a pas de contexte de paramètres (porte
800
+ * d'idempotence).
801
+ *
802
+ * @param request - la requête du contexte.
803
+ * @returns le corps posé par le pont WebSocket, sinon le corps HTTP lu.
804
+ */
805
+ function readInvocationBody(request) {
806
+ const alsBody = RequestContext.get()?.body;
807
+ return alsBody !== void 0 ? alsBody : request?.body;
808
+ }
809
+ /** Un champ d'une valeur décodée ; `undefined` si elle n'est pas un objet. */
810
+ function readField(value, key) {
811
+ if (typeof value !== "object" || value === null) return void 0;
812
+ return Reflect.get(value, key);
813
+ }
814
+ /** Objet vide partagé : une partie absente n'alloue rien. Gelé, jamais muté. */
815
+ const EMPTY_PART = Object.freeze({});
816
+ /**
817
+ * L'entrée BRUTE d'une action à schéma, lue aux MÊMES sources que les
818
+ * décorateurs de paramètre ({@link readQuery}, {@link readBody}) : pont
819
+ * WebSocket compris. Seule allocation de la voie à schéma : cet objet, propre
820
+ * à l'invocation, que la validation complète en place et que l'action reçoit.
821
+ *
822
+ * Arguments POSITIONNELS, pas un objet de contexte : la voie à schéma n'en
823
+ * alloue aucun de plus.
824
+ *
825
+ * @param paramsMap - variables de route nommées.
826
+ * @param request - la requête du contexte.
827
+ * @param queryOverride - query du chemin invoqué (pont WebSocket), sinon rien.
828
+ * @param bodyStream - `true` : `body` est le flux brut, jamais lu.
829
+ * @returns les quatre parties, brutes.
830
+ */
831
+ function readRouteInput(paramsMap, request, queryOverride, bodyStream) {
832
+ return {
833
+ params: paramsMap,
834
+ query: readQueryOf(queryOverride, request) ?? EMPTY_PART,
835
+ body: bodyStream ? request?.request : readInvocationBody(request),
836
+ headers: request?.headers ?? EMPTY_PART
837
+ };
838
+ }
839
+ /**
780
840
  * Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
781
841
  *
782
842
  * @param meta - métadonnée posée par le décorateur (source + clé optionnelle)
@@ -787,15 +847,13 @@ function resolveParamArg(meta, ctx) {
787
847
  switch (meta.source) {
788
848
  case "param": return meta.key !== void 0 ? ctx.paramsMap[meta.key] : ctx.paramsMap;
789
849
  case "query": {
790
- const qg = ctx.queryOverride ?? ctx.request?.queryGet;
791
- return meta.key !== void 0 ? qg?.[meta.key] : qg;
850
+ const q = readQuery(ctx);
851
+ return meta.key !== void 0 ? q?.[meta.key] : q;
792
852
  }
793
853
  case "body": {
794
854
  if (meta.stream) return ctx.request?.request;
795
- const alsBody = RequestContext.get()?.body;
796
- if (alsBody !== void 0) return meta.key !== void 0 ? alsBody?.[meta.key] : alsBody;
797
- const qp = ctx.request?.queryPost;
798
- return meta.key !== void 0 ? qp?.[meta.key] : qp;
855
+ const body = readBody(ctx);
856
+ return meta.key !== void 0 ? readField(body, meta.key) : body;
799
857
  }
800
858
  case "headers": {
801
859
  const h = ctx.request?.headers;
@@ -805,9 +863,9 @@ function resolveParamArg(meta, ctx) {
805
863
  case "session": return meta.key !== void 0 ? ctx.session?.get(meta.key) : ctx.session;
806
864
  case "req": return ctx.request;
807
865
  case "res": return ctx.response;
808
- case "file": return ctx.request?.queryFile?.[0];
809
- case "files": return ctx.request?.queryFile;
810
- case "user": return RequestContext.getUser();
866
+ case "file": return ctx.request?.files?.[0];
867
+ case "files": return ctx.request?.files;
868
+ case "user": return RequestContext.isAnonymous() ? void 0 : RequestContext.getUser();
811
869
  default: return;
812
870
  }
813
871
  }
@@ -826,6 +884,87 @@ function buildParamArgs(metas, ctx) {
826
884
  return result;
827
885
  }
828
886
  /**
887
+ * Résout les paramètres décorés d'une action depuis un `Context` — la mise en
888
+ * forme UNIQUE, appelée par le Resolver (routes HTTP et WS) comme par les
889
+ * actions `@RealtimeAction`, pour qu'un décorateur ne puisse pas prendre deux
890
+ * sens selon le transport.
891
+ *
892
+ * Le corps d'un appel WebSocket voyage dans l'ALS (`RequestContext` clé
893
+ * `body`) et `@CurrentUser` y lit l'utilisateur : l'appelant pose la bulle,
894
+ * cette fonction n'en crée aucune.
895
+ *
896
+ * @param metas - paramètres décorés de l'action ({@link getParamArgsMeta})
897
+ * @param context - le contexte de la requête ou de la connexion
898
+ * @param paramsMap - variables de route nommées (vide hors route)
899
+ * @param queryOverride - query de l'invocation quand elle ne vient pas de l'URL
900
+ * @returns arguments positionnels à passer à l'action
901
+ */
902
+ function buildContextParamArgs(metas, context, paramsMap, queryOverride) {
903
+ return buildParamArgs(metas, {
904
+ paramsMap,
905
+ request: context.request,
906
+ response: context.response,
907
+ session: context.session,
908
+ queryOverride,
909
+ getRequestCookies: (name) => context.getRequestCookies(name)
910
+ });
911
+ }
912
+ /**
913
+ * Les paramètres décorés d'une méthode (`@Body`, `@CurrentUser`…), ou `null`
914
+ * si elle n'en porte aucun. Lecture `Reflect` : à faire une fois, hors du
915
+ * chemin de chaque requête.
916
+ *
917
+ * @param proto - le prototype de la classe
918
+ * @param method - le nom de la méthode
919
+ * @returns les métadonnées, ou `null` — une action sans décorateur ne paie rien
920
+ */
921
+ function getParamArgsMeta(proto, method) {
922
+ const metas = Reflect.getMetadata(PARAM_ARGS_METADATA, proto, method);
923
+ return metas !== void 0 && metas.length > 0 ? metas : null;
924
+ }
925
+ /**
926
+ * Le schéma d'entrée d'une méthode (option de route `schema`), ou `null`.
927
+ * Lecture `Reflect` : à faire une fois, hors du chemin de chaque requête.
928
+ *
929
+ * @param proto - le prototype de la classe.
930
+ * @param method - le nom de la méthode.
931
+ * @returns le schéma, ou `null` — une action sans schéma ne paie rien.
932
+ * @throws BootConfigurationError si le schéma rangé est mal formé.
933
+ */
934
+ function getRouteSchema(proto, method) {
935
+ const schema = Reflect.getMetadata(ROUTE_SCHEMA_METADATA, proto, method);
936
+ if (schema === void 0) return null;
937
+ assertRouteSchemaShape(schema, `${proto.constructor.name}.${method}`);
938
+ return schema;
939
+ }
940
+ /**
941
+ * Refuse, au DÉMARRAGE, une action à schéma mal déclarée — appelé par
942
+ * `Router.setController` pour chaque route du contrôleur, pour qu'une faute
943
+ * ne se révèle pas à la première requête :
944
+ *
945
+ * - schéma mal formé (partie inconnue, valeur qui n'est pas un Standard Schema) ;
946
+ * - schéma ET décorateurs de paramètre (`@Body`…) : une action à schéma reçoit
947
+ * un argument UNIQUE, les deux formes ne se combinent pas ;
948
+ * - `bodyStream: true` ET `schema.body` : un flux ne se valide pas.
949
+ *
950
+ * @param routeDef - la route (nom, contrôleur, méthode).
951
+ * @throws BootConfigurationError qui nomme la route.
952
+ */
953
+ function assertRouteSchemaDeclaration(routeDef) {
954
+ const ctor = routeDef.controller;
955
+ const method = routeDef.classMethod;
956
+ if (!ctor || !method) return;
957
+ const proto = ctor.prototype;
958
+ const raw = Reflect.getMetadata(ROUTE_SCHEMA_METADATA, proto, method);
959
+ if (raw === void 0) {
960
+ if (Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true) throw new BootConfigurationError(`Route "${routeDef.name}" : « bodyStream: true » exige un « schema » — sans schéma, demander le flux par @Body({ stream: true }).`);
961
+ return;
962
+ }
963
+ assertRouteSchemaShape(raw, routeDef.name);
964
+ if (getParamArgsMeta(proto, method) !== null) throw new BootConfigurationError(`Route "${routeDef.name}" : une action à schéma reçoit un argument unique (input) — retirer les décorateurs de paramètre (@Body, @Query, @Param…) de ${proto.constructor.name}.${method}, ou retirer l'option « schema ».`);
965
+ if (raw.body !== void 0 && Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true) throw new BootConfigurationError(`Route "${routeDef.name}" : « bodyStream: true » et « schema.body » s'excluent — un flux n'est pas lu, il ne peut pas être validé.`);
966
+ }
967
+ /**
829
968
  * P2.9 — Indique si l'action d'une route attend le **flux brut** du body
830
969
  * (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
831
970
  * `route.bodyStream` : lecture `Reflect` au 1er appel, O(1) ensuite → 0 coût
@@ -840,18 +979,21 @@ function routeExpectsBodyStream(routeDef) {
840
979
  let flag = false;
841
980
  const ctor = routeDef.controller;
842
981
  const method = routeDef.classMethod;
843
- if (ctor && method) flag = (Reflect.getMetadata("route:paramArgs", ctor.prototype, method) ?? []).some((m) => m.source === "body" && m.stream === true);
982
+ if (ctor && method) flag = Reflect.getMetadata(BODY_STREAM_METADATA, ctor.prototype, method) === true || (getParamArgsMeta(ctor.prototype, method)?.some((m) => m.source === "body" && m.stream === true) ?? false);
844
983
  routeDef.bodyStream = flag;
845
984
  }
846
985
  return routeDef.bodyStream;
847
986
  }
848
987
  const EMPTY_ACTION_META = {
849
988
  paramsMeta: null,
989
+ schema: null,
990
+ bodyStream: false,
850
991
  redirectMeta: null,
851
992
  httpCode: null,
852
993
  headerEntries: null,
853
994
  sessionIntent: null,
854
995
  security: null,
996
+ authentication: null,
855
997
  cspDirectives: null,
856
998
  csrfProtect: false,
857
999
  csrfExempt: false,
@@ -942,17 +1084,19 @@ function computeIdempotent(ctor, method) {
942
1084
  function computeActionMeta(ctor, method) {
943
1085
  if (!ctor || !method) return EMPTY_ACTION_META;
944
1086
  const proto = ctor.prototype;
945
- const params = Reflect.getMetadata(PARAM_ARGS_METADATA, proto, method);
946
1087
  const redirect = Reflect.getMetadata(REDIRECT_METADATA, proto, method);
947
1088
  const httpCode = Reflect.getMetadata(HTTP_CODE_METADATA, proto, method);
948
1089
  const headers = Reflect.getMetadata(HEADERS_METADATA, proto, method);
949
1090
  return {
950
- paramsMeta: params && params.length > 0 ? params : null,
1091
+ paramsMeta: getParamArgsMeta(proto, method),
1092
+ schema: getRouteSchema(proto, method),
1093
+ bodyStream: Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true,
951
1094
  redirectMeta: redirect ?? null,
952
1095
  httpCode: httpCode ?? null,
953
1096
  headerEntries: headers ? Object.entries(headers) : null,
954
1097
  sessionIntent: resolveSessionIntent(ctor, method),
955
1098
  security: computeSecurityRequirement(ctor, method),
1099
+ authentication: computeAuthenticationRequirement(ctor, method),
956
1100
  cspDirectives: computeCspDirectives(ctor, method),
957
1101
  csrfProtect: Reflect.getMetadata(CSRF_PROTECT_METADATA, proto, method) === true || Reflect.getMetadata(CSRF_PROTECT_METADATA, ctor) === true,
958
1102
  csrfExempt: Reflect.getMetadata(CSRF_EXEMPT_METADATA, proto, method) === true || Reflect.getMetadata(CSRF_EXEMPT_METADATA, ctor) === true,
@@ -972,5 +1116,64 @@ function resolveActionMeta(routeDef) {
972
1116
  routeDef.actionMeta ??= computeActionMeta(routeDef.controller, routeDef.classMethod);
973
1117
  return routeDef.actionMeta;
974
1118
  }
975
- //#endregion
976
- 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 };
1119
+ const AUTHENTICATION_REQUIREMENT_METADATA = "nodefony:security:authentication";
1120
+ /**
1121
+ * Exige une connexion RÉCENTE et/ou faite avec certains moyens pour l'action
1122
+ * (décorateur de méthode) ou tout le contrôleur (décorateur de classe).
1123
+ *
1124
+ * - `@RequireAuthentication({ maxAge: 300 })` — connexion de moins de 5 minutes.
1125
+ * - `@RequireAuthentication({ amr: ["otp"] })` — second facteur exigé.
1126
+ * - les deux se combinent ; classe + méthode fusionnent au plus strict.
1127
+ *
1128
+ * Lit ce que la session a retenu à son ouverture (`readSessionAuthentication`,
1129
+ * ADR-0015) et s'évalue dans le `Resolver` APRÈS `@IsGranted` : un rôle absent
1130
+ * reste un 403 définitif, une connexion trop ancienne ou trop faible devient un
1131
+ * **401** dont le `WWW-Authenticate` porte
1132
+ * `error="insufficient_user_authentication"` (RFC 9470) — le client sait qu'il
1133
+ * doit redemander une connexion, pas que l'accès lui est fermé. Une session qui
1134
+ * ne porte pas ces métadonnées est refusée, jamais acceptée.
1135
+ *
1136
+ * @param options - âge maximal et moyens exigés.
1137
+ * @throws TypeError si `maxAge` n'est pas un entier positif ou si un moyen est vide.
1138
+ */
1139
+ function RequireAuthentication(options = {}) {
1140
+ const { maxAge, amr = [] } = options;
1141
+ if (maxAge !== void 0 && (!Number.isInteger(maxAge) || maxAge <= 0)) throw new TypeError(`@RequireAuthentication : maxAge doit être un entier de secondes > 0, reçu ${String(maxAge)}`);
1142
+ if (amr.some((m) => typeof m !== "string" || m.length === 0)) throw new TypeError("@RequireAuthentication : chaque moyen de `amr` est une chaîne non vide (RFC 8176 : pwd, otp, pop)");
1143
+ const requirement = Object.freeze({
1144
+ maxAge: maxAge ?? null,
1145
+ amr: Object.freeze([...amr])
1146
+ });
1147
+ return function(target, propertyKey, descriptor) {
1148
+ if (propertyKey === void 0) {
1149
+ Reflect.defineMetadata(AUTHENTICATION_REQUIREMENT_METADATA, requirement, target);
1150
+ return target;
1151
+ }
1152
+ Reflect.defineMetadata(AUTHENTICATION_REQUIREMENT_METADATA, requirement, target, propertyKey);
1153
+ return descriptor;
1154
+ };
1155
+ }
1156
+ /** Une métadonnée lue par `Reflect` qui a bien la forme d'une exigence, sinon `undefined`. */
1157
+ function asAuthenticationRequirement(value) {
1158
+ return typeof value === "object" && value !== null && "maxAge" in value && "amr" in value && (value.maxAge === null || typeof value.maxAge === "number") && Array.isArray(value.amr) ? {
1159
+ maxAge: value.maxAge,
1160
+ amr: value.amr.filter((m) => typeof m === "string")
1161
+ } : void 0;
1162
+ }
1163
+ /**
1164
+ * Fusionne les `@RequireAuthentication` de classe et de méthode au plus strict,
1165
+ * ou `null` si aucune — `@Anonymous` (méthode) rend l'action publique.
1166
+ */
1167
+ function computeAuthenticationRequirement(ctor, method) {
1168
+ if (Reflect.getMetadata(SECURITY_ANONYMOUS_METADATA, ctor, method) === true) return null;
1169
+ const onMethod = asAuthenticationRequirement(Reflect.getMetadata(AUTHENTICATION_REQUIREMENT_METADATA, ctor.prototype, method));
1170
+ const onClass = Reflect.getMetadata(SECURITY_ANONYMOUS_METADATA, ctor) === true ? void 0 : asAuthenticationRequirement(Reflect.getMetadata(AUTHENTICATION_REQUIREMENT_METADATA, ctor));
1171
+ if (onMethod === void 0) return onClass ?? null;
1172
+ if (onClass === void 0) return onMethod;
1173
+ const ages = [onClass.maxAge, onMethod.maxAge].filter((age) => age !== null);
1174
+ return Object.freeze({
1175
+ maxAge: ages.length > 0 ? Math.min(...ages) : null,
1176
+ amr: Object.freeze([.../* @__PURE__ */ new Set([...onClass.amr, ...onMethod.amr])])
1177
+ });
1178
+ }
1179
+ 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, RequireAuthentication, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, assertRouteSchemaDeclaration, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, getRouteSchema, readInvocationBody, readRouteInput, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
@@ -1,9 +1,8 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
1
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
3
2
  import router_default from "./router.js";
4
3
  import AdminApiController from "../controller/AdminApiController.js";
5
- import { ADMIN_DEFAULT_ROLE, Module, Service, injectable, resolveAdminRole } from "nodefony";
6
- //#region nodefony/service/AdminBroker.ts
4
+ import { ADMIN_DEFAULT_ROLE, Service, injectable, resolveAdminRole } from "nodefony";
5
+ // nodefony/service/AdminBroker.ts
7
6
  const serviceName = "adminBroker";
8
7
  /**
9
8
  * Implémentation du data plane admin (Studio) — collecte les {@link IAdminApi}
@@ -101,7 +100,6 @@ let AdminBroker = class AdminBroker extends Service {
101
100
  return this.get("router");
102
101
  }
103
102
  };
104
- AdminBroker = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], AdminBroker);
103
+ AdminBroker = __decorate([injectable()], AdminBroker);
105
104
  var AdminBroker_default = AdminBroker;
106
- //#endregion
107
105
  export { AdminBroker_default as default };
@@ -2,7 +2,7 @@ import Template from "../src/Template.js";
2
2
  import { extend, injectable } from "nodefony";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { Eta as Eta$1 } from "eta";
5
- //#region nodefony/service/Eta.ts
5
+ // nodefony/service/Eta.ts
6
6
  /**
7
7
  * Options par défaut du moteur Eta.
8
8
  *
@@ -64,5 +64,4 @@ var Eta = class extends Template {
64
64
  }
65
65
  }
66
66
  };
67
- //#endregion
68
67
  export { Eta as default };
@@ -1,7 +1,6 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
3
- import { Module, Service, assertPageQuery, injectable } from "nodefony";
4
- //#region nodefony/service/IdempotencyStore.ts
1
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
2
+ import { Service, assertPageQuery, injectable } from "nodefony";
3
+ // nodefony/service/IdempotencyStore.ts
5
4
  const serviceName = "idempotencyStore";
6
5
  /** Réponses mémorisées : 10 min (un rejeu plausible reste dans cette fenêtre). */
7
6
  const DEFAULT_TTL_MS = 6e5;
@@ -129,7 +128,6 @@ let MemoryIdempotencyStore = class MemoryIdempotencyStore extends Service {
129
128
  }
130
129
  }
131
130
  };
132
- MemoryIdempotencyStore = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], MemoryIdempotencyStore);
131
+ MemoryIdempotencyStore = __decorate([injectable()], MemoryIdempotencyStore);
133
132
  var IdempotencyStore_default = MemoryIdempotencyStore;
134
- //#endregion
135
133
  export { IdempotencyStore_default as default };