@nodefony/http 10.0.0-alpha.1

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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,545 @@
1
+ import { Service, extend } from "nodefony";
2
+ import { randomBytes } from "node:crypto";
3
+ import fsp from "node:fs/promises";
4
+ import path, { resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { execFile } from "node:child_process";
7
+ //#region nodefony/service/certificates.ts
8
+ const execFileAsync = promisify(execFile);
9
+ /**
10
+ * Vrai si `host` est une IP littérale (IPv4 `n.n.n.n` ou IPv6 — contient `:`).
11
+ * Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 6125).
12
+ */
13
+ function isIpLiteral(host) {
14
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
15
+ }
16
+ const defaultOptions = {
17
+ path: resolve(".", "nodefony", "config", "certificates"),
18
+ strategy: "auto",
19
+ privateKeyMode: 384,
20
+ dev: { useMkcert: true },
21
+ san: {
22
+ dns: [],
23
+ ip: []
24
+ },
25
+ openssl: {
26
+ size: 2048,
27
+ hash: "sha256",
28
+ validityDays: 365,
29
+ backdateMinutes: 5,
30
+ attrs: []
31
+ }
32
+ };
33
+ /**
34
+ * Service de fourniture du certificat TLS du serveur HTTPS/WSS.
35
+ *
36
+ * Trois stratégies : `explicit` (certificat fourni en config — le cas de
37
+ * PRODUCTION), `mkcert` (CA locale trustée, confort de dev) et `selfsigned`
38
+ * (auto-signé node-forge, secours). La génération est réservée au
39
+ * DÉVELOPPEMENT : en production sans certificat fourni, le service crie un
40
+ * avertissement (Nodefony n'est pas une autorité de certification).
41
+ *
42
+ * Conformité (génération auto-signée) : signature SHA-256 (jamais SHA-1,
43
+ * RFC 5280 / CA-B Forum), numéro de série aléatoire 128 bits unique
44
+ * (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 6125), `notBefore` reculé,
45
+ * clé privée écrite en `0600`.
46
+ */
47
+ var Certificate = class Certificate extends Service {
48
+ module;
49
+ files = [];
50
+ keysPair = null;
51
+ certForge = null;
52
+ ca = "";
53
+ key = "";
54
+ cert = "";
55
+ fullchainPem = "";
56
+ publicKeyPem = "";
57
+ rootCertPem = "";
58
+ intermediateCertPem = "";
59
+ /** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
60
+ mkcertCaRoot = null;
61
+ /**
62
+ * Backend crypto node-forge, chargé PARESSEUSEMENT. La génération de
63
+ * certificat est un confort de DÉVELOPPEMENT : en production avec un
64
+ * certificat fourni (`strategy: "explicit"`), cette grosse dépendance n'est
65
+ * jamais importée (règle perf-mémoire — pas de dep chargée « au cas où »).
66
+ */
67
+ forge = null;
68
+ path = resolve(".", "nodefony", "config", "certificates");
69
+ serverPath = resolve(this.path, "server");
70
+ caPath = resolve(this.path, "ca", "nodefony-root-ca.crt.pem");
71
+ publicKeyPath = resolve(this.path, "server", "publickey.pem");
72
+ privateKeyPath = resolve(this.path, "server", "privkey.pem");
73
+ certPath = resolve(this.path, "server", "cert.pem");
74
+ fullchainPath = resolve(this.path, "server", "fullchain.pem");
75
+ constructor(module) {
76
+ super("certificates", module.container, module.notificationsCenter, extend(true, {}, defaultOptions, module.options.certificates || {}));
77
+ this.module = module;
78
+ }
79
+ /** Accès typé aux options du service (Service.options est volontairement lâche). */
80
+ get certOptions() {
81
+ return this.options;
82
+ }
83
+ /** Vrai en environnement de développement (mkcert réservé à ce mode). */
84
+ isDev() {
85
+ return this.kernel?.environment === "development";
86
+ }
87
+ /**
88
+ * Charge node-forge à la demande (idempotent) — uniquement sur le chemin de
89
+ * GÉNÉRATION (selfsigned / mkcert / inspection). Le chemin `explicit` (prod)
90
+ * ne l'appelle jamais.
91
+ */
92
+ async loadForge() {
93
+ if (!this.forge) this.forge = (await import("node-forge")).default;
94
+ return this.forge;
95
+ }
96
+ /** Accès au backend forge déjà chargé (lève si `loadForge` n'a pas été appelé). */
97
+ get forgeLib() {
98
+ if (!this.forge) throw new Error("node-forge non chargé — appeler loadForge() avant toute génération.");
99
+ return this.forge;
100
+ }
101
+ async init() {
102
+ this.kernel?.once("onBoot", async () => {
103
+ this.options = extend(true, this.options, this.module.options.certificates || {});
104
+ await this.generateServerCertificates();
105
+ });
106
+ return this;
107
+ }
108
+ /**
109
+ * Numéro de série X.509 — RFC 5280 §4.1.2.2 : entier positif unique par CA.
110
+ * 128 bits aléatoires (≥ 64 bits d'entropie exigés par le CA/Browser Forum
111
+ * contre les attaques par collision) ; bit de poids fort à 0 pour garantir un
112
+ * entier positif en encodage DER ; ≤ 20 octets.
113
+ */
114
+ static generateSerialHex() {
115
+ const bytes = randomBytes(16);
116
+ bytes[0] &= 127;
117
+ if (bytes[0] === 0) bytes[0] = 1;
118
+ return bytes.toString("hex");
119
+ }
120
+ setFiles() {
121
+ this.files = [
122
+ {
123
+ path: this.privateKeyPath,
124
+ variable: this.key
125
+ },
126
+ {
127
+ path: this.publicKeyPath,
128
+ variable: this.publicKeyPem
129
+ },
130
+ {
131
+ path: this.certPath,
132
+ variable: this.cert
133
+ },
134
+ {
135
+ path: this.fullchainPath,
136
+ variable: this.fullchainPem
137
+ }
138
+ ];
139
+ }
140
+ async checkCertificates() {
141
+ try {
142
+ await Promise.all(this.files.map((file) => fsp.access(file.path)));
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+ async ensureDirectoriesExist() {
149
+ const directories = [
150
+ this.path,
151
+ this.serverPath,
152
+ path.dirname(this.privateKeyPath),
153
+ path.dirname(this.caPath),
154
+ path.dirname(this.certPath),
155
+ path.dirname(this.fullchainPath)
156
+ ];
157
+ for await (const directory of directories) try {
158
+ await fsp.access(directory);
159
+ } catch {
160
+ await fsp.mkdir(directory, { recursive: true });
161
+ this.log(`Directory created: ${directory}`, "DEBUG");
162
+ }
163
+ }
164
+ /**
165
+ * Génère (ou recharge) le certificat serveur selon la stratégie résolue :
166
+ * `explicit` (fourni en config / prod), `mkcert` (dev, CA trustée) ou
167
+ * `selfsigned` (auto-signé node-forge, secours). Régénère automatiquement si
168
+ * le certificat présent sur disque n'est pas adéquat pour la stratégie active
169
+ * (expiré, SHA-1, SAN incomplet).
170
+ *
171
+ * @param force - forcer la régénération même si un certificat valide existe
172
+ */
173
+ async generateServerCertificates(force = false) {
174
+ this.setFiles();
175
+ await this.ensureDirectoriesExist();
176
+ const strategy = await this.resolveStrategy();
177
+ if (strategy === "explicit") return this.loadExplicitCert();
178
+ await this.loadForge();
179
+ if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) return this.readCerticates();
180
+ if (strategy === "mkcert") await this.generateWithMkcert();
181
+ else {
182
+ this.keysPair = this.generateKeys();
183
+ this.certForge = this.createCertificate();
184
+ this.setExtension();
185
+ this.sign();
186
+ this.key = this.generatePrivateKeyPem();
187
+ this.publicKeyPem = this.generatePublickeyPem();
188
+ const certPem = this.generateCertPem();
189
+ this.cert = certPem;
190
+ this.ca = certPem;
191
+ this.fullchainPem = this.createFullChain();
192
+ this.setFiles();
193
+ await this.writeCertificates(true);
194
+ await fsp.writeFile(this.caPath, certPem.toString(), "utf8");
195
+ }
196
+ return this.readCerticates();
197
+ }
198
+ /**
199
+ * Résout la stratégie effective à partir de `certificates.strategy` :
200
+ * - `explicit` : `key` + `cert` fournis (prod, Let's Encrypt…). Forcé →
201
+ * erreur si absents.
202
+ * - `mkcert` : dev + binaire mkcert + CA locale présents. Forcé hors dev →
203
+ * retombe sur `selfsigned` avec avertissement.
204
+ * - `selfsigned` : auto-signé node-forge (fallback). En PRODUCTION, crie un
205
+ * avertissement : la génération n'est pas le rôle d'un serveur de prod.
206
+ */
207
+ async resolveStrategy() {
208
+ const requested = this.certOptions.strategy ?? "auto";
209
+ if (requested === "explicit") {
210
+ if (!this.hasExplicitCert()) throw new Error("certificates.strategy='explicit' mais key/cert absents de la configuration.");
211
+ return "explicit";
212
+ }
213
+ if (requested === "auto" && this.hasExplicitCert()) return "explicit";
214
+ if (requested === "mkcert" || requested === "auto") {
215
+ if (this.isDev() && this.certOptions.dev?.useMkcert !== false) {
216
+ const caRoot = await this.detectMkcert();
217
+ if (caRoot) {
218
+ this.mkcertCaRoot = caRoot;
219
+ return "mkcert";
220
+ }
221
+ this.log((requested === "mkcert" ? "strategy='mkcert' mais mkcert introuvable — " : "mkcert introuvable — ") + "fallback certificat auto-signé node-forge (non trusté). `brew install mkcert nss && mkcert -install` pour un HTTPS dev sans erreur (HMR cross-origin/WSS).", "WARNING");
222
+ } else if (requested === "mkcert") this.log("strategy='mkcert' ignoré hors development → certificat auto-signé.", "WARNING");
223
+ }
224
+ if (!this.isDev()) this.log("Aucun certificat TLS fourni en PRODUCTION : génération d'un auto-signé NON trusté (secours). Nodefony n'est PAS une autorité de certification de production — fournissez un vrai certificat (Let's Encrypt, ingress k8s, reverse-proxy edge) via certificates.{ key, cert, ca } (strategy='explicit').", "WARNING");
225
+ return "selfsigned";
226
+ }
227
+ /** `key` + `cert` présents en config (chemin fichier ou Buffer) → cert fourni. */
228
+ hasExplicitCert() {
229
+ const o = this.certOptions;
230
+ return Boolean(o.key) && Boolean(o.cert);
231
+ }
232
+ /**
233
+ * Détecte mkcert : binaire dans le PATH + CA racine générée (rootCA.pem).
234
+ * @returns le chemin CAROOT, ou null si indisponible.
235
+ */
236
+ async detectMkcert() {
237
+ try {
238
+ const { stdout } = await execFileAsync("mkcert", ["-CAROOT"]);
239
+ const caRoot = stdout.trim();
240
+ if (!caRoot) return null;
241
+ await fsp.access(path.join(caRoot, "rootCA.pem"));
242
+ return caRoot;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ /**
248
+ * SAN effectif : config explicite si fournie, sinon dérivé du kernel
249
+ * (localhost + domain en DNS ; loopback en IP). Une IP littérale (ex. domain
250
+ * `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 6125).
251
+ */
252
+ derivedSan() {
253
+ const san = this.certOptions.san;
254
+ if (san && (san.dns.length > 0 || san.ip.length > 0)) return san;
255
+ const dns = ["localhost"];
256
+ const ip = ["127.0.0.1", "::1"];
257
+ const domain = this.kernel?.domain;
258
+ if (domain && domain !== "localhost" && domain !== "0.0.0.0") {
259
+ if (isIpLiteral(domain)) {
260
+ if (!ip.includes(domain)) ip.unshift(domain);
261
+ } else dns.unshift(domain);
262
+ }
263
+ return {
264
+ dns,
265
+ ip
266
+ };
267
+ }
268
+ /** Hostnames DNS couverts par le SAN. */
269
+ sanDnsNames() {
270
+ return this.derivedSan().dns;
271
+ }
272
+ /** Adresses IP couvertes par le SAN. */
273
+ sanIps() {
274
+ return this.derivedSan().ip;
275
+ }
276
+ /** Hostnames passés à mkcert (DNS + IP du SAN). */
277
+ certHostnames() {
278
+ return [...this.sanDnsNames(), ...this.sanIps()];
279
+ }
280
+ /**
281
+ * Génère le certificat de dev via mkcert (signé par la CA locale trustée).
282
+ * Écrit cert + clé privée (mkcert), puis dérive clé publique, fullchain et CA.
283
+ */
284
+ async generateWithMkcert() {
285
+ const caRoot = this.mkcertCaRoot ?? await this.detectMkcert();
286
+ if (!caRoot) throw new Error("mkcert CAROOT introuvable");
287
+ const names = this.certHostnames();
288
+ await execFileAsync("mkcert", [
289
+ "-cert-file",
290
+ this.certPath,
291
+ "-key-file",
292
+ this.privateKeyPath,
293
+ ...names
294
+ ]);
295
+ await this.restrictPrivateKey();
296
+ const rootCaPem = await fsp.readFile(path.join(caRoot, "rootCA.pem"), "utf8");
297
+ const certPem = await fsp.readFile(this.certPath, "utf8");
298
+ const { pki } = this.forgeLib;
299
+ const publicKeyPem = pki.publicKeyToPem(pki.certificateFromPem(certPem).publicKey);
300
+ await fsp.writeFile(this.publicKeyPath, publicKeyPem, "utf8");
301
+ await fsp.writeFile(this.fullchainPath, `${certPem}${rootCaPem}`, "utf8");
302
+ await fsp.writeFile(this.caPath, rootCaPem, "utf8");
303
+ this.log(`Certificat dev généré via mkcert (CA trustée) — ${names.join(", ")}`, "INFO");
304
+ }
305
+ /**
306
+ * Vérifie que le certificat présent sur disque convient à la stratégie :
307
+ * - expiration (RFC 5280 §4.1.2.5) : un cert expiré est inadéquat.
308
+ * - **SAN** couvrant les hostnames requis (les DEUX stratégies) : si le SAN
309
+ * demandé change (ex. `nodefony.com` ajouté via NF_BIND_ALL), on régénère.
310
+ * - `mkcert` : émis par la CA mkcert (issuer organisation contient "mkcert").
311
+ * - `selfsigned` : signature non SHA-1.
312
+ * @returns false si absent, illisible ou inadéquat → déclenche la régénération.
313
+ */
314
+ async isCertAdequate(strategy) {
315
+ try {
316
+ const { pki } = this.forgeLib;
317
+ const certPem = await fsp.readFile(this.certPath, "utf8");
318
+ const cert = pki.certificateFromPem(certPem);
319
+ if (cert.validity.notAfter.getTime() <= Date.now()) return false;
320
+ const ext = cert.getExtension("subjectAltName");
321
+ if (!ext || !this.sanCovers(ext.altNames ?? [])) return false;
322
+ if (strategy === "mkcert") {
323
+ const org = cert.issuer.getField("O");
324
+ return Boolean(org && /mkcert/i.test(String(org.value)));
325
+ }
326
+ return cert.signatureOid !== pki.oids.sha1WithRSAEncryption;
327
+ } catch {
328
+ return false;
329
+ }
330
+ }
331
+ /** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 6125) ? */
332
+ sanCovers(present) {
333
+ const presentDns = new Set(present.filter((p) => p.type === 2 && p.value).map((p) => p.value));
334
+ return this.sanDnsNames().every((name) => presentDns.has(name));
335
+ }
336
+ /** Charge un certificat fourni en config (chemin fichier ou Buffer). */
337
+ async loadExplicitCert() {
338
+ const o = this.certOptions;
339
+ this.key = await this.resolveMaterial(o.key);
340
+ this.cert = await this.resolveMaterial(o.cert);
341
+ this.fullchainPem = this.cert;
342
+ if (o.ca) this.ca = await this.resolveMaterial(o.ca);
343
+ this.setFiles();
344
+ this.log("Certificat TLS chargé depuis la configuration (fourni).", "INFO");
345
+ return this;
346
+ }
347
+ /** Résout un matériel TLS : Buffer renvoyé tel quel, string lue comme chemin. */
348
+ async resolveMaterial(value) {
349
+ if (!value) throw new Error("certificate material is empty");
350
+ if (Buffer.isBuffer(value)) return value;
351
+ return Buffer.from(await fsp.readFile(value, "utf8"));
352
+ }
353
+ createFullChain() {
354
+ return [
355
+ this.cert,
356
+ this.intermediateCertPem,
357
+ this.rootCertPem
358
+ ].map((part) => part?.toString().trim()).filter(Boolean).join("\n");
359
+ }
360
+ async readCerticates() {
361
+ for (const file of this.files) try {
362
+ const buf = Buffer.from(await fsp.readFile(file.path, "utf8"));
363
+ if (file.path === this.privateKeyPath) this.key = buf;
364
+ else if (file.path === this.publicKeyPath) this.publicKeyPem = buf;
365
+ else if (file.path === this.caPath) this.ca = buf;
366
+ else if (file.path === this.certPath) this.cert = buf;
367
+ else if (file.path === this.fullchainPath) this.fullchainPem = buf;
368
+ this.log(`Read Certificat file ${file.path}`, "DEBUG");
369
+ } catch (err) {
370
+ this.log(err, "WARNING");
371
+ }
372
+ try {
373
+ this.ca = Buffer.from(await fsp.readFile(this.caPath, "utf8"));
374
+ } catch {}
375
+ return this;
376
+ }
377
+ async writeCertificates(force = false) {
378
+ await this.ensureDirectoriesExist();
379
+ for await (const file of this.files) try {
380
+ if (file.variable) {
381
+ const mode = file.path === this.privateKeyPath ? this.certOptions.privateKeyMode ?? 384 : 420;
382
+ if (force) await fsp.rm(file.path, { force: true });
383
+ try {
384
+ await fsp.writeFile(file.path, file.variable.toString(), {
385
+ encoding: "utf8",
386
+ flag: "wx",
387
+ mode
388
+ });
389
+ } catch (error) {
390
+ if (error.code === "EEXIST") {
391
+ this.log(`File ${file.path} already exists, skipping.`, "DEBUG");
392
+ continue;
393
+ }
394
+ throw error;
395
+ }
396
+ this.log(`Certificate file ${file.path} written successfully.`, "INFO");
397
+ }
398
+ } catch (err) {
399
+ this.log(`Error writing to file ${file.path}`, "ERROR");
400
+ this.log(err, "ERROR");
401
+ throw err;
402
+ }
403
+ await this.restrictPrivateKey();
404
+ return this;
405
+ }
406
+ /**
407
+ * Durcit les permissions de la clé privée (et de son dossier) après écriture
408
+ * — garantit `0600` même si l'umask du process était permissif (la clé écrite
409
+ * par mkcert passe aussi par ici). Échec silencieux hors POSIX.
410
+ */
411
+ async restrictPrivateKey() {
412
+ const mode = this.certOptions.privateKeyMode ?? 384;
413
+ try {
414
+ await fsp.chmod(this.privateKeyPath, mode);
415
+ await fsp.chmod(this.serverPath, 448);
416
+ } catch (err) {
417
+ this.log(err, "DEBUG");
418
+ }
419
+ }
420
+ generateKeys() {
421
+ return this.forgeLib.pki.rsa.generateKeyPair(this.certOptions.openssl.size);
422
+ }
423
+ generatePrivateKeyPem() {
424
+ if (this.keysPair) return Buffer.from(this.forgeLib.pki.privateKeyToPem(this.keysPair.privateKey));
425
+ throw new Error(`pki.rsa.KeyPair not found`);
426
+ }
427
+ generatePublickeyPem() {
428
+ if (this.keysPair) return Buffer.from(this.forgeLib.pki.publicKeyToPem(this.keysPair.publicKey));
429
+ throw new Error(`pki.rsa.KeyPair not found`);
430
+ }
431
+ generateCertPem() {
432
+ if (this.certForge) return Buffer.from(this.forgeLib.pki.certificateToPem(this.certForge));
433
+ throw new Error(`pki.Certificate not found`);
434
+ }
435
+ createCertificate() {
436
+ if (!this.keysPair) throw new Error(`KeyPair not found`);
437
+ const o = this.certOptions.openssl;
438
+ const cert = this.forgeLib.pki.createCertificate();
439
+ cert.publicKey = this.keysPair.publicKey;
440
+ cert.serialNumber = Certificate.generateSerialHex();
441
+ const backdateMs = o.backdateMinutes * 6e4;
442
+ const start = Date.now() - backdateMs;
443
+ cert.validity.notBefore = new Date(start);
444
+ cert.validity.notAfter = new Date(start + o.validityDays * 864e5);
445
+ cert.setSubject(o.attrs);
446
+ cert.setIssuer(o.attrs);
447
+ return cert;
448
+ }
449
+ /** subjectAltName de l'auto-signé : DNS + IP (config explicite sinon dérivé). */
450
+ altNames() {
451
+ const out = [];
452
+ for (const dns of this.sanDnsNames()) out.push({
453
+ type: 2,
454
+ value: dns
455
+ });
456
+ for (const ip of this.sanIps()) out.push({
457
+ type: 7,
458
+ ip
459
+ });
460
+ return out;
461
+ }
462
+ setExtension() {
463
+ if (!this.certForge) throw new Error(`pki.Certificate not found`);
464
+ this.certForge.setExtensions([
465
+ {
466
+ name: "basicConstraints",
467
+ cA: false
468
+ },
469
+ {
470
+ name: "keyUsage",
471
+ digitalSignature: true,
472
+ keyEncipherment: true
473
+ },
474
+ {
475
+ name: "extKeyUsage",
476
+ serverAuth: true,
477
+ clientAuth: true
478
+ },
479
+ {
480
+ name: "subjectAltName",
481
+ altNames: this.altNames()
482
+ },
483
+ { name: "subjectKeyIdentifier" }
484
+ ]);
485
+ }
486
+ /**
487
+ * Signe le certificat avec le hachage configuré (SHA-256 par défaut).
488
+ * SHA-1 est INTERDIT (collision SHAttered 2017, CA/Browser Forum depuis 2016) :
489
+ * `node-forge` signe en SHA-1 par défaut si on ne passe pas de digest → on en
490
+ * passe toujours un.
491
+ */
492
+ sign() {
493
+ if (!this.certForge || !this.keysPair) throw new Error(`pki.rsa.KeyPair or pki.Certificate not found`);
494
+ this.certForge.sign(this.keysPair.privateKey, this.digestFor(this.certOptions.openssl.hash));
495
+ }
496
+ /**
497
+ * Résumé introspectable du certificat serveur courant — réutilisé par la
498
+ * commande CLI `certificates` et un futur endpoint d'admin Studio (parité
499
+ * CLI ↔ Web). Parse le certificat chargé (charge node-forge à la demande).
500
+ */
501
+ async describe() {
502
+ const info = {
503
+ strategy: this.certOptions.strategy ?? "auto",
504
+ certPath: this.certPath,
505
+ keyPath: this.privateKeyPath,
506
+ fullchainPath: this.fullchainPath
507
+ };
508
+ try {
509
+ await fsp.access(this.caPath);
510
+ info.caPath = this.caPath;
511
+ } catch {}
512
+ if (!this.cert) return info;
513
+ const { pki } = await this.loadForge();
514
+ try {
515
+ const cert = pki.certificateFromPem(this.cert.toString());
516
+ info.serial = cert.serialNumber;
517
+ info.validFrom = cert.validity.notBefore.toISOString();
518
+ info.validTo = cert.validity.notAfter.toISOString();
519
+ info.signatureAlgorithm = this.oidName(cert.signatureOid, pki);
520
+ const cn = cert.subject.getField("CN");
521
+ if (cn) info.commonName = String(cn.value);
522
+ const ext = cert.getExtension("subjectAltName");
523
+ if (ext?.altNames) info.san = ext.altNames.map((a) => a.type === 7 ? a.ip ?? "" : a.value ?? "");
524
+ } catch {}
525
+ return info;
526
+ }
527
+ /** Nom lisible de l'OID d'algorithme de signature. */
528
+ oidName(oid, pki) {
529
+ return pki.oids[oid] ?? oid;
530
+ }
531
+ /** Digest node-forge correspondant au hachage configuré (jamais SHA-1). */
532
+ digestFor(hash) {
533
+ const { md } = this.forgeLib;
534
+ switch (hash) {
535
+ case "sha512": return md.sha512.create();
536
+ case "sha384": return md.sha384.create();
537
+ case "sha256": return md.sha256.create();
538
+ default:
539
+ this.log(`Hachage '${hash}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
540
+ return md.sha256.create();
541
+ }
542
+ }
543
+ };
544
+ //#endregion
545
+ export { Certificate as default };