@nodefony/framework 10.0.0-beta.3 → 10.0.0-beta.5

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 (66) hide show
  1. package/README.md +19 -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 +39 -8
  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 +32 -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 +68 -33
  29. package/dist/nodefony/src/ResourceController.js +13 -6
  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 +7 -2
  45. package/dist/types/index.d.ts +5 -10
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +13 -1
  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 +23 -17
  52. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  53. package/dist/types/nodefony/src/Resolver.d.ts +33 -1
  54. package/dist/types/nodefony/src/ResourceController.d.ts +12 -4
  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/dist/types/nodefony/src/syslogFilters.d.ts +6 -0
  59. package/docs/admin.md +131 -82
  60. package/docs/controller.md +109 -110
  61. package/docs/decorateurs.md +214 -161
  62. package/docs/idempotence.md +154 -136
  63. package/docs/routing.md +105 -90
  64. package/docs/templates.md +55 -55
  65. package/package.json +8 -11
  66. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,4 +1,4 @@
1
- import { Service, Module, FileClass } from "nodefony";
1
+ import { Service, Module, type Pci, type Severity, type Msgid, type Message, FileClass } from "nodefony";
2
2
  import type { IRequestUser, MaybePromise } from "nodefony";
3
3
  import type { IController } from "../interfaces/index.js";
4
4
  import Route from "./Route.js";
@@ -83,6 +83,14 @@ declare class Controller extends Service implements IController {
83
83
  */
84
84
  get context(): ContextType | undefined;
85
85
  set context(context: ContextType | undefined);
86
+ /**
87
+ * Journalise au nom du module du contrôleur, pas du noyau : `moduleName`
88
+ * explicite, sinon le module posé sur la requête (`context.logModule`),
89
+ * sinon le module propriétaire du contrôleur.
90
+ *
91
+ * @param moduleName - module qui signe CETTE ligne
92
+ */
93
+ log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message, moduleName?: string): import("nodefony").Pdu;
86
94
  /**
87
95
  * Route de l'appel en cours. Par requête : posée par le Resolver
88
96
  * (`setRoute`). Singleton : celle du résolveur de l'APPEL quand le pont
@@ -105,27 +113,25 @@ declare class Controller extends Service implements IController {
105
113
  get method(): HTTPMethod | undefined;
106
114
  set method(method: HTTPMethod | undefined);
107
115
  /**
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.
116
+ * Paramètres d'URL de l'appel en cours — jamais le corps, qui reste dans
117
+ * {@link body}. Pour un singleton invoqué par le pont `api.request`,
118
+ * ceux du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas ceux du
119
+ * handshake de la connexion. Une seule lecture de l'ALS.
111
120
  */
112
- get queryGet(): Record<string, unknown>;
113
- set queryGet(value: Record<string, unknown>);
114
- /** Comme {@link queryGet}, pour la query fusionnée (`query`). */
115
121
  get query(): Record<string, unknown>;
116
122
  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>);
123
+ /**
124
+ * Les fichiers reçus dans un corps multipart (`UploadedFile`) ; les champs
125
+ * texte du même corps sont dans {@link body}. Pour une action typée,
126
+ * préférer `@UploadedFiles()`.
127
+ */
128
+ get files(): readonly unknown[];
129
+ set files(value: readonly unknown[]);
121
130
  /**
122
131
  * 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.
132
+ * Fastify, NestJS). Jamais un paramètre d'URL : ceux-là sont dans
133
+ * {@link query}, et les deux ne fusionnent pas. Pour une action typée,
134
+ * préférer le décorateur `@Body()`.
129
135
  */
130
136
  get body(): Record<string, unknown>;
131
137
  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. */
