@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,5 +1,5 @@
1
1
  import { LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_LAYOUTS, LOGIN_PAGE_SKINS, LOGIN_PAGE_SKIN_SPECS, LOGIN_PAGE_THEME_KEY, OAUTH2_CANCELLED_REASON, isLoginPageSkin, loginPageSkinFile, oauth2AuthorizePath, safeRedirectPath } from "nodefony";
2
- //#region nodefony/src/loginPage.ts
2
+ // nodefony/src/loginPage.ts
3
3
  /**
4
4
  * Mémoïse une ressource chargée de façon asynchrone : le SUCCÈS est gardé, un
5
5
  * ÉCHEC est oublié. Garder la promesse rejetée figerait la panne — la page
@@ -210,7 +210,7 @@ const LOGIN_PAGE_TEMPLATE = `<!doctype html>
210
210
  </p>
211
211
  <ul class="badges">
212
212
  <li class="agentic">Agentic ready</li>
213
- <li>Node.js ≥ 24</li>
213
+ <li>Node.js ≥ 24.14</li>
214
214
  <li>TypeScript strict</li>
215
215
  <li>ESM only</li>
216
216
  <li>Licence Apache 2.0</li>
@@ -427,5 +427,4 @@ const LOGIN_PAGE_TEMPLATE = `<!doctype html>
427
427
  </body>
428
428
  </html>
429
429
  `;
430
- //#endregion
431
430
  export { LOGIN_PAGE_ASSET_FILES, LOGIN_PAGE_EMPTY_NOTICE, LOGIN_PAGE_TEMPLATE, ORGANIZATION_ACCOUNT_LABEL, buildLoginPageView, keepSuccess };
@@ -1,5 +1,5 @@
1
1
  import { readSessionAuthentication } from "nodefony";
2
- //#region nodefony/src/requireAuthentication.ts
2
+ // nodefony/src/requireAuthentication.ts
3
3
  /**
4
4
  * Ce qui manque à une session pour satisfaire un `@RequireAuthentication` —
5
5
  * fonction PURE : la session, l'exigence et l'heure lui sont données.
@@ -32,5 +32,4 @@ function authenticationShortfall(session, requirement, now) {
32
32
  function stepUpChallenge(requirement) {
33
33
  return "Session realm=\"nodefony\", error=\"insufficient_user_authentication\", error_description=\"A more recent or stronger authentication is required\"" + (requirement.maxAge === null ? "" : `, max_age="${requirement.maxAge}"`);
34
34
  }
35
- //#endregion
36
35
  export { authenticationShortfall, stepUpChallenge };
@@ -0,0 +1,81 @@
1
+ import { BootConfigurationError, SchemaValidationError, isStandardSchema, toSchemaValidationIssues } from "nodefony";
2
+ // nodefony/src/routeSchema.ts
3
+ const ROUTE_PARTS = [
4
+ "params",
5
+ "query",
6
+ "body",
7
+ "headers"
8
+ ];
9
+ /** Les noms des parties, pour reconnaître une clé lue sur un objet inconnu. */
10
+ const ROUTE_PART_NAMES = new Set(ROUTE_PARTS);
11
+ /**
12
+ * Valide l'entrée d'une action contre son schéma — synchrone tant que chaque
13
+ * validateur l'est, asynchrone seulement si l'un d'eux rend une promesse.
14
+ *
15
+ * Les quatre parties sont toutes validées avant de conclure : un échec rend
16
+ * en une fois TOUTES les anomalies, chacune rattachée à sa partie. La valeur
17
+ * d'une partie validée est remplacée par la SORTIE de son schéma (valeurs par
18
+ * défaut, coercitions) ; la valeur reçue n'est ni copiée ni sérialisée — des
19
+ * octets décodés par le transport arrivent tels quels au validateur.
20
+ *
21
+ * @param schema - le schéma de la route.
22
+ * @param raw - l'entrée brute, mutée en place (objet propre à la requête).
23
+ * @returns l'entrée validée, ou sa promesse.
24
+ * @throws SchemaValidationError (statut 422) quand une partie est invalide —
25
+ * de façon synchrone, ou par la promesse rendue.
26
+ */
27
+ function validateRouteInput(schema, raw) {
28
+ let failures = null;
29
+ let pending = null;
30
+ for (let i = 0; i < ROUTE_PARTS.length; i++) {
31
+ const part = ROUTE_PARTS[i];
32
+ const partSchema = part === void 0 ? void 0 : schema[part];
33
+ if (part === void 0 || partSchema === void 0) continue;
34
+ const result = partSchema["~standard"].validate(raw[part]);
35
+ if (result instanceof Promise) {
36
+ pending ??= [];
37
+ pending.push(result.then((settled) => failures = applyPartResult(raw, part, i, settled, failures)));
38
+ } else failures = applyPartResult(raw, part, i, result, failures);
39
+ }
40
+ if (pending === null) return concludeRouteInput(raw, failures);
41
+ return Promise.all(pending).then(() => concludeRouteInput(raw, failures));
42
+ }
43
+ /**
44
+ * Applique le résultat d'une partie : sa sortie remplace la valeur brute, ou
45
+ * ses anomalies sont rangées à SON rang — l'erreur les rend dans l'ordre des
46
+ * parties, que leurs validateurs aient attendu ou non.
47
+ */
48
+ function applyPartResult(raw, part, rank, result, failures) {
49
+ if (result.issues === void 0) {
50
+ raw[part] = result.value;
51
+ return failures;
52
+ }
53
+ const ranked = failures ?? [];
54
+ ranked[rank] = toSchemaValidationIssues(result.issues, part);
55
+ return ranked;
56
+ }
57
+ /** Rend l'entrée validée, ou lève l'erreur unique qui porte toutes les anomalies. */
58
+ function concludeRouteInput(raw, failures) {
59
+ if (failures === null) return raw;
60
+ const issues = [];
61
+ for (const partIssues of failures) if (partIssues !== void 0) issues.push(...partIssues);
62
+ throw new SchemaValidationError(issues);
63
+ }
64
+ /**
65
+ * Refuse, au démarrage, un schéma de route mal formé : partie inconnue (une
66
+ * faute de frappe — `bodies` — serait sinon ignorée sans un mot), ou valeur
67
+ * qui n'implémente pas Standard Schema (un appelant JavaScript peut passer
68
+ * n'importe quoi).
69
+ *
70
+ * @param schema - le schéma déclaré.
71
+ * @param routeName - la route, nommée dans l'erreur.
72
+ * @throws BootConfigurationError sur la première faute trouvée.
73
+ */
74
+ function assertRouteSchemaShape(schema, routeName) {
75
+ if (typeof schema !== "object" || schema === null) throw new BootConfigurationError(`Route "${routeName}" : l'option « schema » doit être un objet { params, query, body, headers }.`);
76
+ for (const [part, value] of Object.entries(schema)) {
77
+ if (!ROUTE_PART_NAMES.has(part)) throw new BootConfigurationError(`Route "${routeName}" : partie de schéma inconnue « ${part} » (attendu : ${ROUTE_PARTS.join(", ")}).`);
78
+ if (value !== void 0 && !isStandardSchema(value)) throw new BootConfigurationError(`Route "${routeName}" : « schema.${part} » n'implémente pas Standard Schema (Zod, Valibot, ArkType…).`);
79
+ }
80
+ }
81
+ export { assertRouteSchemaShape, validateRouteInput };
@@ -1,6 +1,6 @@
1
1
  import { extractActionScopes } from "../decorators/routerDecorators.js";
