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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +19 -11
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +45 -6
  9. package/dist/nodefony/config/defineModuleConfig.js +2 -3
  10. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -3
  12. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  13. package/dist/nodefony/service/audit-logger.js +22 -16
  14. package/dist/nodefony/service/certificates.js +228 -19
  15. package/dist/nodefony/service/error-renderer.js +30 -13
  16. package/dist/nodefony/service/http-kernel.js +331 -82
  17. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +336 -0
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +7 -10
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +18 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
  29. package/dist/nodefony/service/sessions/sessions-service.js +34 -20
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +51 -32
  36. package/dist/nodefony/src/context/domainMatcher.js +21 -4
  37. package/dist/nodefony/src/context/forwarded.js +3 -4
  38. package/dist/nodefony/src/context/http/HttpContext.js +98 -46
  39. package/dist/nodefony/src/context/http/Request.js +34 -38
  40. package/dist/nodefony/src/context/http/Response.js +39 -7
  41. package/dist/nodefony/src/context/http/SseStream.js +349 -0
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +22 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/responseEnded.js +26 -0
  49. package/dist/nodefony/src/context/transport.js +45 -0
  50. package/dist/nodefony/src/context/trustProxy.js +1 -2
  51. package/dist/nodefony/src/context/websocket/Response.js +42 -6
  52. package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
  53. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  54. package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
  55. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  57. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  58. package/dist/nodefony/src/cookies/cookie.js +3 -7
  59. package/dist/nodefony/src/errors/httpError.js +1 -2
  60. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  61. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  62. package/dist/nodefony/src/profiler/Profiler.js +108 -30
  63. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  64. package/dist/nodefony/src/proxy/forward.js +500 -0
  65. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  66. package/dist/nodefony/src/proxy/rules.js +188 -0
  67. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  68. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  69. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  70. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  71. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  72. package/dist/nodefony/src/servers/portBinder.js +41 -10
  73. package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
  74. package/dist/nodefony/src/session/session.js +30 -15
  75. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  76. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  79. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  80. package/dist/types/index.d.ts +12 -3
  81. package/dist/types/nodefony/config/config.d.ts +16 -0
  82. package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
  83. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  84. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  85. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  86. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  87. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  88. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  89. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  90. package/dist/types/nodefony/service/certificates.d.ts +147 -5
  91. package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
  92. package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
  93. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  94. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  95. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  96. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  97. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  98. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
  99. package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
  100. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  101. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  102. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  103. package/dist/types/nodefony/src/context/Context.d.ts +35 -6
  104. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  105. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
  106. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  107. package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
  108. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  109. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  110. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  111. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  112. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  113. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  118. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  119. package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
  120. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  121. package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
  122. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  123. package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
  124. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  125. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  126. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  127. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
  128. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  129. package/docs/cookies.md +85 -78
  130. package/docs/index.md +5 -1
  131. package/docs/observabilite.md +81 -81
  132. package/docs/rate-limit.md +83 -83
  133. package/docs/reverse-proxy.md +522 -0
  134. package/docs/servers.md +378 -230
  135. package/docs/session.md +180 -180
  136. package/docs/sse.md +287 -0
  137. package/docs/upload.md +120 -117
  138. package/package.json +10 -11
  139. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,17 +1,209 @@
1
1
  import { asServersConfig } from "../src/servers/kernelServers.js";
2
2
  import { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid } from "./x509.js";
3
- import { Service, extend } from "nodefony";
3
+ import { BootConfigurationError, Service, extend } from "nodefony";
4
4
  import { X509Certificate, generateKeyPair, randomBytes } from "node:crypto";
5
5
  import fsp from "node:fs/promises";
6
6
  import path, { resolve } from "node:path";
7
+ import tls from "node:tls";
7
8
  import { promisify } from "node:util";
8
9
  import { execFile } from "node:child_process";