@@ -37,12 +37,20 @@ declare class Resolver implements IResolver {
37
37
  resolve: boolean;
38
38
  variables: unknown[];
39
39
  exception?: HttpError | Error | null | undefined;
40
+ /**
41
+ * Ce que l'action a rendu quand le Resolver n'a rien pu envoyer — absent
42
+ * sinon. Lu par l'avertissement de développement
43
+ * (`IRouteResolver.unrendered`). `declare` : AUCUN champ émis — un Resolver
44
+ * naît à chaque requête, et la propriété n'apparaît que sur la branche qui
45
+ * attend déjà son délai d'expiration (`micro-resolver-field.mjs`).
46
+ */
47
+ unrendered?: string;
40
48
  acceptedProtocol: string | null;
41
49
  bypassFirewall: boolean;
42
50
  /**
43
51
  * Query d'une invocation **par message** (pont WS-RPC `api.request`) — pendant
44
52
  * de `cleanPathOverride` pour le `?…` du path invoqué. Le contexte WS étant
45
- * PARTAGÉ par la connexion (sa `queryGet` = celle du handshake), la query
53
+ * PARTAGÉ par la connexion (sa `query` = celle du handshake), la query
46
54
  * per-invocation vit ici (le Resolver est per-invocation → zéro bleed entre
47
55
  * requêtes concurrentes d'une même socket). `null` (hot path HTTP) = ignoré.
48
56
  * Consommé par `@Query` (`_buildParamArgs`) et copié sur le controller
@@ -70,6 +78,11 @@ declare class Resolver implements IResolver {
70
78
  * administrateurs.
71
79
  */
72
80
  messageInvocation: boolean;
81
+ /**
82
+ * Les gardes de l'action ont été jugées favorables par {@link authorizeAction}
83
+ * et ne se rejugent pas à la prochaine exécution — consommé par elle.
84
+ */
85
+ private preAuthorized;
73
86
  constructor(context: ContextType);
74
87
  match(route: Route, context: ContextType, cleanPath?: string, literal?: boolean): ((string | null)[] & Record<string, unknown>) | null | undefined;
75
88
  /**
@@ -169,6 +182,25 @@ declare class Resolver implements IResolver {
169
182
  * d'action) ; un refus d'autorisation rompt la promesse rendue.
170
183
  */
171
184
  executeAction(data?: unknown[], reload?: boolean, metaArg?: RouteActionMeta): MaybePromise<ActionOutcome>;
185
+ /**
186
+ * Valide l'entrée d'une action à schéma, puis l'exécute avec cette entrée
187
+ * pour argument unique. Synchrone tant que le validateur l'est.
188
+ *
189
+ * @throws SchemaValidationError (422) quand l'entrée viole le schéma.
190
+ */
191
+ private _validateAndInvoke;
192
+ /**
193
+ * Juge les gardes de l'action résolue SANS l'exécuter — appelé par le
194
+ * pipeline WebSocket AVANT d'accepter le handshake (#596), pour qu'un
195
+ * connecté sans le rôle reçoive son 403 en statut HTTP au lieu d'une
196
+ * ouverture suivie d'une fermeture. Un verdict favorable vaut pour la
197
+ * PROCHAINE exécution (l'action de handshake), qui ne le rejuge pas ; les
198
+ * messages suivants, eux, sont jugés chacun.
199
+ *
200
+ * @returns `undefined` quand l'action n'est pas gardée ; sinon la promesse du
201
+ * verdict, rompue par un refus
202
+ */
203
+ authorizeAction(): Promise<void> | undefined;
172
204
  /**
173
205
  * Les gardes d'autorisation d'une action : `@IsGranted`/`@RequireScope`, puis
174
206
  * le filet de la ZONE quand aucune garde n'a décidé du RÔLE.
@@ -67,7 +67,7 @@ export interface IResourceService<T = unknown> {
67
67
  delete?(criteria: Record<string, unknown>): Promise<number> | number;
68
68
  }
69
69
  /**
70
- * Controller de ressource **souverain** (V4.2 — POC API souveraine, Phase 2) :
70
+ * Controller de ressource **duplex** (V4.2 — une action, deux transports) :
71
71
  * la logique métier est écrite UNE fois dans le service de ressource, les
72
72
  * actions de la sous-classe ne sont que des portes (REST, WS-RPC `invoke`,
73
73
  * GraphQL à venir) qui retournent la **valeur brute** — `returnController`
@@ -86,7 +86,7 @@ export interface IResourceService<T = unknown> {
86
86
  * sur `this` doit rétrograder : `static scope = "request"`.
87
87
  *
88
88
  * Sécurité : aucun critère de requête n'est passé AUTOMATIQUEMENT au service
89
- * (pas de `find(this.queryGet)` implicite) — exposer un filtrage est une
89
+ * (pas de `find(this.query)` implicite) — exposer un filtrage est une
90
90
  * décision EXPLICITE de la sous-classe (deny-by-default ; le scope de
91
91
  * sécurité des données se branche au niveau service/criteria, P6).
92
92
  *
@@ -102,8 +102,12 @@ export interface IResourceService<T = unknown> {
102
102
  * return this.listResource();
103
103
  * }
104
104
  * \@route("books-get", { path: "/{id}", requirements: { methods: ["GET", "WEBSOCKET"] } })
105
- * detail(\@Param("id") id: string) {
106
- * return this.getResource(id);
105
+ * async detail(\@Param("id") id: string) {
106
+ * // `null` rendu tel quel = « l'action répond elle-même » : la requête
107
+ * // pendrait jusqu'au délai d'expiration. La porte décide du 404.
108
+ * const found = await this.getResource(id);
109
+ * if (!found) throw new HttpError(`Book ${id} not found`, 404);
110
+ * return found;
107
111
  * }
108
112
  * }
109
113
  * ```
@@ -155,6 +159,10 @@ declare class ResourceController<T = unknown> extends Controller {
155
159
  /**
156
160
  * Lit une entité par id — `null` si absente (la porte décide du 404).
157
161
  *
162
+ * ⚠️ Ne pas rendre ce `null` tel quel depuis une action : pour le Resolver,
163
+ * `null` veut dire « l'action répond elle-même », et la requête attend
164
+ * alors son délai d'expiration. Lever un `HttpError` 404.
165
+ *
158
166
  * `options.relations` charge les associations dans la foulée. La porte doit
159
167
  * n'y laisser passer que des relations qu'elle a DÉCLARÉES : un `include`
160
168
  * libre laisse le client nommer n'importe quelle association, donc lire des
@@ -2,10 +2,13 @@ import { HTTPMethod, SchemeType, ContextType } from "@nodefony/http";
2
2
  import type { IRoute } from "../interfaces/index.js";
3
3
  import type { RouteActionMeta } from "../decorators/routerDecorators.js";
4
4
  import Controller from "./Controller.js";
5
+ import type { ControllerScope } from "./Controller.js";
5
6
  export interface ControllerConstructor {
6
7
  new (...args: any[]): Controller;
7
8
  /** Prototype de la classe — porte les métadonnées des actions (`@Param`…). */
8
9
  readonly prototype: Controller;
10
+ /** Portée de l'instance (`Controller.scope`, statique hérité). */
11
+ readonly scope?: ControllerScope;
9
12
  }
10
13
  /**
11
14
  * Options pour la configuration d'une route.
@@ -141,4 +141,4 @@ export declare function buildLoginPageView(page: ILoginPageDescription, request:
141
141
  * d'empreinte (`i-finger`) reprend `IconFingerprint` de Tabler Icons (MIT),
142
142
  * celle du bouton de Studio : les deux écrans montrent le même geste.
143
143
  */