2
2
  import router_default from "../service/router.js";
3
- //#region nodefony/src/scopeCatalog.ts
3
+ // nodefony/src/scopeCatalog.ts
4
4
  /**
5
5
  * P6.8 — **Découverte au boot** : scanne TOUTES les routes montées (`Router.routes`)
6
6
  * et agrège les scopes déclarés par `@RequireScope`, **regroupés par API** (préfixe
@@ -36,5 +36,4 @@ function collectDeclaredApiScopes() {
36
36
  scopes: [...byApi.get(api) ?? []].sort()
37
37
  }));
38
38
  }
39
- //#endregion
40
39
  export { collectDeclaredApiScopes, collectDeclaredApiScopes as default };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/singletonGuard.ts
1
+ // nodefony/src/singletonGuard.ts
2
2
  /**
3
3
  * Filets d'un contrôleur **singleton** — le défaut : UNE instance par classe,
4
4
  * partagée par toutes les requêtes, concurrentes comprises.
@@ -72,5 +72,4 @@ function guardSingletonState(controller, name) {
72
72
  }
73
73
  Object.preventExtensions(controller);
74
74
  }
75
- //#endregion
76
75
  export { guardSingletonState, isDevelopment, singletonWriteMessage };
@@ -1,5 +1,5 @@
1
1
  import { FLOW_STEPS, SEVERITY_NAMES } from "nodefony";
2
- //#region nodefony/src/syslogFilters.ts
2
+ // nodefony/src/syslogFilters.ts
3
3
  /**
4
4
  * Ce que le journal sait TRIER — un seul axe, le temps.
5
5
  *
@@ -47,5 +47,4 @@ const SYSLOG_FILTERS = {
47
47
  /** Borne haute d'horodatage (epoch ms, incluse). */
