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

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 (47) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js +7 -0
  3. package/dist/index.js +14 -10
  4. package/dist/nodefony/config/config.js +16 -2
  5. package/dist/nodefony/controller/AdminApiController.js +18 -5
  6. package/dist/nodefony/controller/BenchController.js +1 -1
  7. package/dist/nodefony/controller/CspReportController.js +70 -0
  8. package/dist/nodefony/controller/IssuerMetadataController.js +1 -1
  9. package/dist/nodefony/controller/LoginPageController.js +5 -7
  10. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -1
  11. package/dist/nodefony/controller/SessionAuthController.js +6 -7
  12. package/dist/nodefony/controller/TokenAuthController.js +3 -3
  13. package/dist/nodefony/decorators/routerDecorators.js +2 -2
  14. package/dist/nodefony/service/AdminBroker.js +1 -1
  15. package/dist/nodefony/service/IdempotencyStore.js +1 -1
  16. package/dist/nodefony/service/router.js +5 -4
  17. package/dist/nodefony/src/Controller.js +311 -114
  18. package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
  19. package/dist/nodefony/src/Resolver.js +3 -9
  20. package/dist/nodefony/src/SecurityApiController.js +30 -7
  21. package/dist/nodefony/src/conditionalRequest.js +103 -0
  22. package/dist/nodefony/src/docsReader.js +1 -1
  23. package/dist/nodefony/src/idempotency.js +1 -1
  24. package/dist/nodefony/src/loginPage.js +2 -1
  25. package/dist/nodefony/src/scopeCatalog.js +1 -1
  26. package/dist/nodefony/src/viewNonce.js +58 -0
  27. package/dist/types/index.d.ts +3 -2
  28. package/dist/types/nodefony/config/config.d.ts +20 -0
  29. package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
  30. package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
  31. package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
  32. package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
  33. package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
  34. package/dist/types/nodefony/interfaces/IController.d.ts +2 -2
  35. package/dist/types/nodefony/service/router.d.ts +3 -2
  36. package/dist/types/nodefony/src/Controller.d.ts +110 -12
  37. package/dist/types/nodefony/src/Resolver.d.ts +1 -1
  38. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
  39. package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
  40. package/dist/types/nodefony/src/loginPage.d.ts +1 -1
  41. package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
  42. package/docs/controller.md +145 -26
  43. package/docs/decorateurs.md +1 -1
  44. package/docs/idempotence.md +2 -2
  45. package/docs/index.md +4 -2
  46. package/docs/templates.md +15 -0
  47. package/package.json +10 -9