9
- //#region nodefony/service/certificates.ts
10
+ // nodefony/service/certificates.ts
10
11
  const execFileAsync = promisify(execFile);
11
12
  const generateKeyPairAsync = promisify(generateKeyPair);
13
+ /** Compare deux PEM sans dépendre des fins de ligne ni des espaces. */
14
+ function normalizePem(pem) {
15
+ return pem.replace(/\s+/g, "");
16
+ }
17
+ /**
18
+ * Ajoute une autorité aux ancres TLS par défaut du processus (requêtes sortantes
19
+ * `fetch`, `https`, `tls.connect`), sans en retirer aucune.
20
+ *
21
+ * C'est l'équivalent en cours d'exécution de `NODE_EXTRA_CA_CERTS`, qui n'est lu
22
+ * qu'au lancement du processus — trop tôt pour une CA que le boot vient de
23
+ * générer, et que seul un script de banc posait. La capacité se CONSTATE
24
+ * (`tls.setDefaultCACertificates` n'existe qu'à partir de Node 24.5), elle ne se
25
+ * déduit pas d'un numéro de version. Idempotent : une autorité déjà présente
26
+ * n'est pas ajoutée deux fois.
27
+ *
28
+ * @param pem - autorité au format PEM.
29
+ * @param store - accès aux ancres ; `node:tls` par défaut.
30
+ * @returns `added`, `present` (déjà de confiance), ou `unsupported`.
31
+ */
32
+ function addDefaultCaCertificate(pem, store = tls) {
33
+ const { getCACertificates, setDefaultCACertificates } = store;
34
+ if (typeof getCACertificates !== "function" || typeof setDefaultCACertificates !== "function") return "unsupported";
35
+ const current = getCACertificates("default");
36
+ const wanted = normalizePem(pem);
37
+ if (current.some((c) => normalizePem(c) === wanted)) return "present";
38
+ setDefaultCACertificates([...current, pem]);
39
+ return "added";
40
+ }
41
+ const PEM_CERTIFICATE = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;
42
+ /**
43
+ * Découpe un fichier PEM en ses certificats — une chaîne d'entreprise est
44
+ * souvent livrée en un seul fichier (racine + intermédiaires).
45
+ *
46
+ * Chaque certificat est ajouté SÉPARÉMENT : c'est ce qui rend l'ajout
47
+ * idempotent certificat par certificat, la liste par défaut du processus
48
+ * n'étant faite que de certificats isolés.
49
+ *
50
+ * @param pem - contenu d'un fichier PEM (un ou plusieurs certificats)
51
+ * @returns les certificats, dans l'ordre du fichier ; vide s'il n'y en a aucun
52
+ */
53
+ function splitPemCertificates(pem) {
54
+ return pem.match(PEM_CERTIFICATE) ?? [];
55
+ }
56
+ /**
57
+ * Ajoute les autorités d'un fichier PEM aux ancres TLS du processus
58
+ * (`certificates.extraCaFiles`) — l'équivalent, porté par l'APPLICATION, de
59
+ * `NODE_EXTRA_CA_CERTS`.
60
+ *
61
+ * `NODE_EXTRA_CA_CERTS` n'est lu qu'au lancement de Node, et se pose dans la
62
+ * syntaxe du shell — `VAR=… npm run dev` n'existe pas sous Windows. Une autorité
63
+ * d'entreprise (Keycloak interne, proxy sortant qui ré-signe) se déclare donc
64
+ * dans la configuration, et sert à TOUTES les requêtes sortantes du processus :
65
+ * découverte OpenID, point de jeton, jeu de clés, appels d'administration.
66
+ *
67
+ * @param file - chemin du fichier, absolu ou relatif à `root`
68
+ * @param root - racine de l'application
69
+ * Chaque refus dit le problème, d'où vient le chemin quand on le sait, puis le
70
+ * geste qui répare ({@link caFileRemedy}).
71
+ *
72
+ * @param store - accès aux ancres ; `node:tls` par défaut
73
+ * @param origin - phrase qui dit d'où vient le chemin (` Valeur venue de …`), insérée dans un refus
74
+ * @returns le bilan de l'ajout
75
+ * @throws BootConfigurationError - fichier illisible, sans certificat, ou Node
76
+ * incapable d'élargir ses ancres en cours d'exécution
77
+ */
78
+ async function trustCaFile(file, root, store = tls, origin = "") {
79
+ const absolute = path.resolve(root, file);
80
+ let content;
81
+ try {
82
+ content = await fsp.readFile(absolute, "utf8");
83
+ } catch (error) {
84
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
85
+ const reason = typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
86
+ throw new BootConfigurationError(`${unreadableProblem(absolute, reason)}${origin} ${caFileRemedy(reason)}`);
87
+ }
88
+ const certificates = splitPemCertificates(content);
89
+ if (certificates.length === 0) throw new BootConfigurationError(`http.certificates.extraCaFiles : « ${absolute} » ne contient aucun certificat PEM (-----BEGIN CERTIFICATE-----).${origin} ${caFileRemedy("NOPEM")}`);
90
+ let added = 0;
91
+ let present = 0;
92
+ for (const pem of certificates) {
93
+ const verdict = addDefaultCaCertificate(pem, store);
94
+ if (verdict === "unsupported") throw new BootConfigurationError(`http.certificates.extraCaFiles : ce Node ne sait pas élargir ses ancres TLS en cours d'exécution (tls.setDefaultCACertificates, Node ≥ 24.5). Mettre Node à jour, ou lancer avec NODE_EXTRA_CA_CERTS=${absolute}.`);
95
+ if (verdict === "added") added += 1;
96
+ else present += 1;
97
+ }
98
+ return {
99
+ file: absolute,
100
+ added,
101
+ present
102
+ };
103
+ }
104
+ /** Première phrase d'un refus de lecture : le fichier et la cause en un mot. */
105
+ function unreadableProblem(absolute, reason) {
106
+ return `http.certificates.extraCaFiles : « ${absolute} » est illisible (${reason}).`;
107
+ }
108
+ /**
109
+ * Le geste qui répare une autorité déclarée qu'on ne peut pas reconnaître.
110
+ *
111
+ * Un refus au démarrage qui ne dit que la cause envoie chercher ; celui-ci dit
112
+ * quoi faire. Pure : éprouvée cause par cause, y compris celles qu'une machine
113
+ * de test ne sait pas produire (un droit refusé ne l'est pas à `root`, ni
114
+ * sous Windows).
115
+ *
116
+ * @param cause - code système (`ENOENT`, `EACCES`, `EPERM`, `EISDIR`),
117
+ * `NOPEM` pour un fichier lu sans certificat, ou tout autre motif
118
+ * @returns la phrase du geste
119
+ */
120
+ function caFileRemedy(cause) {
121
+ switch (cause) {
122
+ case "ENOENT": return "Poser le fichier à ce chemin (absolu, ou relatif à la racine de l'application), ou retirer la déclaration.";
123
+ case "EACCES":
124
+ case "EPERM": return "Le compte qui lance l'application n'a pas le droit de le lire : lui donner ce droit (sous Kubernetes, monter le ConfigMap ou le Secret lisible par l'utilisateur du pod).";
125
+ case "EISDIR": return "C'est un dossier : désigner le fichier PEM qu'il contient.";
126
+ case "NOPEM": return "Le fichier doit porter le CERTIFICAT de l'autorité (pas sa clé privée), au format PEM ; un fichier DER se convertit par `openssl x509 -inform der -in autorite.der -out autorite.pem`.";
127
+ default: return "Vérifier le chemin et les droits du fichier, ou retirer la déclaration.";
128
+ }
129
+ }
130
+ /**
131
+ * Dossier où Nodefony range les certificats qu'il fabrique en développement,
132
+ * relatif au répertoire courant.
133
+ *
134
+ * @returns le chemin absolu de `nodefony/config/certificates`
135
+ */
136
+ function certificatesDirectory() {
137
+ return resolve(".", "nodefony", "config", "certificates");
138
+ }
139
+ /**
140
+ * Chemin de l'autorité de DÉVELOPPEMENT : le certificat racine que Nodefony
141
+ * fabrique lui-même pour signer le certificat du serveur local. Généré, jamais
142
+ * versionné — une seule convention, que le service et la confiance sortante lisent.
143
+ *
144
+ * @param directory - dossier des certificats ; {@link certificatesDirectory} par défaut
145
+ * @returns le chemin absolu du fichier PEM de l'autorité
146
+ */
147
+ function developmentAuthorityPath(directory = certificatesDirectory()) {
148
+ return resolve(directory, "ca", "nodefony-root-ca.crt.pem");
149
+ }
150
+ /** Vrai si le fichier n'existe pas (et seulement dans ce cas). */
151
+ async function isMissing(file) {
152
+ try {
153
+ await fsp.stat(file);
154
+ return false;
155
+ } catch (error) {
156
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
157
+ }
158
+ }
159
+ /**
160
+ * Ajoute aux ancres TLS du processus les autorités déclarées
161
+ * (`certificates.extraCaFiles`), au plus tôt : appelé à l'enregistrement du
162
+ * module, avant tout service — toute requête sortante, commande console
163
+ * comprise, les trouve en place.
164
+ *
165
+ * L'autorité de DÉVELOPPEMENT déclarée mais pas encore engendrée est ignorée en
166
+ * développement, et le journal le dit : elle n'a signé aucun certificat, il n'y
167
+ * a donc rien à lui faire confiance, et le serveur de développement la pose
168
+ * lui-même quand il la fabrique ({@link Certificate.generateServerCertificates}).
169
+ * Rien n'est fabriqué ici : une commande console n'écrit pas de certificat.
170
+ * Une autre autorité absente, ou n'importe laquelle hors développement, reste
171
+ * un refus — qui nomme la variable d'origine et le geste qui répare
172
+ * ({@link caFileRemedy}).
173
+ *
174
+ * @param files - `certificates.extraCaFiles`
175
+ * @param root - racine de l'application
176
+ * @param log - journal du module
177
+ * @param context - décor (environnement, autorité de dev, ancres)
178
+ * @throws BootConfigurationError - cf {@link trustCaFile}
179
+ */
180
+ async function trustOutboundAuthorities(files, root, log, context = {}) {
181
+ const developmentAuthority = context.developmentAuthority ?? developmentAuthorityPath();
182
+ for (const file of files) {
183
+ const absolute = path.resolve(root, file);
184
+ const isDevelopmentAuthority = absolute === developmentAuthority;
185
+ if (isDevelopmentAuthority && context.development === true && await isMissing(absolute)) {
186
+ log(`autorité de développement ${absolute} pas encore engendrée : ignorée. Elle naît au premier \`npx nodefony development\` (ou \`npx nodefony http:certificates\`), et le serveur de développement lui fait alors confiance de lui-même.`, "INFO");
187
+ continue;
188
+ }
189
+ const origin = originOf(absolute, root, context.env ?? process.env);
190
+ if (isDevelopmentAuthority && await isMissing(absolute)) throw new BootConfigurationError(`${unreadableProblem(absolute, "ENOENT")}${origin} C'est l'autorité de DÉVELOPPEMENT : Nodefony la fabrique au premier \`npx nodefony development\`, jamais hors développement. Pour essayer la production sur ce poste, l'engendrer une fois (\`npx nodefony http:certificates\`) puis relancer ; sinon retirer la déclaration — une production réelle n'en a pas besoin.`);
191
+ const trust = await trustCaFile(file, root, context.store, origin);
192
+ log(`autorité sortante ${trust.file} : ${trust.added} ajoutée(s), ${trust.present} déjà présente(s)`, "INFO");
193
+ }
194
+ }
195
+ /**
196
+ * Nomme la variable d'où vient le chemin, quand c'est `NF_TLS_CA_FILE` — la
197
+ * déclaration que pose le gabarit d'application. Constaté sur la valeur, jamais
198
+ * supposé : une configuration écrite à la main n'est pas attribuée à la variable.
199
+ */
200
+ function originOf(absolute, root, env) {
201
+ const declared = env.NF_TLS_CA_FILE;
202
+ return declared && path.resolve(root, declared) === absolute ? " Valeur venue de NF_TLS_CA_FILE (`.env` ou environnement du processus)." : "";
203
+ }
12
204
  /**
13
205
  * Vrai si `host` est une IP littérale (IPv4 `n.n.n.n` ou IPv6 — contient `:`).
14
- * Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 6125).
206
+ * Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 9525).
15
207
  */