48
48
  to: "int"
49
49
  };
50
- //#endregion
51
50
  export { SYSLOG_FILTERS, SYSLOG_SORTABLE };
@@ -1,7 +1,7 @@
1
1
  import { Kernel, Module } from "nodefony";
2
2
  import type { IFrameworkConfigInput, IFrameworkConfig } from "./nodefony/config/config.js";
3
3
  import { getIdempotencyStoreFactory, registerIdempotencyStore, listIdempotencyStores } from "./nodefony/src/idempotencyStoreRegistry.js";
4
- import Router from "./nodefony/service/router.js";
4
+ import Router, { FRAMEWORK_ROUTER, isFrameworkRouter } from "./nodefony/service/router.js";
5
5
  import Route from "./nodefony/src/Route.js";
6
6
  import Controller from "./nodefony/src/Controller.js";
7
7
  import ResourceController from "./nodefony/src/ResourceController.js";
@@ -24,9 +24,10 @@ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
24
24
  import { buildPlaygroundSnapshot } from "./nodefony/src/PlaygroundAdminApi.js";
25
25
  import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
26
26
  import Eta from "./nodefony/service/Eta.js";
27
- import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
28
- import { mergeSchemas, makeExecutableSchema } from "@graphql-tools/schema";
29
27
  import { controllers, route, controller, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta } from "./nodefony/decorators/routerDecorators.js";