@@ -256,7 +256,7 @@ declare class Resolver implements IResolver {
256
256
  * (`returnController`) — le chemin du **pont `api.request`** (WS) : la valeur
257
257
  * nue est enveloppée `{id, result}` par le peer, jamais rendue sur le
258
258
  * transport. La méthode HTTP logique d'une mutation du pont voyage dans
259
- * {@link methodOverride} ; sans porte ici, un rejeu `socket.mutate` (socket
259
+ * {@link methodOverride} ; sans porte ici, un rejeu `socket.post` (socket
260
260
  * qui reconnecte) ré-exécuterait la mutation (doublon — vécu au banc duplex).
261
261
  *
262
262
  * @returns `{ result }` — la valeur retournée par l'action (ou la réponse
@@ -1,15 +1,28 @@
1
1
  import type { OutgoingHttpHeaders } from "node:http";
2
2
  import Controller from "./Controller.js";
3
+ /**
4
+ * En-têtes d'un 401 : ceux reçus, plus le défi par défaut
5
+ * ({@link SESSION_AUTH_CHALLENGE}) s'ils n'en portent aucun — RFC 9110 §15.5.2
6
+ * exige un `WWW-Authenticate` sur TOUT 401. Un défi posé par l'appelant (un
7
+ * `Bearer` de ressource protégée, par exemple) garde la main.
8
+ *
9
+ * Chemin froid : n'est appelé que pour un 401.
10
+ *
11
+ * @param headers - les en-têtes déjà prévus pour la réponse.
12
+ * @returns les mêmes en-têtes, défi compris.
13
+ */
14
+ export declare function withAuthChallenge(headers: Readonly<OutgoingHttpHeaders> | undefined): OutgoingHttpHeaders;
3
15
  /**
4
16
  * Base des contrôleurs d'authentification (`/nodefony/security/api/*`) : toute
5
- * réponse JSON part en `Cache-Control: no-store`.
17
+ * réponse JSON part en `Cache-Control: no-store`, et tout 401 porte un défi.
6
18
  *
7
19
  * Ces réponses portent une identité, un jeton, un secret affiché une seule
8
20
  * fois (clé API, secret TOTP, codes de récupération) ou leur refus. Un cookie
9
21
  * ou un en-tête dans la requête n'empêche pas un cache partagé de STOCKER la
10
- * réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton. La règle
11
- * vit ici, une fois : une action ajoutée à un contrôleur héritier ne peut pas
12
- * l'oublier. Un en-tête passé par l'action garde la priorité.
22
+ * réponse (RFC 9111 §3) ; RFC 6749 §5.1 exige `no-store` sur un jeton. Un 401
23
+ * reçoit le défi par défaut ({@link withAuthChallenge}, RFC 9110 §15.5.2). Ces
24
+ * règles vivent ici, une fois : une action ajoutée à un contrôleur héritier ne
25
+ * peut pas les oublier. Un en-tête passé par l'action garde la priorité.
13
26
  */
14
27
  declare abstract class SecurityApiController extends Controller {
15
28
  renderJson(obj: unknown, status?: string | number, headers?: OutgoingHttpHeaders): ReturnType<Controller["renderJson"]>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Requêtes conditionnelles sur une représentation servie par un contrôleur —
3
+ * validateurs (RFC 9110 §8.8) et évaluation des préconditions (§13).
4
+ *
5
+ * Fonctions PURES : l'instant courant est un paramètre, aucune lecture
6
+ * d'horloge ni d'en-tête n'a lieu ici. Le contrôleur rassemble les entrées,
7
+ * ce module rend le verdict.
8
+ */
9
+ /** Validateurs d'une représentation (RFC 9110 §8.8). */
10
+ export interface IValidators {
11
+ /** Étiquette d'entité, guillemets compris (`"…"` forte, `W/"…"` faible) ; `null` si aucune. */
12
+ readonly etag: string | null;
13
+ /**
14
+ * Dernière modification (ms depuis l'époque), à la seconde — la précision
15
+ * d'une date HTTP — et jamais postérieure à l'instant de la réponse
16
+ * (§8.8.2.1) ; `null` si aucune.
17
+ */
18
+ readonly lastModified: number | null;
19
+ /**
20
+ * La date est-elle un validateur FORT (§8.8.2.2) : modifiée au moins une
21
+ * seconde avant la réponse, elle ne peut plus changer dans la même seconde.
22
+ * Seul un validateur fort autorise une reprise par `If-Range` (§13.1.5).
23
+ */
24
+ readonly lastModifiedStrong: boolean;
25
+ }
26
+ /** Issue des préconditions : servir (200), rien n'a changé (304), condition fausse (412). */
27
+ export type PreconditionOutcome = 200 | 304 | 412;
28
+ /** En-têtes de requête lus par l'évaluation (noms en minuscules, comme Node les rend). */
29
+ export interface IConditionalHeaders {
30
+ readonly "if-match"?: string | undefined;
31
+ readonly "if-none-match"?: string | undefined;
32
+ readonly "if-modified-since"?: string | undefined;
33
+ readonly "if-unmodified-since"?: string | undefined;
34
+ readonly "if-range"?: string | undefined;
35
+ }
36
+ /**
37
+ * Validateurs d'un fichier, tirés de ses métadonnées sans le lire.
38
+ *
39
+ * L'`ETag` est FORTE : taille et date de modification à la microseconde, deux
40
+ * grandeurs qu'une écriture change. C'est la forme que posent les serveurs de
41
+ * fichiers courants, et la seule qui autorise une reprise (§13.1.5).
42
+ *
43
+ * @param stats - taille et date de modification du fichier
44
+ * @param now - instant de la réponse (ms)
45
+ * @returns les validateurs du fichier
46
+ */
47
+ export declare function fileValidators(stats: {
48
+ readonly size: number;
49
+ readonly mtimeMs: number;
50
+ }, now: number): IValidators;
51
+ /**
52
+ * Date HTTP (RFC 9110 §5.6.7) lue en ms, ou `null` si la valeur n'en est pas
53
+ * une — une condition sur une date invalide DOIT être ignorée (§13.1.3, §13.1.4).
54
+ *
55
+ * @param value - valeur brute de l'en-tête
56
+ */
57
+ export declare function parseHttpDate(value: string | undefined): number | null;
58
+ /**
59
+ * Évalue les préconditions d'une requête sur une représentation qui EXISTE,
60
+ * dans l'ordre de RFC 9110 §13.2.2 — `If-Match`, sinon `If-Unmodified-Since` ;
61
+ * puis `If-None-Match`, sinon `If-Modified-Since`. `Range` et `If-Range`
62
+ * viennent après, s'il faut servir (voir {@link ifRangeHolds}).
63
+ *
64
+ * @param method - méthode de la requête
65
+ * @param headers - en-têtes de la requête
66
+ * @param validators - validateurs de la représentation
67
+ * @returns 200 pour servir, 304 si rien n'a changé (GET et HEAD seulement),
68
+ * 412 si une condition est fausse
69
+ */
70
+ export declare function evaluatePreconditions(method: string, headers: IConditionalHeaders, validators: IValidators): PreconditionOutcome;
71
+ /**
72
+ * Une condition `If-Range` tient-elle (RFC 9110 §13.1.5) ? Vraie, la plage
73
+ * demandée est servie (206) ; fausse, `Range` est ignoré et la représentation
74
+ * part entière (200) — un client ne recolle jamais deux versions.
75
+ *
76
+ * Une étiquette tient si elle est FORTEMENT égale ; une date, si elle égale
77
+ * exactement `Last-Modified` ET que celle-ci est un validateur fort.
78
+ *
79
+ * @param value - valeur brute de `If-Range`
80
+ * @param validators - validateurs de la représentation
81
+ */
82
+ export declare function ifRangeHolds(value: string, validators: IValidators): boolean;
@@ -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.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";
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 <% if (it.nonce) { %><meta property=\"csp-nonce\" nonce=\"<%= it.nonce %>\"><% } %>\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,33 @@
1
+ /**
2
+ * Signature CSP des vues : chaque `<script>` et `<style>` ÉCRIT dans un
3
+ * gabarit reçoit le nonce de la requête, sans geste de l'auteur.
4
+ *
5
+ * La politique par défaut exige un nonce sur les scripts (`'strict-dynamic'`)
6
+ * et sur les styles (ADR-0024 §C1, §C2). Une page écrite vite, par quelqu'un
7
+ * qui ignore tout de la CSP, ne doit pas être bloquée (§C0) : la signature se
8
+ * pose donc au rendu, par le framework.
9
+ *
10
+ * Elle porte sur le TEXTE DU GABARIT, avant que les données y soient insérées :
11
+ * une balise que les données apporteraient (une valeur insérée brute par
12
+ * `<%~ %>`) n'est jamais signée — c'est ce qui garde au nonce sa valeur contre
13
+ * l'injection. Même principe que l'`index.html` d'un front bâti
14
+ * (`prebuiltUi.ts#splitAroundNonce`).
15
+ */
16
+ /** Variable de vue qui porte le nonce CSP de la requête (`<%= cspNonce %>`). */
17
+ export declare const VIEW_NONCE_LOCAL = "cspNonce";
18
+ /**
19
+ * Ajoute le nonce de la requête à chaque `<script>` et `<style>` littéral du
20
+ * gabarit qui n'en porte pas.
21
+ *
22
+ * Une balise déjà signée par l'auteur est laissée telle quelle ; un `<script`
23
+ * écrit À L'INTÉRIEUR d'une balise Eta (une chaîne de code) n'est jamais
24
+ * touché. Un `<script>` dont un attribut vient des données
25
+ * (`src="<%= it.url %>"`) n'est pas signé non plus : le signer exécuterait
26
+ * l'URL qu'elles apportent. L'auteur qui la tient pour sûre signe lui-même
27
+ * (`nonce="<%= it.cspNonce %>"`). Le rendu doit fournir la variable
28
+ * {@link VIEW_NONCE_LOCAL}.
29
+ *
30
+ * @param source - texte du gabarit Eta, tel que lu sur le disque.
31
+ * @returns le gabarit signé — la MÊME chaîne quand il n'y a rien à signer.
32
+ */
33
+ export declare function signTemplateTags(source: string): string;
@@ -415,14 +415,14 @@ de ce que tu as retourné :
415
415
  | Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts#Resolver._callWithIdempotency` |
416
416
  | Un `Buffer` | Envoyé brut | `Resolver.ts#Resolver.returnController` |
417
417
  | Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts#Resolver._invokeAction` |
418
- | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts#NO_BODY_STATUS`) |
418
+ | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `isContentlessStatus` (`Response.ts#isContentlessStatus`, `@nodefony/http`) |
419
419
  | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts#Resolver.returnController` |
420
420
  | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts#Resolver.returnController` |
421
421
 
422
422
  > [!WARNING]
423
423
  > **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
424
424
  > répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
425
- > `NO_BODY_STATUS` (`Resolver.ts#NO_BODY_STATUS`) contient 204, 205 et 304. Donc un `@Delete` qui fait
425
+ > `isContentlessStatus` (`Response.ts#isContentlessStatus`, liste unique de `@nodefony/http`) rend vrai pour 204, 205 et 304. Donc un `@Delete` qui fait
426
426
  > `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
427
427
  > laisse la requête pendue.
428
428
 
@@ -463,8 +463,10 @@ Deux besoins distincts, deux helpers.
463
463
  ### Téléchargement — `renderFileDownload()`
464
464
 
465
465
  `renderFileDownload(file, options?, headers?)` (`Controller.ts#Controller.renderFileDownload`) pose
466
- `Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
467
- de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts#Controller.getFileAsync`) ;
466
+ `Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier et un `Expires` déjà
467
+ passé, puis délègue au moteur de flux. Un nom de fichier accentué ou porteur de `"` part en
468
+ `filename*` UTF-8, précédé d'un repli ASCII (RFC 6266 §4.3, RFC 8187) — un nom quelconque ne fait
469
+ jamais lever la réponse. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts#Controller.getFileAsync`) ;
468
470
  la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
469
471
  et gèle le process le temps du stat.
470
472
 
@@ -473,25 +475,136 @@ et gèle le process le temps du stat.
473
475
  `renderMediaStream(file, headers?, options?)` (`Controller.ts#Controller.renderMediaStream`) implémente les **requêtes par
474
476
  plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
475
477
 
476
- | Le client envoie… | Réponse |
477
- | --------------------------------------------- | --------------------------------------------------------------- |
478
- | Pas de `Range` | 200 + fichier complet, `Accept-Ranges: bytes` |
479
- | `Range: bytes=0-499` | **206** + `Content-Range`, bornes clampées à la taille réelle |
480
- | `Range: bytes=-500` (suffixe) | 206 sur les 500 derniers octets |
481
- | Plage hors fichier | **416** + `Content-Range: bytes */<taille>` (RFC 9110 §15.5.17) |
482
- | Syntaxe invalide, multi-plage, unité inconnue | En-tête **ignoré** → 200 complet (jamais un 500) |
478
+ | Le client envoie… | Réponse |
479
+ | ------------------------------------------------ | --------------------------------------------------------------- |
480
+ | Pas de `Range` | 200 + fichier complet, `Accept-Ranges: bytes` |
481
+ | `Range: bytes=0-499` | **206** + `Content-Range`, bornes clampées à la taille réelle |
482
+ | `Range: bytes=-500` (suffixe) | 206 sur les 500 derniers octets |
483
+ | Plage hors fichier | **416** + `Content-Range: bytes */<taille>` (RFC 9110 §15.5.17) |
484
+ | Syntaxe invalide, multi-plage, unité inconnue | En-tête **ignoré** → 200 complet (jamais un 500) |
485
+ | `HEAD` avec `Range` | `Range` **ignoré** : en-têtes du GET complet (RFC 9110 §14.2) |
486
+ | `If-Range` égal à l'`ETag` ou au `Last-Modified` | 206 : la reprise est sûre, le fichier n'a pas changé |
487
+ | `If-Range` différent, ou étiquette faible | `Range` **ignoré** → 200 complet (RFC 9110 §13.1.5) |
488
+
489
+ Une condition `If-Range` fausse sert le fichier entier — plutôt qu'une plage d'un fichier qui a
490
+ changé, que le client recollerait à l'ancienne. Seuls un `ETag` FORTEMENT égal, ou une date égale
491
+ à un `Last-Modified` vieux d'au moins une seconde, autorisent la reprise.
483
492
 
484
493
  La logique est isolée dans une fonction pure exportée, `parseByteRange()` (`Controller.ts#parseByteRange`) —
485
494
  donc testable sans serveur.
486
495
 
487
496
  ### Ce que `streamFile()` garantit
488
497
 
489
- `streamFile()` (`Controller.ts#Controller.streamFile`) est le moteur commun. Sa subtilité n'est pas le pipe, c'est le
490
- **nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
491
- téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
492
- écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
493
- résout la promesse (`Controller.ts#Controller.streamFile`), puis se retire lui-même (`Controller.ts#Controller.streamFile`). Un
494
- téléchargement interrompu ne coûte donc **rien** en ressource retenue.
498
+ `streamFile()` (`Controller.ts#Controller.streamFile`) est le moteur commun, et il repose sur le
499
+ même cycle de vie que `renderStream()` ci-dessous (`HttpContext.openStream`) :
500
+
501
+ - **la réponse est prise à l'appel**, les en-têtes partent au premier morceau ;
502
+ - **un client qui raccroche détruit le flux**, donc ferme le descripteur : un téléchargement
503
+ interrompu ne retient rien ;
504
+ - **un fichier illisible à l'ouverture** (`EACCES`) rend la page d'erreur ordinaire, 500 ; une
505
+ lecture qui échoue en route coupe la connexion, jamais une fin propre ;
506
+ - **la longueur annoncée est tenue** : la lecture est bornée à la taille relevée (un fichier qui
507
+ grossit pendant l'envoi n'envoie pas plus que `Content-Length`), et un extrait `start`/`end`
508
+ annonce sa propre longueur ;
509
+ - **un fichier n'est pas un flux vivant** : ni `X-Accel-Buffering: no`, ni délai d'inactivité.
510
+
511
+ ### Requêtes conditionnelles — 304, 412, reprise
512
+
513
+ Les trois helpers posent sur le fichier ENTIER une `ETag` forte et un `Last-Modified`, tirés de sa
514
+ taille et de sa date de modification, sans le lire. Les préconditions sont évaluées dans l'ordre de
515
+ RFC 9110 §13.2.2, AVANT d'ouvrir le fichier (`conditionalRequest.ts#evaluatePreconditions`) :
516
+
517
+ | Le client envoie… | Réponse |
518
+ | ------------------------------------------------------------ | ------------------------------------------- |
519
+ | `If-None-Match` égal (comparaison faible), ou `*` | **304** en GET/HEAD, sans corps ; 412 sinon |
520
+ | `If-Modified-Since` ≥ date du fichier (sans `If-None-Match`) | **304** en GET/HEAD |
521
+ | `If-Match` différent (comparaison forte) | **412** `Precondition Failed` |
522
+ | `If-Unmodified-Since` < date du fichier (sans `If-Match`) | **412** |
523
+
524
+ Le 304 porte ce qu'aurait porté le 200 : `ETag`, `Last-Modified`, `Cache-Control`, `Expires`,
525
+ `Vary` (RFC 9110 §15.4.5). Le helper rend alors `null` : aucun fichier n'a été ouvert.
526
+
527
+ **`Cache-Control: private, no-cache` par défaut.** Sans lui, `Last-Modified` rendrait la réponse
528
+ « heuristiquement cacheable » (RFC 9111 §4.2.2) : un cache partagé pourrait garder le fichier et le
529
+ resservir sans redemander — or un fichier servi par une action est le plus souvent protégé.
530
+ `private` ferme les caches partagés, `no-cache` fait revalider avant chaque réutilisation, ce qui
531
+ ne coûte qu'un 304. Un fichier public fixe le sien :
532
+
533
+ ```ts
534
+ return this.renderMediaStream(file, {
535
+ "Cache-Control": "public, max-age=3600",
536
+ });
537
+ ```
538
+
539
+ Une `ETag` ou un `Last-Modified` passés dans les en-têtes (un hachage du contenu, une version)
540
+ remplacent ceux du fichier ET servent à la comparaison. Un extrait `streamFile(f, h, { start, end })`
541
+ n'est pas la représentation entière : il ne reçoit ni validateur ni évaluation.
542
+
543
+ **Ce que l'application peut changer** :
544
+
545
+ | Besoin | Geste |
546
+ | ----------------------------------------------------- | -------------------------------------------------------------------------- |
547
+ | Un autre `Cache-Control` (fichier public, `no-store`) | le passer dans les en-têtes, ou `@Header("Cache-Control", …)` sur l'action |
548
+ | Sa propre `ETag` (hachage du contenu, version) | la passer dans les en-têtes, ou `@Header("ETag", …)` — elle sert au 304 |
549
+ | Aucun validateur, aucun 304, aucun `Cache-Control` | `{ validators: false }` dans les options |
550
+ | Le défaut de toute l'application | la section `fileServing` de la config du module (ci-dessous) |
551
+
552
+ Un en-tête posé AVANT l'appel (`@Header`, `this.response.setHeader`) est respecté comme s'il était
553
+ passé en argument.
554
+
555
+ **Pour toute l'application**, la section `fileServing` de la config du module fixe le défaut
556
+ (`config.ts#fileServingSchema`) ; un appel le remplace toujours :
557
+
558
+ ```ts
559
+ use("@nodefony/framework", {
560
+ fileServing: {
561
+ validators: true, // false : aucun validateur, aucun 304, aucun Cache-Control par défaut
562
+ cacheControl: "private, no-cache", // null : n'en poser aucun
563
+ },
564
+ });
565
+ ```
566
+
567
+ `cacheControl` est validé au démarrage : une valeur porteuse d'un retour à la ligne (qui forgerait
568
+ un en-tête de plus) l'interrompt en nommant la clé. `{ validators: true }` dans les options d'un
569
+ appel rétablit les validateurs quand la config les a coupés.
570
+
571
+ ⚠️ **La date d'un fichier n'est pas toujours fiable.** Une archive décompressée ou un build
572
+ reproductible donne la même date à tous les fichiers : deux contenus de même taille portent alors
573
+ la même `ETag`, et le client garde l'ancien sur un 304 sans que rien ne le signale. Pour ces
574
+ fichiers, passer une `ETag` tirée du contenu — le 304 reste juste — ou couper les validateurs :
575
+
576
+ ```ts
577
+ // le 304 garde son sens : l'étiquette suit le contenu
578
+ return this.renderMediaStream(file, { ETag: `"${hash}"` });
579
+ // ou : aucune requête conditionnelle, le fichier part toujours entier
580
+ return this.renderMediaStream(file, {}, { validators: false });
581
+ ```
582
+
583
+ ### Une source qui n'est pas un fichier — `renderStream()`
584
+
585
+ La sortie d'un processus (ffmpeg, `tar`), le corps d'un `fetch()` relayé, une archive construite à
586
+ la volée : `renderStream(source, options?)` (`Controller.ts#Controller.renderStream`) répond par
587
+ ce flux d'octets, en HTTP/1.1 comme en HTTP/2. La réponse est prise à l'appel, ses en-têtes partent
588
+ au premier morceau, et la source est **détruite** à la fin quelle qu'en soit la raison. La promesse
589
+ ne rejette jamais : elle rend `{ reason, bytes }`.
590
+
591
+ ```typescript
592
+ const ff = spawn("ffmpeg", args, {
593
+ stdio: ["ignore", "pipe", "ignore"],
594
+ windowsHide: true,
595
+ });
596
+ try {
597
+ await this.renderStream(ff.stdout, {
598
+ headers: { "content-type": "video/mp4" },
599
+ });
600
+ } finally {
601
+ ff.kill("SIGKILL"); // le flux est fini, quelle qu'en soit la raison
602
+ }
603
+ ```
604
+
605
+ Page complète — options, raisons de fin, erreurs avant et après le premier octet, relais `fetch`,
606
+ pièges : [Flux d'octets](../../http/docs/byte-stream.md). Pour des **événements texte** lus par
607
+ `EventSource`, voir [Flux SSE](../../http/docs/sse.md).
495
608
 
496
609
  ## ⚠️ Erreurs — lever, rendre, observer
497
610
 
@@ -594,15 +707,21 @@ code du framework applique — et attend de toi — les règles suivantes :
594
707
 
595
708
  ## 📜 Normes appliquées
596
709
 
597
- | Domaine | Norme | Comment le code s'y conforme |
598
- | -------------------------------- | ------------------------ | ------------------------------------------------------------------------------------ |
599
- | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts#NO_BODY_STATUS`) |
600
- | Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts#parseByteRange`) |
601
- | Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts#Controller.renderResponse`) |
602
- | Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts#HttpResponse.send`) |
603
- | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts#Resolver.returnController`), vérifié par le banc `auto-json` |
604
- | Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts#Resolver._callWithIdempotency`) |
605
- | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts#DefaultErrorRenderer.renderWebsocket`) |
710
+ | Domaine | Norme | Comment le code s'y conforme |
711
+ | --------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------- |
712
+ | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.3.6/§15.4.5 | `isContentlessStatus` (`Response.ts#isContentlessStatus`) — aucun corps ne part |
713
+ | Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts#parseByteRange`) |
714
+ | Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts#Controller.renderResponse`) |
715
+ | `Range` hors GET, sous `If-Range` | RFC 9110 §14.2, §13.1.5 | `renderMediaStream()` (`Controller.ts#Controller.renderMediaStream`) |
716
+ | Validateurs d'un fichier | RFC 9110 §8.8.2, §8.8.3 | `fileValidators()` (`conditionalRequest.ts#fileValidators`) |
717
+ | Ordre des préconditions → 304/412 | RFC 9110 §13.1, §13.2.2 | `evaluatePreconditions()` (`conditionalRequest.ts#evaluatePreconditions`) |
718
+ | Cache d'un fichier protégé | RFC 9111 §4.2.2, §5.2.2 | `Cache-Control: private, no-cache` par défaut (`Controller.ts#Controller.streamFile`) |
719
+ | Nom de fichier téléchargé | RFC 6266 §4.3, RFC 8187 | `contentDisposition()` (`Controller.ts#contentDisposition`) |
720
+ | Longueur du corps tenue | RFC 9112 §6.3, RFC 9113 §8.1.1 | `streamFile()` (`Controller.ts#Controller.streamFile`) + `ByteStream.ts#ByteStream` |
721
+ | Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts#HttpResponse.send`) |
722
+ | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts#Resolver.returnController`), vérifié par le banc `auto-json` |
723
+ | Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts#Resolver._callWithIdempotency`) |
724
+ | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts#DefaultErrorRenderer.renderWebsocket`) |
606
725
 
607
726
  ## 📡 Observabilité — Studio
608
727
 
@@ -431,7 +431,7 @@ update(input: RouteInput<{ params: typeof postParams; body: typeof postBody }>)
431
431
  **Ce que fait le framework, dans l'ordre :**
432
432
 
433
433
  - chaque partie déclarée est un [Standard Schema](https://standardschema.dev) — Zod, Valibot,
434
- ArkType… ; l'interface est recopiée au cœur (`runtime/standardSchema.ts#IStandardSchemaV1`), aucune
434
+ ArkType… ; l'interface est recopiée au cœur (`runtime/standardSchemaTypes.ts#IStandardSchemaV1`), aucune
435
435
  dépendance n'est imposée ;
436
436
  - la validation a lieu **après** les gardes (`@IsGranted` : un 403 ne révèle pas le contrat) et
437
437
  **avant** l'instanciation du contrôleur : une entrée invalide ne construit rien
@@ -263,7 +263,7 @@ Deux mécanismes rendent ça possible côté socket :
263
263
  `resolveIdempotencyKey()` (`idempotency.ts#resolveIdempotencyKey`) donne la **priorité à l'ALS** sur l'en-tête HTTP.
264
264
  - **La méthode logique voyage par `methodOverride`** — sur une socket, `context.method` vaut
265
265
  `WEBSOCKET`, qui n'est **pas** une mutation. Sans override, la porte serait sautée et un rejeu de
266
- frame `socket.mutate` créerait un doublon. Le Resolver teste donc
266
+ frame `socket.post` créerait un doublon. Le Resolver teste donc
267
267
  `isMutationMethod(this.methodOverride ?? context.method)` (`Resolver.ts#Resolver.action`).
268
268
 
269
269
  Le pont utilise `executeActionGuarded()` (`Resolver.ts#Resolver.executeActionGuarded`) : porte d'idempotence **sans** rendu HTTP
@@ -656,7 +656,7 @@ pour l'affichage Studio, `idempotencyStoreRegistry.ts#listIdempotencyBackends`).
656
656
  <!-- prettier-ignore -->
657
657
  | Sujet | Norme | Ancrage |
658
658
  | --- | --- | --- |
659
- | En-tête `Idempotency-Key`, statuts, rejeu | `draft-ietf-httpapi-idempotency-key-header-06` | `evaluateIdempotency()` (`idempotency.ts#evaluateIdempotency`) |
659
+ | En-tête `Idempotency-Key`, statuts, rejeu | `draft-ietf-httpapi-idempotency-key-header-08` | `evaluateIdempotency()` (`idempotency.ts#evaluateIdempotency`) |
660
660
  | Clé réutilisée avec un autre payload → 422 | draft §2.2 / §2.7 | `idempotency.ts#evaluateIdempotency` |
661
661
  | Exécution concurrente identique → 409 | draft §2.6 | `idempotency.ts#evaluateIdempotency` |
662
662
  | Clé requise absente → 400 | draft §2.7 | `idempotency.ts#evaluateIdempotency` |
package/docs/index.md CHANGED
@@ -108,8 +108,10 @@ dans `.ai/symbols.json` et les types générés — jamais recopiées ici, où e
108
108
  ## ⚙️ Configuration
109
109
 
110
110
  Bloc Zod (`nodefony/config/config.ts`), déclaré depuis l'application via
111
- `use("@nodefony/framework", { … })` : `router`, `adminBroker`, et `idempotency` (choix du store et
112
- réglages du GC — détaillé dans la page [Idempotence](idempotence.md)).
111
+ `use("@nodefony/framework", { … })` : `router`, `adminBroker`, `idempotency` (choix du store et
112
+ réglages du GC — détaillé dans la page [Idempotence](idempotence.md)), et `fileServing` (validateurs
113
+ et `Cache-Control` des fichiers servis par une action — détaillé dans la page
114
+ [Contrôleur](controller.md#requêtes-conditionnelles--304-412-reprise)).
113
115
 
114
116
  ## 📜 Normes appliquées
115
117
 
package/docs/templates.md CHANGED
@@ -249,6 +249,21 @@ inerte `&lt;script&gt;alert(1)&lt;/script&gt;`.
249
249
  > par un utilisateur (pseudo, commentaire, champ de formulaire, paramètre d'URL). Réserve-le à des
250
250
  > fragments HTML que ton propre code a construits.
251
251
 
252
+ **La politique de sécurité du contenu (CSP) est signée pour toi.** La politique par défaut n'exécute
253
+ un `<script>` et n'applique un `<style>` que s'ils portent le nonce de la requête. `renderView` le
254
+ pose seul sur chaque balise ÉCRITE dans le gabarit (`viewNonce.ts#signTemplateTags`) — rien à
255
+ ajouter. Il ne signe jamais :
256
+
257
+ - une balise que les données apportent (`<%~ %>`) : c'est ce qui laisse au nonce sa valeur contre
258
+ l'injection ;
259
+ - un `<script>` dont un attribut vient des données (`src="<%= it.url %>"`) : le signer exécuterait
260
+ l'URL qu'elles apportent. Si tu la tiens pour sûre, écris le nonce toi-même :
261
+ `<script nonce="<%= it.cspNonce %>" src="<%= it.url %>">` ;
262
+ - un gestionnaire en attribut (`onclick="…"`, `onload="…"`) : aucun nonce ne s'y pose, la politique
263
+ le refuse. Attache l'écouteur depuis un `<script>` du gabarit (`addEventListener`).
264
+
265
+ Le détail de la politique et ses réglages : [En-têtes de sécurité](../../security/docs/headers.md).
266
+
252
267
  ## 🧰 API publique
253
268
 
254
269
  Deux niveaux : ce que le **service Eta** expose, et ce que le **contrôleur** t'offre au-dessus.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/framework",
3
- "version": "10.0.0-beta.5",
3
+ "version": "10.0.0-beta.6",
4
4
  "description": "Le modèle de programmation Nodefony : routeur, contrôleurs, décorateurs et vues — HTTP et WebSocket dans le même contexte",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "main": "./dist/index.js",
@@ -46,23 +46,24 @@
46
46
  "dependencies": {
47
47
  "eta": "4.6.0",
48
48
  "reflect-metadata": "0.2.2",
49
- "tslib": "2.8.1"
49
+ "tslib": "2.9.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@nodefony/http": "^10.0.0-beta.5",
52
+ "@nodefony/http": "^10.0.0-beta.6",
53
53
  "@types/chai": "5.2.3",
54
- "@types/node": "26.6.5",
55
- "@vitest/coverage-v8": "5.0.5",
56
- "nodefony": "^10.0.0-beta.5",
54
+ "@types/node": "26.6.6",
55
+ "@vitest/coverage-v8": "5.1.0",
56
+ "nodefony": "^10.0.0-beta.6",
57
57
  "tsx": "4.23.15",
58
- "vitest": "5.0.5"
58
+ "vitest": "5.1.0",
59
+ "ws": "8.22.1"
59
60
  },
60
61
  "license": "Apache-2.0",
61
62
  "readmeFilename": "README.md",
62
63
  "contributors": [],
63
64
  "peerDependencies": {
64
- "@nodefony/http": "^10.0.0-beta.5",
65
- "nodefony": "^10.0.0-beta.5",
65
+ "@nodefony/http": "^10.0.0-beta.6",
66
+ "nodefony": "^10.0.0-beta.6",
66
67
  "zod": "^4.6.7"
67
68
  },
68
69
  "files": [