@nodefony/framework 10.0.0-beta.3 → 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 (65) hide show
  1. package/README.md +4 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +9 -18
  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 +38 -7
  7. package/dist/nodefony/controller/ApiKeyController.js +16 -8
  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 +4 -11
  11. package/dist/nodefony/controller/OAuth2Controller.js +3 -4
  12. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
  13. package/dist/nodefony/controller/SessionAuthController.js +13 -4
  14. package/dist/nodefony/controller/TokenAuthController.js +3 -4
  15. package/dist/nodefony/controller/TotpController.js +2 -3
  16. package/dist/nodefony/controller/WebAuthnController.js +2 -3
  17. package/dist/nodefony/controller/oauthAuthority.js +1 -2
  18. package/dist/nodefony/decorators/routerDecorators.js +155 -53
  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 +49 -15
  23. package/dist/nodefony/src/Controller.js +22 -39
  24. package/dist/nodefony/src/FrameworkAdminApi.js +31 -10
  25. package/dist/nodefony/src/KernelAdminApi.js +7 -9
  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 +66 -34
  29. package/dist/nodefony/src/ResourceController.js +2 -3
  30. package/dist/nodefony/src/Route.js +2 -3
  31. package/dist/nodefony/src/SecurityApiController.js +1 -2
  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 +1 -2
  36. package/dist/nodefony/src/idempotency.js +1 -2
  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 +2 -3
  40. package/dist/nodefony/src/requireAuthentication.js +1 -2
  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 +5 -10
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
  47. package/dist/types/nodefony/controller/SessionAuthController.d.ts +7 -0
  48. package/dist/types/nodefony/decorators/routerDecorators.d.ts +133 -32
  49. package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
  50. package/dist/types/nodefony/service/router.d.ts +27 -1
  51. package/dist/types/nodefony/src/Controller.d.ts +14 -16
  52. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  53. package/dist/types/nodefony/src/Resolver.d.ts +25 -1
  54. package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
  55. package/dist/types/nodefony/src/Route.d.ts +3 -0
  56. package/dist/types/nodefony/src/loginPage.d.ts +1 -1
  57. package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
  58. package/docs/admin.md +80 -80
  59. package/docs/controller.md +108 -109
  60. package/docs/decorateurs.md +208 -161
  61. package/docs/idempotence.md +133 -133
  62. package/docs/routing.md +90 -89
  63. package/docs/templates.md +55 -55
  64. package/package.json +8 -11
  65. package/dist/_virtual/_@oxc-project_runtime@0.153.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;