28
+ export type { IHttpMethodDecorator } from "./nodefony/decorators/routerDecorators.js";
29
+ export { validateRouteInput } from "./nodefony/src/routeSchema.js";
30
+ export type { IRouteSchema, RouteInput, RoutePart, RawRouteInput, RawRouteParams, RawRouteQuery, RawRouteHeaders, } from "./nodefony/src/routeSchema.js";
30
31
  declare module "nodefony" {
31
32
  interface NodefonyModuleConfig {
32
33
  "@nodefony/framework": IFrameworkConfigInput;
@@ -77,14 +78,8 @@ declare class Framework extends Module<IFrameworkConfig> {
77
78
  */
78
79
  onKernelReady(): Promise<this>;
79
80
  }
80
- declare const graphql: {
81
- mergeSchemas: typeof mergeSchemas;
82
- makeExecutableSchema: typeof makeExecutableSchema;
83
- mergeResolvers: typeof mergeResolvers;
84
- mergeTypeDefs: typeof mergeTypeDefs;
85
- };
86
81
  export default Framework;
87
- export { Controller, ResourceController, Route, Router, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, LoginPageController, mountLoginPage, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta, graphql, registerIdempotencyStore, getIdempotencyStoreFactory, listIdempotencyStores, };
82
+ export { Controller, ResourceController, Route, Router, FRAMEWORK_ROUTER, isFrameworkRouter, Resolver, AdminBroker, MemoryIdempotencyStore, AdminApiController, SessionAuthController, mountSessionAuthRoutes, LoginPageController, mountLoginPage, TokenAuthController, mountTokenAuthRoutes, IssuerMetadataController, mountIssuerMetadataRoutes, ProtectedResourceMetadataController, mountProtectedResourceRoutes, protectedResourceRoutePaths, collectProtectedResources, WebAuthnController, mountWebAuthnRoutes, OAuth2Controller, mountOAuth2Routes, BenchController, mountBenchRoutes, ApiKeyController, mountApiKeyRoutes, TotpController, mountTotpRoutes, createKernelAdminApi, createFrameworkAdminApi, createSyslogAdminApi, buildPlaygroundSnapshot, Eta, route, controller, controllers, Get, Post, Put, Delete, Patch, Options, Head, All, Domain, BypassFirewall, IsGranted, RequireScope, RequireAuthentication, Anonymous, Csp, CsrfProtect, CsrfExempt, Idempotent, CurrentUser, Scope, UseSession, HttpCode, Header, Redirect, Param, Body, Query, Headers, Cookie, Session, Req, Res, UploadedFile, UploadedFiles, routeExpectsBodyStream, buildContextParamArgs, getParamArgsMeta, registerIdempotencyStore, getIdempotencyStoreFactory, listIdempotencyStores, };
88
83
  export { frameworkConfigSchema } from "./nodefony/config/config.js";
89
84
  export type { IdempotencyStoreFactory, IIdempotencyStoreFactoryContext, } from "./nodefony/src/idempotencyStoreRegistry.js";
90
85
  export type { SecurityClause, SecurityRequirement, IAuthenticationRequirement, IRequireAuthenticationOptions, CspDirectives, IParamArgSource, ParamMeta, } from "./nodefony/decorators/routerDecorators.js";
@@ -1,5 +1,17 @@
1
1
  import type { ContextType } from "@nodefony/http";
2
2
  import Controller from "../src/Controller.js";
3
+ type AdminQuery = Record<string, string | string[]>;
4
+ /**
5
+ * Ramène les paramètres d'URL au contrat {@link IAdminRequest.query} : seules
6
+ * les chaînes et les tableaux de chaînes passent. `qs` produit des objets pour
7
+ * `a[b]=1` ; les laisser passer sous un type qui les nie tromperait chaque
8
+ * producteur du plan d'administration.
9
+ *
10
+ * @param source - paramètres d'URL de la requête (`Controller.query`)
11
+ * @returns `source` lui-même quand il respecte déjà le contrat (aucune
12
+ * allocation), sinon une copie sans les valeurs imbriquées
13
+ */
14
+ export declare function toAdminQuery(source: Record<string, unknown> | undefined): AdminQuery;
3
15
  /**
4
16
  * Controller pont unique du data plane admin (Studio).
5
17
  *
@@ -91,6 +91,13 @@ declare class SessionAuthController extends SecurityApiController {
91
91
  * Identité de la session courante, re-résolue (rôles frais). Toujours `200` :
92
92
  * `{ user: null }` quand il n'y a pas de session (ou qu'elle ne mène plus à
93
93
  * un compte actif) — l'absence de session est un état, pas une erreur.
94
+ *
95
+ * **Sème aussi le jeton anti-CSRF** (cookie lisible `csrf-token`) : c'est la
96
+ * seule requête sûre que TOUT client connaît d'avance. Sans elle, une mutation
97
+ * `@CsrfProtect` n'a de jeton à rejouer que si son auteur a AUSSI décoré une
98
+ * lecture — protéger les seules écritures, le geste naturel, rendait alors 403
99
+ * sans issue, au navigateur comme aux tests générés. Le jeton est sans état
100
+ * (HMAC) : celui-ci vaut pour toute route protégée.
94
101
  */
95
102
  me(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
96
103
  }
@@ -1,10 +1,12 @@
1
1
  import "reflect-metadata";
2
+ import type { Readable } from "node:stream";
2
3
  import { TypeController } from "../service/router.js";
3
4
  import { RouteOptions } from "../src/Route.js";
4
5
  import Controller from "../src/Controller.js";
5
6
  import type { ControllerScope } from "../src/Controller.js";
6
7
  import { Module } from "nodefony";
7
8
  import { ControllerConstructor } from "../src/Route.js";
9
+ import type { IRouteSchema, RawRouteInput, RouteInput } from "../src/routeSchema.js";
8
10
  import type { SessionIntent } from "@nodefony/http";
9
11
  type Constructor<T = {}> = new (...args: any[]) => T;
10
12
  /**
@@ -66,7 +68,30 @@ declare function controller(prefix: string): <T extends ControllerConstructor &
66
68
  * return this.renderJson({ name });
67
69
  * }
68
70
  */
69
- declare function route(name: string, options: RouteOptions): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
71
+ declare function route(name: string, options: RouteOptions & {
72
+ schema?: undefined;
73
+ bodyStream?: false;
74
+ }): RouteMethodDecorator;
75
+ declare function route<S extends IRouteSchema>(name: string, options: RouteOptionsWithSchema<S> & {
76
+ bodyStream?: false;
77
+ }): SchemaRouteMethodDecorator<S, false>;
78
+ declare function route<S extends IRouteSchema & {
79
+ readonly body?: undefined;
80
+ }>(name: string, options: RouteOptionsWithSchema<S> & {
81
+ bodyStream: true;
82
+ }): SchemaRouteMethodDecorator<S, true>;
83
+ /** Options d'une route qui déclare un schéma. */
84
+ type RouteOptionsWithSchema<S extends IRouteSchema> = RouteOptions & {
85
+ schema: S;
86
+ };
87
+ /** Décorateur de méthode d'une route sans schéma : la signature de l'action est libre. */
88
+ type RouteMethodDecorator = (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
89
+ /**
90
+ * Décorateur d'une action à schéma : la compilation refuse une action dont
91
+ * l'argument ne reçoit pas `RouteInput<S, Stream>` (champ absent du schéma,
92
+ * type divergent, second argument).
93
+ */
94
+ type SchemaRouteMethodDecorator<S extends IRouteSchema, Stream extends boolean> = <T extends (input: RouteInput<S, Stream>) => unknown>(target: object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
70
95
  export declare const HTTP_CODE_METADATA = "route:httpCode";
71
96
  export declare const HEADERS_METADATA = "route:responseHeaders";
72
97
  export declare const REDIRECT_METADATA = "route:redirect";
@@ -158,19 +183,40 @@ export interface IdempotentMeta {
158
183
  readonly required: boolean;
159
184
  }
160
185
  type MethodDecoratorOptions = Omit<RouteOptions, "path" | "method">;
161
- declare const Get: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
162
- declare const Post: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
163
- declare const Put: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
164
- declare const Delete: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
165
- declare const Patch: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
166
- declare const Options: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
167
- declare const Head: (path?: string, options?: MethodDecoratorOptions) => (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
186
+ /**
187
+ * Un décorateur de méthode HTTP (`@Get`, `@Post`…). Sans schéma, la signature
188
+ * de l'action est libre (décorateurs de paramètre) ; avec `schema`, l'action
189
+ * reçoit un argument unique `RouteInput<S>`, vérifié à la compilation.
190
+ */
191
+ export interface IHttpMethodDecorator {
192
+ (path?: string, options?: MethodDecoratorOptions & {
193
+ schema?: undefined;
194
+ bodyStream?: false;
195
+ }): RouteMethodDecorator;
196
+ <S extends IRouteSchema>(path: string, options: MethodDecoratorOptions & {
197
+ schema: S;
198
+ bodyStream?: false;
199
+ }): SchemaRouteMethodDecorator<S, false>;
200
+ <S extends IRouteSchema & {
201
+ readonly body?: undefined;
202
+ }>(path: string, options: MethodDecoratorOptions & {
203
+ schema: S;
204
+ bodyStream: true;
205
+ }): SchemaRouteMethodDecorator<S, true>;
206
+ }
207
+ declare const Get: IHttpMethodDecorator;
208
+ declare const Post: IHttpMethodDecorator;
209
+ declare const Put: IHttpMethodDecorator;
210
+ declare const Delete: IHttpMethodDecorator;
211
+ declare const Patch: IHttpMethodDecorator;
212
+ declare const Options: IHttpMethodDecorator;
213
+ declare const Head: IHttpMethodDecorator;
168
214
  /**
169
215
  * `@All` — route sans restriction de méthode : matche **toutes** les méthodes
170
216
  * HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
171
217
  * `Route.matchRequirements` ne lève jamais 405 sur la méthode.
172
218
  */
173
- declare function All(path?: string, options?: MethodDecoratorOptions): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
219
+ declare const All: IHttpMethodDecorator;
174
220
  /**
175
221
  * Fixe le code de statut HTTP de la réponse d'une action.
176
222
  *
@@ -470,26 +516,18 @@ declare const Query: (key?: string) => (target: object, propertyKey: string, par
470
516
  *
471
517
  * ## Le corps n'est PAS validé — le type écrit ici ne promet rien
472
518
  *
473
- * `@Body()` injecte le corps **tel qu'il a été parsé** : `@Body() dto: CreateOrder`
474
- * compile, mais rien ne garantit qu'un `CreateOrder` soit arrivé. C'est un choix
475
- * assumé, pas un oubli — valider ici ne garderait que la porte HTTP, et devrait
476
- * rester **synchrone** ({@link resolveParamArg} l'est, et le rendre asynchrone
477
- * taxerait toute requête à paramètres décorés).
478
- *
479
- * Où valider, donc :
519
+ * `@Body()` injecte le corps **tel qu'il a été décodé** (objet JSON ou de
520
+ * formulaire, chaîne pour `text/*`, octets `Uint8Array` pour tout autre type) :
521
+ * `@Body() dto: CreateOrder` compile, mais rien ne garantit qu'un `CreateOrder`
522
+ * soit arrivé.
480
523
  *
481
- * - **Une entité** → les hooks `beforeCreate` / `beforeUpdate` d'
482
- * `AbstractCrudService` : ils sont `await`és — donc une règle asynchrone
483
- * (unicité en base) y est possible — et ils gardent REST, WebSocket **et** la
484
- * CLI d'un seul geste. C'est ce que génère `nodefony create entity`.
485
- * - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
486
- * modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
487
- * devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
488
- * quel message, quelle règle.
524
+ * Pour un contrat vérifié, déclarer l'option de route `schema` (ADR-0018) : le
525
+ * corps est validé AVANT l'action — 422 qui nomme chaque champ —, sur HTTP comme
526
+ * sur le pont WebSocket, et l'action le reçoit typé dans `input.body`. Une action
527
+ * à schéma ne prend aucun décorateur de paramètre.
489
528
  *
490
- * Et typer le paramètre avec `z.infer<typeof createXSchema>` plutôt qu'avec
491
- * `Partial<XRow>` : le premier décrit le contrat d'**entrée**, le second promet la
492
- * ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
529
+ * Sans schéma, `schema.parse(body)` en première ligne de l'action reste le geste
530
+ * d'un cas isolé : une `ZodError` qui remonte devient le même **422**.
493
531
  *
494
532
  * @example
495
533
  * ```ts
@@ -524,20 +562,21 @@ export interface IParamArgContext {
524
562
  /**
525
563
  * Query de l'invocation courante quand elle ne vient PAS de l'URL du
526
564
  * transport — pont WS-RPC `api.request` (`Resolver.queryOverride`). Prime
527
- * sur `request.queryGet` pour `@Query` uniquement (`@Req` reste le brut).
565
+ * sur `request.query` pour `@Query` uniquement (`@Req` reste le brut).
528
566
  */
529
567
  queryOverride?: Record<string, unknown> | undefined;
530
568
  request?: {
531
- queryGet?: Record<string, unknown> | undefined;
532
- queryPost?: Record<string, unknown> | undefined;
533
- queryFile?: unknown[] | undefined;
569
+ query?: Record<string, unknown> | undefined;
570
+ /** Corps décodé : objet JSON, champs de formulaire, octets… */
571
+ body?: unknown;
572
+ files?: readonly unknown[] | undefined;
534
573
  headers?: Record<string, unknown> | undefined;
535
574
  /**
536
575
  * P2.9 — `IncomingMessage` brut (un `Readable`) sous-jacent au wrapper
537
576
  * `HttpRequest`. Injecté tel quel par `@Body({ stream: true })` (le pipeline
538
577
  * n'a pas consommé/parsé ce flux). `undefined` pour les contextes WS.
539
578
  */
540
- request?: NodeJS.ReadableStream | undefined;
579
+ request?: Readable | undefined;
541
580
  } | null | undefined;
542
581
  response?: unknown;
543
582
  session?: {
@@ -545,6 +584,31 @@ export interface IParamArgContext {
545
584
  } | null | undefined;
546
585
  getRequestCookies(name?: string): unknown;
547
586
  }
587
+ /**
588
+ * Le corps DÉCODÉ d'une invocation, depuis la requête seule — la règle de
589
+ * {@link readBody}, pour qui n'a pas de contexte de paramètres (porte
590
+ * d'idempotence).
591
+ *
592
+ * @param request - la requête du contexte.
593
+ * @returns le corps posé par le pont WebSocket, sinon le corps HTTP lu.
594
+ */
595
+ export declare function readInvocationBody(request: IParamArgContext["request"]): unknown;
596
+ /**
597
+ * L'entrée BRUTE d'une action à schéma, lue aux MÊMES sources que les
598
+ * décorateurs de paramètre ({@link readQuery}, {@link readBody}) : pont
599
+ * WebSocket compris. Seule allocation de la voie à schéma : cet objet, propre
600
+ * à l'invocation, que la validation complète en place et que l'action reçoit.
601
+ *
602
+ * Arguments POSITIONNELS, pas un objet de contexte : la voie à schéma n'en
603
+ * alloue aucun de plus.
604
+ *
605
+ * @param paramsMap - variables de route nommées.
606
+ * @param request - la requête du contexte.
607
+ * @param queryOverride - query du chemin invoqué (pont WebSocket), sinon rien.
608
+ * @param bodyStream - `true` : `body` est le flux brut, jamais lu.
609
+ * @returns les quatre parties, brutes.
610
+ */
611
+ export declare function readRouteInput(paramsMap: Record<string, unknown>, request: IParamArgContext["request"], queryOverride: Record<string, unknown> | undefined, bodyStream: boolean): RawRouteInput;
548
612
  /**
549
613
  * Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
550
614
  *
@@ -605,6 +669,36 @@ export declare function buildContextParamArgs(metas: readonly ParamMeta[], conte
605
669
  * @returns les métadonnées, ou `null` — une action sans décorateur ne paie rien
606
670
  */
607
671
  export declare function getParamArgsMeta(proto: object, method: string | symbol): readonly ParamMeta[] | null;
672
+ /**
673
+ * Le schéma d'entrée d'une méthode (option de route `schema`), ou `null`.
674
+ * Lecture `Reflect` : à faire une fois, hors du chemin de chaque requête.
675
+ *
676
+ * @param proto - le prototype de la classe.
677
+ * @param method - le nom de la méthode.
678
+ * @returns le schéma, ou `null` — une action sans schéma ne paie rien.
679
+ * @throws BootConfigurationError si le schéma rangé est mal formé.
680
+ */
681
+ export declare function getRouteSchema(proto: object, method: string): IRouteSchema | null;
682
+ /**
683
+ * Refuse, au DÉMARRAGE, une action à schéma mal déclarée — appelé par
684
+ * `Router.setController` pour chaque route du contrôleur, pour qu'une faute
685
+ * ne se révèle pas à la première requête :
686
+ *
687
+ * - schéma mal formé (partie inconnue, valeur qui n'est pas un Standard Schema) ;
688
+ * - schéma ET décorateurs de paramètre (`@Body`…) : une action à schéma reçoit
689
+ * un argument UNIQUE, les deux formes ne se combinent pas ;
690
+ * - `bodyStream: true` ET `schema.body` : un flux ne se valide pas.
691
+ *
692
+ * @param routeDef - la route (nom, contrôleur, méthode).
693
+ * @throws BootConfigurationError qui nomme la route.
694
+ */
695
+ export declare function assertRouteSchemaDeclaration(routeDef: {
696
+ name: string;
697
+ controller?: {
698
+ prototype: object;
699
+ } | null | undefined;
700
+ classMethod?: string | undefined;
701
+ }): void;
608
702
  /**
609
703
  * P2.9 — Indique si l'action d'une route attend le **flux brut** du body
610
704
  * (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
@@ -631,6 +725,13 @@ declare function routeExpectsBodyStream(routeDef: {
631
725
  export interface RouteActionMeta {
632
726
  /** Paramètres décorés (`@Param`/`@Body`/`@Query`…) — `null` si aucun. */
633
727
  paramsMeta: readonly ParamMeta[] | null;
728
+ /**
729
+ * Schéma d'entrée (`schema` de l'option de route) — `null` si l'action n'en
730
+ * déclare pas (cas courant → une comparaison, aucune allocation).
731
+ */
732
+ schema: IRouteSchema | null;
733
+ /** `bodyStream: true` déclaré en option de route. */
734
+ bodyStream: boolean;
634
735
  /** `@Redirect` de l'action — `null` si absent. */
635
736
  redirectMeta: RedirectMeta | null;
636
737
  /** `@HttpCode` de l'action — `null` si absent. */
@@ -10,10 +10,9 @@ export interface IController {
10
10
  context?: ContextType | undefined;
11
11
  session?: Session | null;
12
12
  method?: HTTPMethod | undefined;
13
- queryGet: Record<string, unknown>;
14
13
  query: Record<string, unknown>;
15
- queryFile: unknown[];
16
- queryPost: Record<string, unknown>;
14
+ files: readonly unknown[];
15
+ body: Record<string, unknown>;
17
16
  module?: Module | undefined;
18
17
  setContext(context: ContextType): void;
19
18
  setContextJson(encoding?: BufferEncoding): unknown;
@@ -6,7 +6,16 @@ import type { IRequestRouter } from "@nodefony/http";
6
6
  import Resolver from "../src/Resolver.js";
7
7
  import Controller from "../src/Controller.js";
8
8
  export type TypeController<T> = new (...args: any[]) => T;
9
+ /**
10
+ * Marque du Router de `@nodefony/framework` — ce que {@link isFrameworkRouter}
11
+ * lit. `Symbol.for` : la même marque pour deux copies installées du framework,
12
+ * là où `instanceof` échouerait en silence. Un faux routeur de test s'en
13
+ * réclame explicitement.
14
+ */
15
+ export declare const FRAMEWORK_ROUTER: unique symbol;
9
16
  declare class Router extends Service implements IRequestRouter {
17
+ /** Marque {@link FRAMEWORK_ROUTER} — sur le prototype, aucun champ par instance. */
18
+ get [FRAMEWORK_ROUTER](): true;
10
19
  static routes: Route[];
11
20
  routes: Route[];
12
21
  private singletonControllers;
@@ -55,7 +64,10 @@ declare class Router extends Service implements IRequestRouter {
55
64
  * WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
56
65
  * GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
57
66
  * `undefined` (GET/HTTP) → match historique sur `context.method`.
58
- * @returns un `Resolver` (`.resolve === true` si une route a matché).
67
+ * @returns un `Resolver` (`.resolve === true` si une route a matché) — ou,
68
+ * pour un chemin servi en WebSocket SEULEMENT et demandé en HTTP, un
69
+ * `Resolver` dont `exception` porte le `426` (en-têtes `Upgrade` posés) :
70
+ * le pipeline le lève après le pare-feu.
59
71
  */
60
72
  resolve(context: ContextType, cleanPathOverride?: string, methodOverride?: string): Resolver;
61
73
  resolveController(contex: ContextType, name: string): Resolver;
@@ -89,4 +101,18 @@ declare class Router extends Service implements IRequestRouter {
89
101
  */
90
102
  static getRoutesForController(myconstructor: TypeController<Controller>): Route[];
91
103
  }
104
+ /**
105
+ * Dit si le service `router` d'un contexte est le Router de
106
+ * `@nodefony/framework` — la garde qui rétrécit le contrat `IRequestRouter` de
107
+ * `@nodefony/http` sans conversion, pour qui lit des membres propres au
108
+ * framework (le Resolver complet que rend `resolve`).
109
+ *
110
+ * Lit la marque {@link FRAMEWORK_ROUTER}, jamais `instanceof` : deux copies
111
+ * installées du framework feraient échouer `instanceof` en silence. Une
112
+ * lecture de propriété.
113
+ *
114
+ * @param router - le routeur lu sur le contexte (`context.router`).
115
+ * @returns `true` si c'est le Router du framework.
116
+ */
117
+ export declare function isFrameworkRouter(router: IRequestRouter | null | undefined): router is Router;
92
118
  export default Router;
@@ -105,27 +105,25 @@ declare class Controller extends Service implements IController {
105
105
  get method(): HTTPMethod | undefined;
106
106
  set method(method: HTTPMethod | undefined);
107
107
  /**
108
- * Query string de l'appel en cours — pour un singleton invoqué par le pont
109
- * `api.request`, celle du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas
110
- * celle du handshake de la connexion. Une seule lecture de l'ALS.
108
+ * Paramètres d'URL de l'appel en cours — jamais le corps, qui reste dans
109
+ * {@link body}. Pour un singleton invoqué par le pont `api.request`,
110
+ * ceux du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas ceux du
111
+ * handshake de la connexion. Une seule lecture de l'ALS.
111
112
  */
112
- get queryGet(): Record<string, unknown>;
113
- set queryGet(value: Record<string, unknown>);
114
- /** Comme {@link queryGet}, pour la query fusionnée (`query`). */
115
113
  get query(): Record<string, unknown>;
116
114
  set query(value: Record<string, unknown>);
117
- get queryFile(): unknown[];
118
- set queryFile(value: unknown[]);
119
- get queryPost(): Record<string, unknown>;
120
- set queryPost(value: Record<string, unknown>);
115
+ /**
116
+ * Les fichiers reçus dans un corps multipart (`UploadedFile`) ; les champs
117
+ * texte du même corps sont dans {@link body}. Pour une action typée,
118
+ * préférer `@UploadedFiles()`.
119
+ */
120
+ get files(): readonly unknown[];
121
+ set files(value: readonly unknown[]);
121
122
  /**
122
123
  * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
123
- * Fastify, NestJS), alias de {@link queryPost}.
124
- *
125
- * Ne pas confondre avec {@link query}, qui FUSIONNE la query string et le
126
- * corps. Pour une action typée, préférer le décorateur `@Body()`.
127
- *
128
- * Getter (aucune allocation) : `queryPost` reste la source unique.
124
+ * Fastify, NestJS). Jamais un paramètre d'URL : ceux-là sont dans
125
+ * {@link query}, et les deux ne fusionnent pas. Pour une action typée,
126
+ * préférer le décorateur `@Body()`.
129
127
  */
130
128
  get body(): Record<string, unknown>;
131
129
  set body(value: Record<string, unknown>);
@@ -1,4 +1,5 @@
1
1
  import type { IAdminEndpoint } from "nodefony";
2
+ import type { RoutePart } from "./routeSchema.js";
2
3
  /**
3
4
  * Endpoints « Playground » du data plane framework — la source de données de la
4
5
  * console vivante des controllers dans Studio (`/nodefony/playground`).
@@ -65,7 +66,18 @@ export interface PlaygroundAction {
65
66
  variables: string[];
66
67
  /** Défauts de variables (hors clé interne `controller`). */
67
68
  defaults: Record<string, unknown>;
69
+ /**
70
+ * Entrées de l'action. Pour une action à schéma, déduites du schéma (une
71
+ * entrée par champ de `query`/`headers`/`params`, une pour `body`) : le
72
+ * formulaire se construit pareil, quelle que soit la forme de l'action.
73
+ */
68
74
  params: PlaygroundParam[];
75
+ /**
76
+ * Le contrat d'une action à schéma — JSON Schema d'entrée de chaque partie
77
+ * déclarée (`null` quand la bibliothèque ne sait pas le décrire). `null`
78
+ * pour une action sans schéma.
79
+ */
80
+ schema: Partial<Record<RoutePart, Record<string, unknown> | null>> | null;
69
81
  guards: PlaygroundGuards;
70
82
  }
71
83
  /** Un controller et ses actions, groupés pour la navigation Studio. */