16
208
  function isIpLiteral(host) {
17
209
  return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
@@ -44,7 +236,7 @@ const defaultOptions = {
44
236
  *
45
237
  * Conformité (génération auto-signée) : signature SHA-256 (jamais SHA-1,
46
238
  * RFC 5280 / CA-B Forum), numéro de série aléatoire 128 bits unique
47
- * (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 6125), `notBefore` reculé,
239
+ * (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 9525), `notBefore` reculé,
48
240
  * clé privée écrite en `0600`.
49
241
  */
50
242
  var Certificate = class Certificate extends Service {
@@ -56,13 +248,11 @@ var Certificate = class Certificate extends Service {
56
248
  cert = "";
57
249
  fullchainPem = "";
58
250
  publicKeyPem = "";
59
- rootCertPem = "";
60
- intermediateCertPem = "";
61
251
  /** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
62
252
  mkcertCaRoot = null;
63
- path = resolve(".", "nodefony", "config", "certificates");
253
+ path = certificatesDirectory();
64
254
  serverPath = resolve(this.path, "server");
65
- caPath = resolve(this.path, "ca", "nodefony-root-ca.crt.pem");
255
+ caPath = developmentAuthorityPath(this.path);
66
256
  publicKeyPath = resolve(this.path, "server", "publickey.pem");
67
257
  privateKeyPath = resolve(this.path, "server", "privkey.pem");
68
258
  certPath = resolve(this.path, "server", "cert.pem");
@@ -181,7 +371,11 @@ var Certificate = class Certificate extends Service {
181
371
  await this.ensureDirectoriesExist();
182
372
  const strategy = await this.resolveStrategy();
183
373
  if (strategy === "explicit") return this.loadExplicitCert();
184
- if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) return this.readCerticates();
374
+ if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) {
375
+ await this.readCerticates();
376
+ this.trustDevelopmentAuthority();
377
+ return this;
378
+ }
185
379
  if (strategy === "mkcert") await this.generateWithMkcert();
186
380
  else {
187
381
  this.keysPair = await this.generateKeys();
@@ -195,7 +389,26 @@ var Certificate = class Certificate extends Service {
195
389
  await this.writeCertificates(true);
196
390
  await fsp.writeFile(this.caPath, certPem.toString(), "utf8");
197
391
  }
198
- return this.readCerticates();
392
+ await this.readCerticates();
393
+ this.trustDevelopmentAuthority();
394
+ return this;
395
+ }
396
+ /**
397
+ * En développement, fait confiance à la CA que Nodefony vient de générer ou de
398
+ * relire, pour les requêtes SORTANTES du processus.
399
+ *
400
+ * Sans elle, un service du décor qui sert le certificat de l'application
401
+ * (Keycloak, l'auto-vérification d'un jeton MCP) est injoignable depuis
402
+ * `nodefony development` — `fetch failed` / `SELF_SIGNED_CERT_IN_CHAIN` — alors
403
+ * que `curl -k` répond : la confiance n'existait que sous le script de banc.
404
+ * Réservé au DÉVELOPPEMENT et aux certificats que Nodefony fabrique : un
405
+ * certificat `explicit` (production) n'élargit jamais la confiance du processus.
406
+ */
407
+ trustDevelopmentAuthority() {
408
+ if (!this.isDev() || !this.ca || this.ca.length === 0) return;
409
+ const verdict = addDefaultCaCertificate(this.ca.toString());
410
+ if (verdict === "added") this.log(`CA de développement ajoutée aux ancres TLS du processus (${this.caPath})`, "DEBUG");
411
+ else if (verdict === "unsupported") this.log(`Ce Node ne sait pas élargir ses ancres TLS en cours d'exécution (tls.setDefaultCACertificates, Node ≥ 24.5) : les requêtes sortantes vers un service qui sert le certificat de développement échoueront. Relancer avec NODE_EXTRA_CA_CERTS=${this.caPath}`, "WARNING");
199
412
  }
200
413
  /**
201
414
  * Résout la stratégie effective à partir de `certificates.strategy` :
@@ -249,7 +462,7 @@ var Certificate = class Certificate extends Service {
249
462
  /**
250
463
  * SAN effectif : config explicite si fournie, sinon dérivé du kernel
251
464
  * (localhost + domain en DNS ; loopback en IP). Une IP littérale (ex. domain
252
- * `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 6125).
465
+ * `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 9525).
253
466
  */
254
467
  derivedSan() {
255
468
  const san = this.certOptions.san;
@@ -329,7 +542,7 @@ var Certificate = class Certificate extends Service {
329
542
  return false;
330
543
  }
331
544
  }
332
- /** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 6125) ? */
545
+ /** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 9525) ? */
333
546
  sanCovers(presentDns) {
334
547
  return this.sanDnsNames().every((name) => presentDns.includes(name));
335
548
  }
@@ -351,11 +564,8 @@ var Certificate = class Certificate extends Service {
351
564
  return Buffer.from(await fsp.readFile(value, "utf8"));
352
565
  }
353
566
  createFullChain() {
354
- return [
355
- this.cert,
356
- this.intermediateCertPem,
357
- this.rootCertPem
358
- ].map((part) => part?.toString().trim()).filter(Boolean).join("\n");
567
+ const pem = this.cert?.toString().trim() ?? "";
568
+ return pem === "" ? "" : `${pem}\n`;
359
569
  }
360
570
  async readCerticates() {
361
571
  for (const file of this.files) try {
@@ -509,5 +719,4 @@ var Certificate = class Certificate extends Service {
509
719
  }
510
720
  }
511
721
  };
512
- //#endregion
513
- export { Certificate as default };
722
+ export { addDefaultCaCertificate, caFileRemedy, certificatesDirectory, Certificate as default, developmentAuthorityPath, splitPemCertificates, trustCaFile, trustOutboundAuthorities };
@@ -1,7 +1,7 @@
1
1
  import HttpError from "../src/errors/httpError.js";
2
2
  import { toWsCloseCode } from "../src/context/websocket/WebsocketContext.js";
3
3
  import { Nodefony, nodefonyError } from "nodefony";
4
- //#region nodefony/service/error-renderer.ts
4
+ // nodefony/service/error-renderer.ts
5
5
  /** Une donnée rejetée par la validation n'est pas une requête malformée : 422, pas 400. */
6
6
  const VALIDATION_STATUS = 422;
7
7
  /**
@@ -74,8 +74,15 @@ const INTERNAL_ERROR_KEYS = [
74
74
  * deux tests — un gate qu'on ne peut plus voir mordre.
75
75
  */
76
76
  const isProduction = () => Nodefony.getKernel()?.environment === "production";
77
+ /** Une propriété d'une anomalie de forme inconnue, lue sans rien supposer. */
78
+ function readIssueProperty(issue, key) {
79
+ return typeof issue === "object" && issue !== null ? Reflect.get(issue, key) : void 0;
80
+ }
81
+ /** Erreurs de validation reconnues : Zod, et celle des routes à schéma (cœur). */
82
+ const VALIDATION_ERROR_NAMES = /* @__PURE__ */ new Set(["ZodError", "SchemaValidationError"]);
77
83
  /**
78
- * Reconnaît une erreur de validation Zod et en extrait les champs fautifs.
84
+ * Reconnaît une erreur de validation — `ZodError`, ou `SchemaValidationError`
85
+ * d'une route à schéma — et en extrait les champs fautifs.
79
86
  *
80
87
  * Reconnaissance **structurelle** (`name` + `issues`), pas `instanceof ZodError` :
81
88
  * une application peut embarquer sa propre copie de zod (résolutions npm multiples),
@@ -88,13 +95,21 @@ const isProduction = () => Nodefony.getKernel()?.environment === "production";
88
95
  * @returns les champs fautifs, ou `null` si ce n'est pas une erreur de validation.
89
96
  */
90
97
  function toValidationFields(error) {
91
- const candidate = error;
92
- if (error.name !== "ZodError" || !Array.isArray(candidate.issues)) return null;
93
- return candidate.issues.map((issue) => ({
94
- field: Array.isArray(issue.path) ? issue.path.join(".") : "",
95
- message: issue.message ?? "invalide",
96
- rule: issue.code
97
- }));
98
+ const issues = Reflect.get(error, "issues");
99
+ if (!VALIDATION_ERROR_NAMES.has(error.name) || !Array.isArray(issues)) return null;
100
+ return issues.map((issue) => {
101
+ const path = readIssueProperty(issue, "path");
102
+ const message = readIssueProperty(issue, "message");
103
+ const code = readIssueProperty(issue, "code");
104
+ const part = readIssueProperty(issue, "in");
105
+ const field = {
106
+ field: Array.isArray(path) ? path.map(String).join(".") : "",
107
+ message: typeof message === "string" ? message : "invalide",
108
+ rule: typeof code === "string" ? code : void 0
109
+ };
110
+ if (typeof part === "string") field.in = part;
111
+ return field;
112
+ });
98
113
  }
99
114
  /**
100
115
  * Construit l'erreur **422** exposée au client à partir des champs fautifs.
@@ -109,7 +124,10 @@ function toValidationFields(error) {
109
124
  * reçoit `error.fields` et sait QUEL champ corriger — pas seulement que « ça a échoué ».
110
125
  */
111
126
  function toValidationError(error, fields) {
112
- const summary = fields.map((f) => f.field ? `${f.field}: ${f.message}` : f.message).join(" · ");
127
+ const summary = fields.map((f) => {
128
+ const where = f.in && f.field ? `${f.in}.${f.field}` : f.in || f.field;
129
+ return where ? `${where}: ${f.message}` : f.message;
130
+ }).join(" · ");
113
131
  const validation = new nodefonyError(`Validation failed — ${summary}`, VALIDATION_STATUS);
114
132
  if (error.stack !== void 0) validation.stack = error.stack;
115
133
  validation.fields = fields;
@@ -348,7 +366,7 @@ const SCHEMA_MISMATCH_HINT = {
348
366
  * reason: error.message
349
367
  *
350
368
  * Stateless singleton — zero per-request allocation. Override via
351
- * `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC 7807.
369
+ * `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC 9457.
352
370
  */
353
371
  var DefaultErrorRenderer = class {
354
372
  renderHttp(error, context) {
@@ -429,5 +447,4 @@ var DefaultErrorRenderer = class {
429
447
  return code;
430
448
  }
431
449
  };
432
- //#endregion
433
- export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf };
450
+ export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf, toValidationFields };