@@ -211,6 +205,10 @@ function extractControllerFilePath(stackTrace) {
211
205
  const HTTP_CODE_METADATA = "route:httpCode";
212
206
  const HEADERS_METADATA = "route:responseHeaders";
213
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";
214
212
  const PARAM_ARGS_METADATA = "route:paramArgs";
215
213
  const DOMAIN_CLASS_METADATA = "route:domainClass";
216
214
  const DOMAIN_METHOD_METADATA = "route:domainMethod";
@@ -232,7 +230,7 @@ function httpMethodDecorator(methods) {
232
230
  const declaredMethods = options.requirements?.methods;
233
231
  const added = declaredMethods === void 0 ? [] : Array.isArray(declaredMethods) ? declaredMethods : [declaredMethods];
234
232
  const fusion = added.length > 0 ? [.../* @__PURE__ */ new Set([...methods, ...added])] : methods;
235
- return route(name, {
233
+ return defineRoute(name, {
236
234
  ...options,
237
235
  path,
238
236
  requirements: {
@@ -255,14 +253,14 @@ const Head = httpMethodDecorator(["HEAD"]);
255
253
  * HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
256
254
  * `Route.matchRequirements` ne lève jamais 405 sur la méthode.
257
255
  */
258
- function All(path = "", options = {}) {
256
+ const All = function(path = "", options = {}) {
259
257
  return function(target, propertyKey, descriptor) {
260
- return route(`${target.constructor.name}::${propertyKey}`, {
258
+ return defineRoute(`${target.constructor.name}::${propertyKey}`, {
261
259
  ...options,
262
260
  path
263
261
  })(target, propertyKey, descriptor);
264
262
  };
265
- }
263
+ };
266
264
  /**
267
265
  * Fixe le code de statut HTTP de la réponse d'une action.
268
266
  *
@@ -722,26 +720,18 @@ const Query = paramDecoratorFactory("query");
722
720
  *
723
721
  * ## Le corps n'est PAS validé — le type écrit ici ne promet rien
724
722
  *
725
- * `@Body()` injecte le corps **tel qu'il a été parsé** : `@Body() dto: CreateOrder`
726
- * compile, mais rien ne garantit qu'un `CreateOrder` soit arrivé. C'est un choix
727
- * assumé, pas un oubli — valider ici ne garderait que la porte HTTP, et devrait
728
- * rester **synchrone** ({@link resolveParamArg} l'est, et le rendre asynchrone
729
- * taxerait toute requête à paramètres décorés).
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é.
730
727
  *
731
- * Où valider, donc :
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.
732
732
  *
733
- * - **Une entité** → les hooks `beforeCreate` / `beforeUpdate` d'
734
- * `AbstractCrudService` : ils sont `await`és — donc une règle asynchrone
735
- * (unicité en base) y est possible — et ils gardent REST, WebSocket **et** la
736
- * CLI d'un seul geste. C'est ce que génère `nodefony create entity`.
737
- * - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
738
- * modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
739
- * devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
740
- * quel message, quelle règle.
741
- *
742
- * Et typer le paramètre avec `z.infer<typeof createXSchema>` plutôt qu'avec
743
- * `Partial<XRow>` : le premier décrit le contrat d'**entrée**, le second promet la
744
- * 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**.
745
735
  *
746
736
  * @example
747
737
  * ```ts
@@ -778,6 +768,75 @@ const Res = paramDecoratorFactory("res");
778
768
  const UploadedFile = paramDecoratorFactory("file");
779
769
  const UploadedFiles = paramDecoratorFactory("files");
780
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
+ /**
781
840
  * Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
782
841
  *
783
842
  * @param meta - métadonnée posée par le décorateur (source + clé optionnelle)
@@ -788,15 +847,13 @@ function resolveParamArg(meta, ctx) {
788
847
  switch (meta.source) {
789
848
  case "param": return meta.key !== void 0 ? ctx.paramsMap[meta.key] : ctx.paramsMap;
790
849
  case "query": {
791
- const qg = ctx.queryOverride ?? ctx.request?.queryGet;
792
- return meta.key !== void 0 ? qg?.[meta.key] : qg;
850
+ const q = readQuery(ctx);
851
+ return meta.key !== void 0 ? q?.[meta.key] : q;
793
852
  }
794
853
  case "body": {
795
854
  if (meta.stream) return ctx.request?.request;
796
- const alsBody = RequestContext.get()?.body;
797
- if (alsBody !== void 0) return meta.key !== void 0 ? alsBody?.[meta.key] : alsBody;
798
- const qp = ctx.request?.queryPost;
799
- 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;
800
857
  }
801
858
  case "headers": {
802
859
  const h = ctx.request?.headers;
@@ -806,8 +863,8 @@ function resolveParamArg(meta, ctx) {
806
863
  case "session": return meta.key !== void 0 ? ctx.session?.get(meta.key) : ctx.session;
807
864
  case "req": return ctx.request;
808
865
  case "res": return ctx.response;
809
- case "file": return ctx.request?.queryFile?.[0];
810
- case "files": return ctx.request?.queryFile;
866
+ case "file": return ctx.request?.files?.[0];
867
+ case "files": return ctx.request?.files;
811
868
  case "user": return RequestContext.isAnonymous() ? void 0 : RequestContext.getUser();
812
869
  default: return;
813
870
  }
@@ -866,6 +923,48 @@ function getParamArgsMeta(proto, method) {
866
923
  return metas !== void 0 && metas.length > 0 ? metas : null;
867
924
  }
868
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
+ /**
869
968
  * P2.9 — Indique si l'action d'une route attend le **flux brut** du body
870
969
  * (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
871
970
  * `route.bodyStream` : lecture `Reflect` au 1er appel, O(1) ensuite → 0 coût
@@ -880,13 +979,15 @@ function routeExpectsBodyStream(routeDef) {
880
979
  let flag = false;
881
980
  const ctor = routeDef.controller;
882
981
  const method = routeDef.classMethod;
883
- 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);
884
983
  routeDef.bodyStream = flag;
885
984
  }
886
985
  return routeDef.bodyStream;
887
986
  }
888
987
  const EMPTY_ACTION_META = {
889
988
  paramsMeta: null,
989
+ schema: null,
990
+ bodyStream: false,
890
991
  redirectMeta: null,
891
992
  httpCode: null,
892
993
  headerEntries: null,
@@ -988,6 +1089,8 @@ function computeActionMeta(ctor, method) {
988
1089
  const headers = Reflect.getMetadata(HEADERS_METADATA, proto, method);
989
1090
  return {
990
1091
  paramsMeta: getParamArgsMeta(proto, method),
1092
+ schema: getRouteSchema(proto, method),
1093
+ bodyStream: Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true,
991
1094
  redirectMeta: redirect ?? null,
992
1095
  httpCode: httpCode ?? null,
993
1096
  headerEntries: headers ? Object.entries(headers) : null,
@@ -1073,5 +1176,4 @@ function computeAuthenticationRequirement(ctor, method) {
1073
1176
  amr: Object.freeze([.../* @__PURE__ */ new Set([...onClass.amr, ...onMethod.amr])])
1074
1177
  });
1075
1178
  }
1076
- //#endregion
1077
- 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, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
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.153.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.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.153.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.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 };
@@ -1,13 +1,12 @@
1
1
  import Route from "../src/Route.js";
2
- import { routeExpectsBodyStream } from "../decorators/routerDecorators.js";
2
+ import { assertRouteSchemaDeclaration, routeExpectsBodyStream } from "../decorators/routerDecorators.js";
3
3
  import Resolver from "../src/Resolver.js";
4
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
5
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
6
- import { Injector, Module, Service, injectable, isPromise, stripTrailingSlashes } from "nodefony";
4
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
5
+ import { BootConfigurationError, Injector, Module, Service, injectable, isPromise, stripTrailingSlashes } from "nodefony";
7
6
  import { HttpError, isDomainAllowed } from "@nodefony/http";
8
- //#region nodefony/service/router.ts
7
+ // nodefony/service/router.ts
9
8
  var _Router;
10
- let routeNoticePromoted = null;
9
+ let routeMatchLogged = null;
11
10
  /**
12
11
  * Méthodes que sert une route — lues sur le `methodsSet` COMPILÉ par
13
12
  * `Route.compileRequirements`, jamais recalculées depuis la config brute.
@@ -92,10 +91,21 @@ function indexRoute(route, i, statics, dynamics) {
92
91
  prefix: literalPrefix(path)
93
92
  });
94
93
  }
94
+ /**
95
+ * Marque du Router de `@nodefony/framework` — ce que {@link isFrameworkRouter}
96
+ * lit. `Symbol.for` : la même marque pour deux copies installées du framework,
97
+ * là où `instanceof` échouerait en silence. Un faux routeur de test s'en
98
+ * réclame explicitement.
99
+ */
100
+ const FRAMEWORK_ROUTER = Symbol.for("nodefony.framework.router");
95
101
  let Router = class Router extends Service {
96
102
  static {
97
103
  _Router = this;
98
104
  }
105
+ /** Marque {@link FRAMEWORK_ROUTER} — sur le prototype, aucun champ par instance. */
106
+ get [FRAMEWORK_ROUTER]() {
107
+ return true;
108
+ }
99
109
  static routes = routes;
100
110
  routes = _Router.routes;
101
111
  singletonControllers = null;
@@ -167,7 +177,10 @@ let Router = class Router extends Service {
167
177
  * WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
168
178
  * GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
169
179
  * `undefined` (GET/HTTP) → match historique sur `context.method`.
170
- * @returns un `Resolver` (`.resolve === true` si une route a matché).
180
+ * @returns un `Resolver` (`.resolve === true` si une route a matché) — ou,
181
+ * pour un chemin servi en WebSocket SEULEMENT et demandé en HTTP, un
182
+ * `Resolver` dont `exception` porte le `426` (en-têtes `Upgrade` posés) :
183
+ * le pipeline le lève après le pare-feu.
171
184
  */
172
185
  resolve(context, cleanPathOverride, methodOverride) {
173
186
  const resolver = new Resolver(context);
@@ -200,8 +213,8 @@ let Router = class Router extends Service {
200
213
  }
201
214
  try {
202
215
  if (resolver.match(route, context, cleanPath, fromLiterals)) {
203
- routeNoticePromoted ??= this.kernel?.environment !== "production";
204
- if (routeNoticePromoted) this.log(`Match route : ${route.name}`, "NOTICE");
216
+ routeMatchLogged ??= this.kernel?.environment !== "production";
217
+ if (routeMatchLogged) this.log(`Match route : ${route.name}`, "DEBUG");
205
218
  resolver.exception = void 0;
206
219
  routeExpectsBodyStream(route);
207
220
  return resolver;
@@ -221,11 +234,13 @@ let Router = class Router extends Service {
221
234
  }
222
235
  const servesWebsocket = allowed.delete("WEBSOCKET");
223
236
  if (allowed.size === 0 && servesWebsocket) {
224
- context.response?.setHeaders({
237
+ const req = context.request;
238
+ if ((req && "request" in req ? req.request : req)?.httpVersionMajor !== 2) context.response?.setHeaders({
225
239
  Upgrade: "websocket",
226
240
  Connection: "Upgrade"
227
241
  });
228
- throw new HttpError("Upgrade Required: websocket", 426, context);
242
+ resolver.exception = new HttpError("Upgrade Required: websocket", 426, context);
243
+ return resolver;
229
244
  }
230
245
  if (allowed.size > 0) {
231
246
  const allowHeader = Array.from(allowed).join(", ");
@@ -276,6 +291,7 @@ let Router = class Router extends Service {
276
291
  }
277
292
  }
278
293
  static createRoute(name, obj) {
294
+ if ("schema" in obj || "bodyStream" in obj) throw new BootConfigurationError(`Route "${name}" : « schema » et « bodyStream » se déclarent par un décorateur de route (@Get, @Post, @route…), pas par createRoute.`);
279
295
  const routenew = new Route(name, obj);
280
296
  routes.push(routenew);
281
297
  invalidateRouteIndex();
@@ -307,7 +323,10 @@ let Router = class Router extends Service {
307
323
  });
308
324
  const key = `${module.name}:${myconstructor.name}`;
309
325
  if (Module.controllers[key]) module.log(/* @__PURE__ */ new Error(`Controller already exist ${key}`), "WARNING");
310
- for (const r of routes) if (r.controller === myconstructor) r.module = { name: module.name };
326
+ for (const r of routes) if (r.controller === myconstructor) {
327
+ assertRouteSchemaDeclaration(r);
328
+ r.module = { name: module.name };
329
+ }
311
330
  return Module.controllers[key] = myconstructor;
312
331
  }
313
332
  /**
@@ -319,7 +338,22 @@ let Router = class Router extends Service {
319
338
  return routes.filter((r) => r.controller === myconstructor);
320
339
  }
321
340
  };
322
- Router = _Router = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], Router);
341
+ Router = _Router = __decorate([injectable()], Router);
342
+ /**
343
+ * Dit si le service `router` d'un contexte est le Router de
344
+ * `@nodefony/framework` — la garde qui rétrécit le contrat `IRequestRouter` de
345
+ * `@nodefony/http` sans conversion, pour qui lit des membres propres au
346
+ * framework (le Resolver complet que rend `resolve`).
347
+ *
348
+ * Lit la marque {@link FRAMEWORK_ROUTER}, jamais `instanceof` : deux copies
349
+ * installées du framework feraient échouer `instanceof` en silence. Une
350
+ * lecture de propriété.
351
+ *
352
+ * @param router - le routeur lu sur le contexte (`context.router`).
353
+ * @returns `true` si c'est le Router du framework.
354
+ */
355
+ function isFrameworkRouter(router) {
356
+ return router != null && FRAMEWORK_ROUTER in router;
357
+ }
323
358
  var router_default = Router;
324
- //#endregion
325
- export { router_default as default };
359
+ export { FRAMEWORK_ROUTER, router_default as default, isFrameworkRouter };
@@ -3,7 +3,7 @@ import { FileClass, RequestContext, Service } from "nodefony";
3
3
  import { HttpContext, HttpError, acceptsEventStream } from "@nodefony/http";
4
4
  import fs, { createReadStream } from "node:fs";
5
5
  import { promisify } from "node:util";
6
- //#region nodefony/src/Controller.ts
6
+ // nodefony/src/Controller.ts
7
7
  const fsClose = promisify(fs.close);
8
8
  /**
9
9
  * Un `<script>` EN LIGNE qui ne porte pas d'attribut `nonce`.
@@ -108,10 +108,9 @@ var Controller = class extends Service {
108
108
  #request = null;
109
109
  #response = null;
110
110
  #method = null;
111
- #queryGet = null;
112
111
  #query = null;
113
- #queryFile = null;
114
- #queryPost = null;
112
+ #files = null;
113
+ #body = null;
115
114
  module;
116
115
  template;
117
116
  /**
@@ -184,21 +183,11 @@ var Controller = class extends Service {
184
183
  this.#method = method ?? null;
185
184
  }
186
185
  /**
187
- * Query string de l'appel en cours — pour un singleton invoqué par le pont
188
- * `api.request`, celle du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas
189
- * celle du handshake de la connexion. Une seule lecture de l'ALS.
186
+ * Paramètres d'URL de l'appel en cours — jamais le corps, qui reste dans
187
+ * {@link body}. Pour un singleton invoqué par le pont `api.request`,
188
+ * ceux du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas ceux du
189
+ * handshake de la connexion. Une seule lecture de l'ALS.
190
190
  */
191
- get queryGet() {
192
- if (this.#queryGet !== null) return this.#queryGet;
193
- if (this.#context !== null) return this.#context.request?.queryGet;
194
- const store = RequestContext.get();
195
- return (store?.resolver)?.queryOverride ?? ((store?.context)?.request)?.queryGet;
196
- }
197
- set queryGet(value) {
198
- this.#assertPerRequest("queryGet");
199
- this.#queryGet = value;
200
- }
201
- /** Comme {@link queryGet}, pour la query fusionnée (`query`). */
202
191
  get query() {
203
192
  if (this.#query !== null) return this.#query;
204
193
  if (this.#context !== null) return this.#context.request?.query;
@@ -209,35 +198,30 @@ var Controller = class extends Service {
209
198
  this.#assertPerRequest("query");
210
199
  this.#query = value;
211
200
  }
212
- get queryFile() {
213
- return this.#queryFile ?? (this.context?.request)?.queryFile;
214
- }
215
- set queryFile(value) {
216
- this.#assertPerRequest("queryFile");
217
- this.#queryFile = value;
218
- }
219
- get queryPost() {
220
- return this.#queryPost ?? (this.context?.request)?.queryPost;
201
+ /**
202
+ * Les fichiers reçus dans un corps multipart (`UploadedFile`) ; les champs
203
+ * texte du même corps sont dans {@link body}. Pour une action typée,
204
+ * préférer `@UploadedFiles()`.
205
+ */
206
+ get files() {
207
+ return this.#files ?? (this.context?.request)?.files;
221
208
  }
222
- set queryPost(value) {
223
- this.#assertPerRequest("queryPost");
224
- this.#queryPost = value;
209
+ set files(value) {
210
+ this.#assertPerRequest("files");
211
+ this.#files = value;
225
212
  }
226
213
  /**
227
214
  * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
228
- * Fastify, NestJS), alias de {@link queryPost}.
229
- *
230
- * Ne pas confondre avec {@link query}, qui FUSIONNE la query string et le
231
- * corps. Pour une action typée, préférer le décorateur `@Body()`.
232
- *
233
- * Getter (aucune allocation) : `queryPost` reste la source unique.
215
+ * Fastify, NestJS). Jamais un paramètre d'URL : ceux-là sont dans
216
+ * {@link query}, et les deux ne fusionnent pas. Pour une action typée,
217
+ * préférer le décorateur `@Body()`.
234
218
  */
235
219
  get body() {
236
- return this.queryPost;
220
+ return this.#body ?? (this.context?.request)?.body;
237
221
  }
238
222
  set body(value) {
239
223
  this.#assertPerRequest("body");
240
- this.queryPost = value;
224
+ this.#body = value;
241
225
  }
242
226
  /**
243
227
  * Session courante, ou `null`. Getter direct sur `context.session` (peuplé au
@@ -662,5 +646,4 @@ var Controller = class extends Service {
662
646
  return this.streamFile(File, head, value);
663
647
  }
664
648
  };
665
- //#endregion
666
649
  export { Controller as default, parseByteRange };