144
- export declare const LOGIN_PAGE_TEMPLATE = "<!doctype html>\n<html lang=\"fr\"<% if (it.theme) { %> data-theme=\"<%= it.theme %>\"<% } %>>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark light\" />\n <meta name=\"robots\" content=\"noindex\" />\n <title><%= it.title %> \u2014 <%= it.brand %></title>\n <link rel=\"icon\" href=\"<%= it.logoHref %>\" type=\"image/svg+xml\" />\n<% if (!it.theme) { %>\n <script nonce=\"<%= it.nonce %>\">try{var t=localStorage.getItem(\"nf-login-theme\");if(t===\"light\"||t===\"dark\")document.documentElement.dataset.theme=t}catch(e){}</script>\n<% } %>\n <link rel=\"stylesheet\" href=\"<%= it.styleHref %>\" />\n<% if (it.skinHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.skinHref %>\" />\n<% } %>\n<% if (it.stylesheetHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.stylesheetHref %>\" />\n<% } %>\n <script type=\"module\" src=\"<%= it.scriptHref %>\" nonce=\"<%= it.nonce %>\"></script>\n </head>\n <body data-skin=\"<%= it.skin %>\" data-layout=\"<%= it.layout %>\"<% if (it.backdrop) { %> data-backdrop<% } %><% if (it.ssoFirst) { %> data-sso-first<% } %>>\n <svg width=\"0\" height=\"0\" style=\"position: absolute\" aria-hidden=\"true\">\n <symbol id=\"i-user\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"8\" r=\"3.5\" /><path d=\"M5.5 20a6.5 6.5 0 0 1 13 0\" /></symbol>\n <symbol id=\"i-lock\" viewBox=\"0 0 24 24\"><rect x=\"5\" y=\"10.5\" width=\"14\" height=\"10\" rx=\"2\" /><path d=\"M8.5 10.5V7.5a3.5 3.5 0 0 1 7 0v3\" /></symbol>\n <symbol id=\"i-eye\" viewBox=\"0 0 24 24\"><path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\" /><circle cx=\"12\" cy=\"12\" r=\"2.8\" /></symbol>\n <symbol id=\"i-arrow\" viewBox=\"0 0 24 24\"><path d=\"M5 12h14M13 6l6 6-6 6\" /></symbol>\n <symbol id=\"i-key\" viewBox=\"0 0 24 24\"><circle cx=\"8\" cy=\"15\" r=\"3.8\" /><path d=\"M10.8 12.2 19.5 3.5M16.5 6.5l2.5 2.5M14 9l2 2\" /></symbol>\n <symbol id=\"i-check\" viewBox=\"0 0 24 24\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></symbol>\n <symbol id=\"i-finger\" viewBox=\"0 0 24 24\"><path d=\"M18.9 7a8 8 0 0 1 1.1 5v1a6 6 0 0 0 .8 3\" /><path d=\"M8 11a4 4 0 0 1 8 0v1a10 10 0 0 0 2 6\" /><path d=\"M12 11v2a14 14 0 0 0 2.5 8\" /><path d=\"M8 15a18 18 0 0 0 1.8 6\" /><path d=\"M4.9 19a22 22 0 0 1-.9-7v-1a8 8 0 0 1 12-6.95\" /></symbol>\n <symbol id=\"i-sun\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4\" /></symbol>\n <symbol id=\"i-moon\" viewBox=\"0 0 24 24\"><path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" /></symbol>\n <symbol id=\"i-shield\" viewBox=\"0 0 24 24\"><path d=\"M12 3l7.5 3v5.5c0 4.6-3.2 8-7.5 9.5-4.3-1.5-7.5-4.9-7.5-9.5V6z\" /></symbol>\n <symbol id=\"m-github\" viewBox=\"0 0 16 16\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" /></symbol>\n </svg>\n <header class=\"nf-strip\">\n <div class=\"nf-strip-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n </header>\n <div class=\"nf-layout\">\n <aside class=\"nf-hero\" aria-hidden=\"true\">\n <div class=\"nf-hero-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n<% if (it.hero === null) { %>\n <div class=\"nf-home\">\n <img\n class=\"nf-home-logo\"\n src=\"<%= it.logoHref %>\"\n alt=\"\"\n />\n <h2>Le framework Node.js fullstack</h2>\n <p class=\"sub\">\n Temps r\u00E9el natif \u00B7 d\u00E9veloppement agentic-ready \u00B7 socle TypeScript\n isomorphe\n </p>\n <p class=\"sig\">\n Une action de contr\u00F4leur. Deux transports. La m\u00EAme session, la m\u00EAme\n s\u00E9curit\u00E9, le m\u00EAme code.\n </p>\n <ul class=\"badges\">\n <li class=\"agentic\">Agentic ready</li>\n <li>Node.js \u2265 24</li>\n <li>TypeScript strict</li>\n <li>ESM only</li>\n <li>Licence Apache 2.0</li>\n </ul>\n <div\n class=\"install\"\n role=\"img\"\n aria-label=\"Commande d'installation : npx nodefony create app\"\n >\n <span class=\"p\">$</span><span>npx nodefony create app</span>\n </div>\n <div class=\"features\">\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polygon\n points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"\n /></svg\n ></span>\n <h3>Temps r\u00E9el natif</h3>\n <p>\n Le WebSocket n'est pas un ajout : m\u00EAme pipeline, m\u00EAme table de\n routes, m\u00EAme s\u00E9curit\u00E9 que le HTTP.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\" />\n <path d=\"m9 12 2 2 4-4\" /></svg\n ></span>\n <h3>S\u00E9curit\u00E9 par conception</h3>\n <p>\n Pare-feu applicatif par zones, Zero Trust, CSRF Fetch Metadata,\n sessions hybrides, WebAuthn.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" /></svg\n ></span>\n <h3>DI & modules</h3>\n <p>\n Injection de d\u00E9pendances par d\u00E9corateurs, modules autonomes,\n configuration valid\u00E9e au boot.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path\n d=\"M3 12h18M12 3c2.5 2.6 4 5.7 4 9s-1.5 6.4-4 9c-2.5-2.6-4-5.7-4-9s1.5-6.4 4-9z\"\n /></svg\n ></span>\n <h3>Socle isomorphe</h3>\n <p>\n Le m\u00EAme paquet c\u00F4t\u00E9 serveur et c\u00F4t\u00E9 navigateur : client temps\n r\u00E9el, r\u00E8gles et types \u00E9crits une fois.\n </p>\n </div>\n </div>\n </div>\n <small>Propuls\u00E9 par Nodefony</small>\n<% } else if (it.hero) { %>\n <div class=\"nf-home\">\n <h2><%= it.hero.heading %></h2>\n<% if (it.hero.text) { %>\n <p class=\"sub\"><%= it.hero.text %></p>\n<% } %>\n </div>\n<% } %>\n </aside>\n <main class=\"nf-stage\" data-nf-login data-from=\"<%= it.from %>\">\n <button type=\"button\" class=\"nf-theme\" data-theme-toggle aria-label=\"Basculer entre th\u00E8me clair et th\u00E8me sombre\"><svg class=\"nf-icon t-sun\" aria-hidden=\"true\"><use href=\"#i-sun\" /></svg><svg class=\"nf-icon t-moon\" aria-hidden=\"true\"><use href=\"#i-moon\" /></svg></button>\n <section class=\"nf-card\" aria-labelledby=\"nf-title\">\n <div class=\"nf-head\">\n <div class=\"nf-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n <h1 id=\"nf-title\"><%= it.title %></h1>\n <p class=\"nf-sub\" id=\"nf-sub\"><%= it.lead %></p>\n </div>\n <div class=\"nf-body\">\n<% if (it.password) { %>\n <ol class=\"nf-steps\" aria-label=\"\u00C9tapes\">\n <li data-tab=\"identifier\" aria-current=\"step\">Identifiant</li>\n <li data-tab=\"password\">Mot de passe</li>\n <li data-tab=\"mfa\" hidden>V\u00E9rification</li>\n </ol>\n <form data-step=\"identifier\">\n <div class=\"nf-field\">\n <label for=\"nf-username\">Identifiant ou adresse e-mail</label>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-user\" /></svg>\n <input id=\"nf-username\" name=\"username\" autocomplete=\"username\" autocapitalize=\"none\" spellcheck=\"false\" required />\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Continuer<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"password\" hidden>\n <!-- Compte de l'\u00E9tape pr\u00E9c\u00E9dente, masqu\u00E9 : le gestionnaire de mots de passe\n associe le mot de passe \u00E0 CE compte (HTML, jeton \u00AB username \u00BB). -->\n <input name=\"username\" autocomplete=\"username\" hidden readonly tabindex=\"-1\" />\n <div class=\"nf-account\">\n <span class=\"nf-avatar\" aria-hidden=\"true\"></span><span class=\"who\"></span>\n <button type=\"button\" class=\"nf-link\" data-go=\"identifier\">Changer</button>\n </div>\n <div class=\"nf-field\">\n <div class=\"nf-label-row\"><label for=\"nf-password\">Mot de passe</label></div>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-lock\" /></svg>\n <input id=\"nf-password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required />\n <button type=\"button\" class=\"nf-reveal\" data-reveal aria-label=\"Afficher le mot de passe\" aria-pressed=\"false\"><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-eye\" /></svg></button>\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Se connecter<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"mfa\" hidden>\n <div class=\"nf-field\">\n <label id=\"nf-otp-label\">Code \u00E0 6 chiffres</label>\n <div class=\"nf-otp\" role=\"group\" aria-labelledby=\"nf-otp-label\">\n <input inputmode=\"numeric\" maxlength=\"1\" autocomplete=\"one-time-code\" aria-label=\"Chiffre 1\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 2\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 3\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 4\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 5\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 6\" />\n </div>\n </div>\n <p class=\"nf-hint\">Le code affich\u00E9 par votre application d'authentification.</p>\n <button class=\"nf-btn primary\" type=\"submit\">V\u00E9rifier</button>\n </form>\n<% } %>\n<% if (it.password || it.passkey) { %>\n <div data-step=\"authenticated\" hidden>\n <div class=\"nf-done\" role=\"status\">\n <span class=\"ring\" aria-hidden=\"true\"><svg class=\"nf-icon\"><use href=\"#i-check\" /></svg></span>\n <strong>Session ouverte</strong><span class=\"dest\"></span>\n <span class=\"nf-progress\" aria-hidden=\"true\"><span></span></span>\n </div>\n </div>\n<% } %>\n<% if (it.providers.length > 0 || it.passkey) { %>\n <div data-alt>\n<% if (it.password) { %>\n <div class=\"nf-or\">ou</div>\n<% } %>\n <div class=\"nf-alt\">\n<% if (it.passkey) { %>\n <button type=\"button\" class=\"nf-btn span\" data-passkey hidden><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-finger\" /></svg>Passkey / empreinte digitale</button>\n<% } %>\n<% it.providers.forEach(function (provider) { %>\n <a class=\"nf-btn\" href=\"<%= provider.href %>\"><% if (provider.image) { %><img class=\"nf-icon\" src=\"<%= provider.image %>\" alt=\"\" /><% } else { %><svg class=\"<%= provider.icon === 'm-github' ? 'nf-mark' : 'nf-icon' %>\" aria-hidden=\"true\"><use href=\"#<%= provider.icon %>\" /></svg><% } %><% if (provider.caption) { %><span class=\"nf-label\"><%= provider.label %><small><%= provider.caption %></small></span><% } else { %><%= provider.label %><% } %></a>\n<% }) %>\n </div>\n </div>\n<% } %>\n <div class=\"nf-message\" aria-live=\"polite\" data-message><% if (it.empty) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">\u2014</span><span>Aucun moyen de connexion n'est disponible pour le moment. Pr\u00E9venez l'administrateur du site.</span></p><% } else if (it.notice) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">SSO</span><span><%= it.notice %></span></p><% } %></div>\n<% if (it.password || it.passkey) { %>\n <noscript><p class=\"nf-hint\">Cette page a besoin de JavaScript pour vous connecter.</p></noscript>\n<% } %>\n </div>\n </section>\n<% if (it.picker) { %>\n <form class=\"nf-picker\" method=\"get\" data-preview-picker<% if (it.picker.hook) { %> data-preview-hook=\"<%= it.picker.hook %>\"<% } %> aria-label=\"Aper\u00E7u de la page (d\u00E9veloppement)\">\n <input type=\"hidden\" name=\"from\" value=\"<%= it.from %>\" />\n <label>Habillage <select name=\"skin\"><% it.picker.skins.forEach(function (skin) { %><option value=\"<%= skin %>\"<% if (skin === it.skin) { %> selected<% } %>><%= skin %></option><% }) %></select></label>\n <label>Mise en page <select name=\"layout\"><% it.picker.layouts.forEach(function (layout) { %><option value=\"<%= layout %>\"<% if (layout === it.layout) { %> selected<% } %>><%= layout %></option><% }) %></select></label>\n <noscript><button type=\"submit\">Appliquer</button></noscript>\n </form>\n<% } %>\n<% if (it.footer) { %>\n <div class=\"nf-foot\">\n <ul class=\"nf-facts\" aria-label=\"Protection de la session\">\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>cookie HttpOnly</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>anti-CSRF</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>CSP stricte</li>\n </ul>\n <a class=\"nf-powered\" href=\"https://github.com/nodefony/nodefony-core\" rel=\"noopener noreferrer\"><img class=\"nf-logo nf-logo-mono\" src=\"<%= it.logoHref %>\" alt=\"\" />Propuls\u00E9 par Nodefony</a>\n </div>\n<% } %>\n </main>\n </div>\n </body>\n</html>\n";
144
+ export declare const LOGIN_PAGE_TEMPLATE = "<!doctype html>\n<html lang=\"fr\"<% if (it.theme) { %> data-theme=\"<%= it.theme %>\"<% } %>>\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark light\" />\n <meta name=\"robots\" content=\"noindex\" />\n <title><%= it.title %> \u2014 <%= it.brand %></title>\n <link rel=\"icon\" href=\"<%= it.logoHref %>\" type=\"image/svg+xml\" />\n<% if (!it.theme) { %>\n <script nonce=\"<%= it.nonce %>\">try{var t=localStorage.getItem(\"nf-login-theme\");if(t===\"light\"||t===\"dark\")document.documentElement.dataset.theme=t}catch(e){}</script>\n<% } %>\n <link rel=\"stylesheet\" href=\"<%= it.styleHref %>\" />\n<% if (it.skinHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.skinHref %>\" />\n<% } %>\n<% if (it.stylesheetHref) { %>\n <link rel=\"stylesheet\" href=\"<%= it.stylesheetHref %>\" />\n<% } %>\n <script type=\"module\" src=\"<%= it.scriptHref %>\" nonce=\"<%= it.nonce %>\"></script>\n </head>\n <body data-skin=\"<%= it.skin %>\" data-layout=\"<%= it.layout %>\"<% if (it.backdrop) { %> data-backdrop<% } %><% if (it.ssoFirst) { %> data-sso-first<% } %>>\n <svg width=\"0\" height=\"0\" style=\"position: absolute\" aria-hidden=\"true\">\n <symbol id=\"i-user\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"8\" r=\"3.5\" /><path d=\"M5.5 20a6.5 6.5 0 0 1 13 0\" /></symbol>\n <symbol id=\"i-lock\" viewBox=\"0 0 24 24\"><rect x=\"5\" y=\"10.5\" width=\"14\" height=\"10\" rx=\"2\" /><path d=\"M8.5 10.5V7.5a3.5 3.5 0 0 1 7 0v3\" /></symbol>\n <symbol id=\"i-eye\" viewBox=\"0 0 24 24\"><path d=\"M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z\" /><circle cx=\"12\" cy=\"12\" r=\"2.8\" /></symbol>\n <symbol id=\"i-arrow\" viewBox=\"0 0 24 24\"><path d=\"M5 12h14M13 6l6 6-6 6\" /></symbol>\n <symbol id=\"i-key\" viewBox=\"0 0 24 24\"><circle cx=\"8\" cy=\"15\" r=\"3.8\" /><path d=\"M10.8 12.2 19.5 3.5M16.5 6.5l2.5 2.5M14 9l2 2\" /></symbol>\n <symbol id=\"i-check\" viewBox=\"0 0 24 24\"><path d=\"M5 12.5l4.5 4.5L19 7.5\" /></symbol>\n <symbol id=\"i-finger\" viewBox=\"0 0 24 24\"><path d=\"M18.9 7a8 8 0 0 1 1.1 5v1a6 6 0 0 0 .8 3\" /><path d=\"M8 11a4 4 0 0 1 8 0v1a10 10 0 0 0 2 6\" /><path d=\"M12 11v2a14 14 0 0 0 2.5 8\" /><path d=\"M8 15a18 18 0 0 0 1.8 6\" /><path d=\"M4.9 19a22 22 0 0 1-.9-7v-1a8 8 0 0 1 12-6.95\" /></symbol>\n <symbol id=\"i-sun\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4\" /></symbol>\n <symbol id=\"i-moon\" viewBox=\"0 0 24 24\"><path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\" /></symbol>\n <symbol id=\"i-shield\" viewBox=\"0 0 24 24\"><path d=\"M12 3l7.5 3v5.5c0 4.6-3.2 8-7.5 9.5-4.3-1.5-7.5-4.9-7.5-9.5V6z\" /></symbol>\n <symbol id=\"m-github\" viewBox=\"0 0 16 16\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" /></symbol>\n </svg>\n <header class=\"nf-strip\">\n <div class=\"nf-strip-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n </header>\n <div class=\"nf-layout\">\n <aside class=\"nf-hero\" aria-hidden=\"true\">\n <div class=\"nf-hero-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n<% if (it.hero === null) { %>\n <div class=\"nf-home\">\n <img\n class=\"nf-home-logo\"\n src=\"<%= it.logoHref %>\"\n alt=\"\"\n />\n <h2>Le framework Node.js fullstack</h2>\n <p class=\"sub\">\n Temps r\u00E9el natif \u00B7 d\u00E9veloppement agentic-ready \u00B7 socle TypeScript\n isomorphe\n </p>\n <p class=\"sig\">\n Une action de contr\u00F4leur. Deux transports. La m\u00EAme session, la m\u00EAme\n s\u00E9curit\u00E9, le m\u00EAme code.\n </p>\n <ul class=\"badges\">\n <li class=\"agentic\">Agentic ready</li>\n <li>Node.js \u2265 24.14</li>\n <li>TypeScript strict</li>\n <li>ESM only</li>\n <li>Licence Apache 2.0</li>\n </ul>\n <div\n class=\"install\"\n role=\"img\"\n aria-label=\"Commande d'installation : npx nodefony create app\"\n >\n <span class=\"p\">$</span><span>npx nodefony create app</span>\n </div>\n <div class=\"features\">\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <polygon\n points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"\n /></svg\n ></span>\n <h3>Temps r\u00E9el natif</h3>\n <p>\n Le WebSocket n'est pas un ajout : m\u00EAme pipeline, m\u00EAme table de\n routes, m\u00EAme s\u00E9curit\u00E9 que le HTTP.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\" />\n <path d=\"m9 12 2 2 4-4\" /></svg\n ></span>\n <h3>S\u00E9curit\u00E9 par conception</h3>\n <p>\n Pare-feu applicatif par zones, Zero Trust, CSRF Fetch Metadata,\n sessions hybrides, WebAuthn.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"3\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" />\n <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\" rx=\"1\" /></svg\n ></span>\n <h3>DI & modules</h3>\n <p>\n Injection de d\u00E9pendances par d\u00E9corateurs, modules autonomes,\n configuration valid\u00E9e au boot.\n </p>\n </div>\n <div class=\"feature\">\n <span class=\"ic\"\n ><svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path\n d=\"M3 12h18M12 3c2.5 2.6 4 5.7 4 9s-1.5 6.4-4 9c-2.5-2.6-4-5.7-4-9s1.5-6.4 4-9z\"\n /></svg\n ></span>\n <h3>Socle isomorphe</h3>\n <p>\n Le m\u00EAme paquet c\u00F4t\u00E9 serveur et c\u00F4t\u00E9 navigateur : client temps\n r\u00E9el, r\u00E8gles et types \u00E9crits une fois.\n </p>\n </div>\n </div>\n </div>\n <small>Propuls\u00E9 par Nodefony</small>\n<% } else if (it.hero) { %>\n <div class=\"nf-home\">\n <h2><%= it.hero.heading %></h2>\n<% if (it.hero.text) { %>\n <p class=\"sub\"><%= it.hero.text %></p>\n<% } %>\n </div>\n<% } %>\n </aside>\n <main class=\"nf-stage\" data-nf-login data-from=\"<%= it.from %>\">\n <button type=\"button\" class=\"nf-theme\" data-theme-toggle aria-label=\"Basculer entre th\u00E8me clair et th\u00E8me sombre\"><svg class=\"nf-icon t-sun\" aria-hidden=\"true\"><use href=\"#i-sun\" /></svg><svg class=\"nf-icon t-moon\" aria-hidden=\"true\"><use href=\"#i-moon\" /></svg></button>\n <section class=\"nf-card\" aria-labelledby=\"nf-title\">\n <div class=\"nf-head\">\n <div class=\"nf-brand\"><img class=\"nf-logo\" src=\"<%= it.logo %>\" alt=\"\" /><%= it.brand %></div>\n <h1 id=\"nf-title\"><%= it.title %></h1>\n <p class=\"nf-sub\" id=\"nf-sub\"><%= it.lead %></p>\n </div>\n <div class=\"nf-body\">\n<% if (it.password) { %>\n <ol class=\"nf-steps\" aria-label=\"\u00C9tapes\">\n <li data-tab=\"identifier\" aria-current=\"step\">Identifiant</li>\n <li data-tab=\"password\">Mot de passe</li>\n <li data-tab=\"mfa\" hidden>V\u00E9rification</li>\n </ol>\n <form data-step=\"identifier\">\n <div class=\"nf-field\">\n <label for=\"nf-username\">Identifiant ou adresse e-mail</label>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-user\" /></svg>\n <input id=\"nf-username\" name=\"username\" autocomplete=\"username\" autocapitalize=\"none\" spellcheck=\"false\" required />\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Continuer<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"password\" hidden>\n <!-- Compte de l'\u00E9tape pr\u00E9c\u00E9dente, masqu\u00E9 : le gestionnaire de mots de passe\n associe le mot de passe \u00E0 CE compte (HTML, jeton \u00AB username \u00BB). -->\n <input name=\"username\" autocomplete=\"username\" hidden readonly tabindex=\"-1\" />\n <div class=\"nf-account\">\n <span class=\"nf-avatar\" aria-hidden=\"true\"></span><span class=\"who\"></span>\n <button type=\"button\" class=\"nf-link\" data-go=\"identifier\">Changer</button>\n </div>\n <div class=\"nf-field\">\n <div class=\"nf-label-row\"><label for=\"nf-password\">Mot de passe</label></div>\n <div class=\"nf-input\">\n <svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-lock\" /></svg>\n <input id=\"nf-password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required />\n <button type=\"button\" class=\"nf-reveal\" data-reveal aria-label=\"Afficher le mot de passe\" aria-pressed=\"false\"><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-eye\" /></svg></button>\n </div>\n </div>\n <button class=\"nf-btn primary\" type=\"submit\">Se connecter<svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-arrow\" /></svg></button>\n </form>\n <form data-step=\"mfa\" hidden>\n <div class=\"nf-field\">\n <label id=\"nf-otp-label\">Code \u00E0 6 chiffres</label>\n <div class=\"nf-otp\" role=\"group\" aria-labelledby=\"nf-otp-label\">\n <input inputmode=\"numeric\" maxlength=\"1\" autocomplete=\"one-time-code\" aria-label=\"Chiffre 1\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 2\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 3\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 4\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 5\" /><input inputmode=\"numeric\" maxlength=\"1\" aria-label=\"Chiffre 6\" />\n </div>\n </div>\n <p class=\"nf-hint\">Le code affich\u00E9 par votre application d'authentification.</p>\n <button class=\"nf-btn primary\" type=\"submit\">V\u00E9rifier</button>\n </form>\n<% } %>\n<% if (it.password || it.passkey) { %>\n <div data-step=\"authenticated\" hidden>\n <div class=\"nf-done\" role=\"status\">\n <span class=\"ring\" aria-hidden=\"true\"><svg class=\"nf-icon\"><use href=\"#i-check\" /></svg></span>\n <strong>Session ouverte</strong><span class=\"dest\"></span>\n <span class=\"nf-progress\" aria-hidden=\"true\"><span></span></span>\n </div>\n </div>\n<% } %>\n<% if (it.providers.length > 0 || it.passkey) { %>\n <div data-alt>\n<% if (it.password) { %>\n <div class=\"nf-or\">ou</div>\n<% } %>\n <div class=\"nf-alt\">\n<% if (it.passkey) { %>\n <button type=\"button\" class=\"nf-btn span\" data-passkey hidden><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-finger\" /></svg>Passkey / empreinte digitale</button>\n<% } %>\n<% it.providers.forEach(function (provider) { %>\n <a class=\"nf-btn\" href=\"<%= provider.href %>\"><% if (provider.image) { %><img class=\"nf-icon\" src=\"<%= provider.image %>\" alt=\"\" /><% } else { %><svg class=\"<%= provider.icon === 'm-github' ? 'nf-mark' : 'nf-icon' %>\" aria-hidden=\"true\"><use href=\"#<%= provider.icon %>\" /></svg><% } %><% if (provider.caption) { %><span class=\"nf-label\"><%= provider.label %><small><%= provider.caption %></small></span><% } else { %><%= provider.label %><% } %></a>\n<% }) %>\n </div>\n </div>\n<% } %>\n <div class=\"nf-message\" aria-live=\"polite\" data-message><% if (it.empty) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">\u2014</span><span>Aucun moyen de connexion n'est disponible pour le moment. Pr\u00E9venez l'administrateur du site.</span></p><% } else if (it.notice) { %><p class=\"nf-alert\" role=\"alert\"><span class=\"code\">SSO</span><span><%= it.notice %></span></p><% } %></div>\n<% if (it.password || it.passkey) { %>\n <noscript><p class=\"nf-hint\">Cette page a besoin de JavaScript pour vous connecter.</p></noscript>\n<% } %>\n </div>\n </section>\n<% if (it.picker) { %>\n <form class=\"nf-picker\" method=\"get\" data-preview-picker<% if (it.picker.hook) { %> data-preview-hook=\"<%= it.picker.hook %>\"<% } %> aria-label=\"Aper\u00E7u de la page (d\u00E9veloppement)\">\n <input type=\"hidden\" name=\"from\" value=\"<%= it.from %>\" />\n <label>Habillage <select name=\"skin\"><% it.picker.skins.forEach(function (skin) { %><option value=\"<%= skin %>\"<% if (skin === it.skin) { %> selected<% } %>><%= skin %></option><% }) %></select></label>\n <label>Mise en page <select name=\"layout\"><% it.picker.layouts.forEach(function (layout) { %><option value=\"<%= layout %>\"<% if (layout === it.layout) { %> selected<% } %>><%= layout %></option><% }) %></select></label>\n <noscript><button type=\"submit\">Appliquer</button></noscript>\n </form>\n<% } %>\n<% if (it.footer) { %>\n <div class=\"nf-foot\">\n <ul class=\"nf-facts\" aria-label=\"Protection de la session\">\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>cookie HttpOnly</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>anti-CSRF</li>\n <li><svg class=\"nf-icon\" aria-hidden=\"true\"><use href=\"#i-shield\" /></svg>CSP stricte</li>\n </ul>\n <a class=\"nf-powered\" href=\"https://github.com/nodefony/nodefony-core\" rel=\"noopener noreferrer\"><img class=\"nf-logo nf-logo-mono\" src=\"<%= it.logoHref %>\" alt=\"\" />Propuls\u00E9 par Nodefony</a>\n </div>\n<% } %>\n </main>\n </div>\n </body>\n</html>\n";
@@ -0,0 +1,112 @@
1
+ import type { Readable } from "node:stream";
2
+ import type { IStandardSchemaV1, InferSchemaOutput, MaybePromise } from "nodefony";
3
+ /**
4
+ * Le schéma d'une route : un Standard Schema par partie de l'entrée (Zod,
5
+ * Valibot, ArkType…). Chaque partie est facultative ; une partie déclarée est
6
+ * validée AVANT l'action, une partie omise est servie brute.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * \@Post("/posts", { schema: { body: z.object({ title: z.string() }) } })
11
+ * create(input: RouteInput<{ body: typeof createPost }>) {
12
+ * return this.createResource(input.body);
13
+ * }
14
+ * ```
15
+ */
16
+ export interface IRouteSchema {
17
+ /** Variables de route (`/posts/{id}`). */
18
+ readonly params?: IStandardSchemaV1 | undefined;
19
+ /** Query (`?page=2`) — celle du chemin INVOQUÉ sur le pont WebSocket. */
20
+ readonly query?: IStandardSchemaV1 | undefined;
21
+ /**
22
+ * Corps DÉCODÉ par le transport : JSON, formulaire, ou octets (`Uint8Array`
23
+ * — un `Buffer` HTTP en est un). Jamais re-sérialisé pour être validé.
24
+ */
25
+ readonly body?: IStandardSchemaV1 | undefined;
26
+ /** En-têtes (clés en minuscules) ; sur WebSocket, ceux du handshake. */
27
+ readonly headers?: IStandardSchemaV1 | undefined;
28
+ }
29
+ /**
30
+ * Options de route propres aux DÉCORATEURS (`@route`, `@Get`, `@Post`…) : elles
31
+ * décrivent l'entrée de l'action et se rangent sur sa méthode. Une route créée
32
+ * par programme (`Router.createRoute`) ne les accepte pas — elle n'a pas de
33
+ * décorateur pour les ranger, et les ignorer en silence mentirait.
34
+ */
35
+ export interface IRouteSchemaOptions {
36
+ /**
37
+ * Schéma de l'entrée de l'action. Les parties déclarées sont validées AVANT
38
+ * l'action (422 sinon), et l'action reçoit un argument unique, `RouteInput`.
39
+ * Exclusif des décorateurs de paramètre (`@Body`…) sur la même action : le
40
+ * mélange est refusé au démarrage.
41
+ */
42
+ schema?: IRouteSchema | undefined;
43
+ /**
44
+ * L'action lit le corps en FLUX (`input.body` est le flux brut de la
45
+ * requête) : le pipeline ne le lit pas en mémoire — gros envoi sans pic de
46
+ * mémoire. Remplace `@Body({ stream: true })` pour une action à schéma : il
47
+ * EXIGE un `schema` (sans lui, le flux ne serait livré à personne) et exclut
48
+ * `schema.body`. Défaut `false`.
49
+ */
50
+ bodyStream?: boolean | undefined;
51
+ }
52
+ /** Parties de l'entrée d'une route, dans l'ordre où elles sont validées. */
53
+ export type RoutePart = keyof IRouteSchema;
54
+ /** Variables de route brutes (une valeur par défaut n'est pas forcément une chaîne). */
55
+ export type RawRouteParams = Readonly<Record<string, unknown>>;
56
+ /** Query brute. */
57
+ export type RawRouteQuery = Readonly<Record<string, unknown>>;
58
+ /** En-têtes bruts, clés en minuscules. */
59
+ export type RawRouteHeaders = Readonly<Record<string, string | string[] | undefined>>;
60
+ /** Type d'une partie : la sortie de son schéma s'il est déclaré, la forme brute sinon. */
61
+ type RoutePartOf<S extends IRouteSchema, K extends RoutePart, Raw> = S[K] extends IStandardSchemaV1 ? InferSchemaOutput<S[K]> : Raw;
62
+ /**
63
+ * L'argument UNIQUE d'une action à schéma : les quatre apports de la
64
+ * requête, toujours présents — validés et typés par le schéma quand il les
65
+ * déclare, servis bruts et typés large sinon.
66
+ *
67
+ * @typeParam S - le schéma de la route.
68
+ * @typeParam Stream - `true` quand la route déclare `bodyStream: true` :
69
+ * `body` est alors le flux brut de la requête, jamais lu en mémoire.
70
+ */
71
+ export type RouteInput<S extends IRouteSchema = IRouteSchema, Stream extends boolean = false> = {
72
+ params: RoutePartOf<S, "params", RawRouteParams>;
73
+ query: RoutePartOf<S, "query", RawRouteQuery>;
74
+ body: Stream extends true ? Readable | undefined : RoutePartOf<S, "body", unknown>;
75
+ headers: RoutePartOf<S, "headers", RawRouteHeaders>;
76
+ };
77
+ /** L'entrée brute d'une action, avant validation. */
78
+ export type RawRouteInput = {
79
+ params: unknown;
80
+ query: unknown;
81
+ body: unknown;
82
+ headers: unknown;
83
+ };
84
+ /**
85
+ * Valide l'entrée d'une action contre son schéma — synchrone tant que chaque
86
+ * validateur l'est, asynchrone seulement si l'un d'eux rend une promesse.
87
+ *
88
+ * Les quatre parties sont toutes validées avant de conclure : un échec rend
89
+ * en une fois TOUTES les anomalies, chacune rattachée à sa partie. La valeur
90
+ * d'une partie validée est remplacée par la SORTIE de son schéma (valeurs par
91
+ * défaut, coercitions) ; la valeur reçue n'est ni copiée ni sérialisée — des
92
+ * octets décodés par le transport arrivent tels quels au validateur.
93
+ *
94
+ * @param schema - le schéma de la route.
95
+ * @param raw - l'entrée brute, mutée en place (objet propre à la requête).
96
+ * @returns l'entrée validée, ou sa promesse.
97
+ * @throws SchemaValidationError (statut 422) quand une partie est invalide —
98
+ * de façon synchrone, ou par la promesse rendue.
99
+ */
100
+ export declare function validateRouteInput(schema: IRouteSchema, raw: RawRouteInput): MaybePromise<RawRouteInput>;
101
+ /**
102
+ * Refuse, au démarrage, un schéma de route mal formé : partie inconnue (une
103
+ * faute de frappe — `bodies` — serait sinon ignorée sans un mot), ou valeur
104
+ * qui n'implémente pas Standard Schema (un appelant JavaScript peut passer
105
+ * n'importe quoi).
106
+ *
107
+ * @param schema - le schéma déclaré.
108
+ * @param routeName - la route, nommée dans l'erreur.
109
+ * @throws BootConfigurationError sur la première faute trouvée.
110
+ */
111
+ export declare function assertRouteSchemaShape(schema: unknown, routeName: string): asserts schema is IRouteSchema;
112
+ export {};
@@ -28,6 +28,8 @@ export declare const SYSLOG_FILTERS: {
28
28
  readonly module: "string";
29
29
  /** Catégorie de message (msgid) — inclusion insensible à la casse. */
30
30
  readonly msgid: "string";
31
+ /** Module exclu — égalité insensible à la casse (`NODEFONY` = le framework). */
32
+ readonly excludeModule: "string";
31
33
  /** Protocole d'origine ; absent = les deux. */
32
34
  readonly protocol: readonly ["ws", "http"];
33
35
  /**
@@ -45,6 +47,10 @@ export declare const SYSLOG_FILTERS: {
45
47
  readonly flow: {
46
48
  readonly each: FlowStepId[];
47
49
  };
50
+ /** Écarte les lignes d'accès de la console d'administration elle-même. */
51
+ readonly hideConsole: "boolean";
52
+ /** Une ligne par requête (sa ligne d'accès), avec le nombre de lignes regroupées. */
53
+ readonly groupByRequest: "boolean";
48
54
  /** Borne basse d'horodatage (epoch ms, incluse). */
49
55
  readonly from: "int";
50
56
  /** Borne haute d'horodatage (epoch ms, incluse). */