@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.2

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 (100) hide show
  1. package/README.md +12 -3
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
  5. package/dist/index.js +6 -4
  6. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  7. package/dist/nodefony/command/certificatesCommand.js +1 -2
  8. package/dist/nodefony/command/networkCommand.js +1 -1
  9. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  10. package/dist/nodefony/config/config.js +26 -4
  11. package/dist/nodefony/interfaces/IRouting.js +1 -0
  12. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  13. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  14. package/dist/nodefony/service/audit-logger.js +16 -13
  15. package/dist/nodefony/service/certificates.js +87 -144
  16. package/dist/nodefony/service/error-renderer.js +11 -8
  17. package/dist/nodefony/service/http-kernel.js +342 -165
  18. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  19. package/dist/nodefony/service/servers/clientError.js +2 -2
  20. package/dist/nodefony/service/servers/server-http.js +23 -23
  21. package/dist/nodefony/service/servers/server-https.js +41 -42
  22. package/dist/nodefony/service/servers/server-static.js +13 -17
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
  24. package/dist/nodefony/service/servers/server-websocket.js +11 -10
  25. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  26. package/dist/nodefony/service/sessions/sessions-service.js +22 -15
  27. package/dist/nodefony/service/trace.js +3 -2
  28. package/dist/nodefony/service/upload/upload-service.js +11 -10
  29. package/dist/nodefony/service/x509.js +324 -0
  30. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  31. package/dist/nodefony/src/context/Context.js +34 -19
  32. package/dist/nodefony/src/context/forwarded.js +4 -4
  33. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  34. package/dist/nodefony/src/context/http/Request.js +108 -46
  35. package/dist/nodefony/src/context/http/Response.js +134 -54
  36. package/dist/nodefony/src/context/http/parser.js +9 -10
  37. package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
  38. package/dist/nodefony/src/context/http2/Response.js +53 -25
  39. package/dist/nodefony/src/context/metaData.js +1 -1
  40. package/dist/nodefony/src/context/trustProxy.js +4 -3
  41. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  42. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  43. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  44. package/dist/nodefony/src/cookies/cookie.js +3 -3
  45. package/dist/nodefony/src/errors/httpError.js +6 -6
  46. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  47. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  48. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  49. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  50. package/dist/nodefony/src/servers/portBinder.js +65 -23
  51. package/dist/nodefony/src/session/session.js +18 -10
  52. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  53. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  54. package/dist/types/index.d.ts +16 -0
  55. package/dist/types/nodefony/config/config.d.ts +23 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  57. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  58. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  59. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  60. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  61. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  62. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  63. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  64. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  65. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  66. package/dist/types/nodefony/service/certificates.d.ts +42 -57
  67. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  68. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  69. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  70. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  71. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  72. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  73. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  74. package/dist/types/nodefony/service/x509.d.ts +86 -0
  75. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  76. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  77. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  78. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  79. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  80. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  81. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  82. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  83. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  84. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  85. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  86. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  87. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  88. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  89. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  90. package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
  91. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  92. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  93. package/docs/cookies.md +25 -25
  94. package/docs/index.md +4 -2
  95. package/docs/observabilite.md +27 -27
  96. package/docs/rate-limit.md +20 -20
  97. package/docs/servers.md +82 -79
  98. package/docs/session.md +74 -69
  99. package/docs/upload.md +49 -43
  100. package/package.json +9 -12
@@ -1,11 +1,14 @@
1
+ import { asServersConfig } from "../src/servers/kernelServers.js";
2
+ import { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid } from "./x509.js";
1
3
  import { Service, extend } from "nodefony";
2
- import { randomBytes } from "node:crypto";
4
+ import { X509Certificate, generateKeyPair, randomBytes } from "node:crypto";
3
5
  import fsp from "node:fs/promises";
4
6
  import path, { resolve } from "node:path";
5
7
  import { promisify } from "node:util";
6
8
  import { execFile } from "node:child_process";
7
9
  //#region nodefony/service/certificates.ts
8
10
  const execFileAsync = promisify(execFile);
11
+ const generateKeyPairAsync = promisify(generateKeyPair);
9
12
  /**
10
13
  * Vrai si `host` est une IP littérale (IPv4 `n.n.n.n` ou IPv6 — contient `:`).
11
14
  * Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 6125).
@@ -35,7 +38,7 @@ const defaultOptions = {
35
38
  *
36
39
  * Trois stratégies : `explicit` (certificat fourni en config — le cas de
37
40
  * 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
41
+ * (auto-signé, secours). La génération est réservée au
39
42
  * DÉVELOPPEMENT : en production sans certificat fourni, le service crie un
40
43
  * avertissement (Nodefony n'est pas une autorité de certification).
41
44
  *
@@ -48,7 +51,6 @@ var Certificate = class Certificate extends Service {
48
51
  module;
49
52
  files = [];
50
53
  keysPair = null;
51
- certForge = null;
52
54
  ca = "";
53
55
  key = "";
54
56
  cert = "";
@@ -58,13 +60,6 @@ var Certificate = class Certificate extends Service {
58
60
  intermediateCertPem = "";
59
61
  /** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
60
62
  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
63
  path = resolve(".", "nodefony", "config", "certificates");
69
64
  serverPath = resolve(this.path, "server");
70
65
  caPath = resolve(this.path, "ca", "nodefony-root-ca.crt.pem");
@@ -85,20 +80,6 @@ var Certificate = class Certificate extends Service {
85
80
  return this.kernel?.environment === "development";
86
81
  }
87
82
  /**
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
- /**
102
83
  * Fabrique-t-on un certificat au démarrage ?
103
84
  *
104
85
  * 🔴 Seulement si un serveur TLS est ACTIF. Sans cette question, le hook
@@ -120,7 +101,7 @@ var Certificate = class Certificate extends Service {
120
101
  * exemplaire. La promesse était écrite ; rien ne la tenait.
121
102
  */
122
103
  get tlsWanted() {
123
- return !!this.module.kernel?.options?.servers?.https;
104
+ return !!asServersConfig(this.module.kernel?.options.servers)?.https;
124
105
  }
125
106
  async init() {
126
107
  if (!this.tlsWanted) return this;
@@ -138,8 +119,8 @@ var Certificate = class Certificate extends Service {
138
119
  */
139
120
  static generateSerialHex() {
140
121
  const bytes = randomBytes(16);
141
- bytes[0] &= 127;
142
- if (bytes[0] === 0) bytes[0] = 1;
122
+ const head = bytes.readUInt8(0) & 127;
123
+ bytes[0] = head === 0 ? 1 : head;
143
124
  return bytes.toString("hex");
144
125
  }
145
126
  setFiles() {
@@ -179,7 +160,7 @@ var Certificate = class Certificate extends Service {
179
160
  path.dirname(this.certPath),
180
161
  path.dirname(this.fullchainPath)
181
162
  ];
182
- for await (const directory of directories) try {
163
+ for (const directory of directories) try {
183
164
  await fsp.access(directory);
184
165
  } catch {
185
166
  await fsp.mkdir(directory, { recursive: true });
@@ -189,7 +170,7 @@ var Certificate = class Certificate extends Service {
189
170
  /**
190
171
  * Génère (ou recharge) le certificat serveur selon la stratégie résolue :
191
172
  * `explicit` (fourni en config / prod), `mkcert` (dev, CA trustée) ou
192
- * `selfsigned` (auto-signé node-forge, secours). Régénère automatiquement si
173
+ * `selfsigned` (auto-signé, secours). Régénère automatiquement si
193
174
  * le certificat présent sur disque n'est pas adéquat pour la stratégie active
194
175
  * (expiré, SHA-1, SAN incomplet).
195
176
  *
@@ -200,17 +181,13 @@ var Certificate = class Certificate extends Service {
200
181
  await this.ensureDirectoriesExist();
201
182
  const strategy = await this.resolveStrategy();
202
183
  if (strategy === "explicit") return this.loadExplicitCert();
203
- await this.loadForge();
204
184
  if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) return this.readCerticates();
205
185
  if (strategy === "mkcert") await this.generateWithMkcert();
206
186
  else {
207
- this.keysPair = this.generateKeys();
208
- this.certForge = this.createCertificate();
209
- this.setExtension();
210
- this.sign();
187
+ this.keysPair = await this.generateKeys();
211
188
  this.key = this.generatePrivateKeyPem();
212
189
  this.publicKeyPem = this.generatePublickeyPem();
213
- const certPem = this.generateCertPem();
190
+ const certPem = await this.createCertificate();
214
191
  this.cert = certPem;
215
192
  this.ca = certPem;
216
193
  this.fullchainPem = this.createFullChain();
@@ -226,7 +203,7 @@ var Certificate = class Certificate extends Service {
226
203
  * erreur si absents.
227
204
  * - `mkcert` : dev + binaire mkcert + CA locale présents. Forcé hors dev →
228
205
  * retombe sur `selfsigned` avec avertissement.
229
- * - `selfsigned` : auto-signé node-forge (fallback). En PRODUCTION, crie un
206
+ * - `selfsigned` : auto-signé (fallback). En PRODUCTION, crie un
230
207
  * avertissement : la génération n'est pas le rôle d'un serveur de prod.
231
208
  */
232
209
  async resolveStrategy() {
@@ -237,13 +214,13 @@ var Certificate = class Certificate extends Service {
237
214
  }
238
215
  if (requested === "auto" && this.hasExplicitCert()) return "explicit";
239
216
  if (requested === "mkcert" || requested === "auto") {
240
- if (this.isDev() && this.certOptions.dev?.useMkcert !== false) {
217
+ if (this.isDev() && this.certOptions.dev.useMkcert !== false) {
241
218
  const caRoot = await this.detectMkcert();
242
219
  if (caRoot) {
243
220
  this.mkcertCaRoot = caRoot;
244
221
  return "mkcert";
245
222
  }
246
- 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");
223
+ this.log((requested === "mkcert" ? "strategy='mkcert' mais mkcert introuvable — " : "mkcert introuvable — ") + "fallback certificat auto-signé (non trusté). `brew install mkcert nss && mkcert -install` pour un HTTPS dev sans erreur (HMR cross-origin/WSS).", "WARNING");
247
224
  } else if (requested === "mkcert") this.log("strategy='mkcert' ignoré hors development → certificat auto-signé.", "WARNING");
248
225
  }
249
226
  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");
@@ -320,8 +297,10 @@ var Certificate = class Certificate extends Service {
320
297
  await this.restrictPrivateKey();
321
298
  const rootCaPem = await fsp.readFile(path.join(caRoot, "rootCA.pem"), "utf8");
322
299
  const certPem = await fsp.readFile(this.certPath, "utf8");
323
- const { pki } = this.forgeLib;
324
- const publicKeyPem = pki.publicKeyToPem(pki.certificateFromPem(certPem).publicKey);
300
+ const publicKeyPem = new X509Certificate(certPem).publicKey.export({
301
+ type: "spki",
302
+ format: "pem"
303
+ });
325
304
  await fsp.writeFile(this.publicKeyPath, publicKeyPem, "utf8");
326
305
  await fsp.writeFile(this.fullchainPath, `${certPem}${rootCaPem}`, "utf8");
327
306
  await fsp.writeFile(this.caPath, rootCaPem, "utf8");
@@ -338,25 +317,21 @@ var Certificate = class Certificate extends Service {
338
317
  */
339
318
  async isCertAdequate(strategy) {
340
319
  try {
341
- const { pki } = this.forgeLib;
342
- const certPem = await fsp.readFile(this.certPath, "utf8");
343
- const cert = pki.certificateFromPem(certPem);
344
- if (cert.validity.notAfter.getTime() <= Date.now()) return false;
345
- const ext = cert.getExtension("subjectAltName");
346
- if (!ext || !this.sanCovers(ext.altNames ?? [])) return false;
320
+ const cert = new X509Certificate(await fsp.readFile(this.certPath));
321
+ if (cert.validToDate.getTime() <= Date.now()) return false;
322
+ if (!this.sanCovers(parseSubjectAltName(cert.subjectAltName).dns)) return false;
347
323
  if (strategy === "mkcert") {
348
- const org = cert.issuer.getField("O");
349
- return Boolean(org && /mkcert/i.test(String(org.value)));
324
+ const org = distinguishedNameField(cert.issuer, "O");
325
+ return org !== null && /mkcert/i.test(org);
350
326
  }
351
- return cert.signatureOid !== pki.oids.sha1WithRSAEncryption;
327
+ return signatureAlgorithmOid(cert.raw) !== SHA1_WITH_RSA_OID;
352
328
  } catch {
353
329
  return false;
354
330
  }
355
331
  }
356
332
  /** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 6125) ? */
357
- sanCovers(present) {
358
- const presentDns = new Set(present.filter((p) => p.type === 2 && p.value).map((p) => p.value));
359
- return this.sanDnsNames().every((name) => presentDns.has(name));
333
+ sanCovers(presentDns) {
334
+ return this.sanDnsNames().every((name) => presentDns.includes(name));
360
335
  }
361
336
  /** Charge un certificat fourni en config (chemin fichier ou Buffer). */
362
337
  async loadExplicitCert() {
@@ -401,7 +376,7 @@ var Certificate = class Certificate extends Service {
401
376
  }
402
377
  async writeCertificates(force = false) {
403
378
  await this.ensureDirectoriesExist();
404
- for await (const file of this.files) try {
379
+ for (const file of this.files) try {
405
380
  if (file.variable) {
406
381
  const mode = file.path === this.privateKeyPath ? this.certOptions.privateKeyMode ?? 384 : 420;
407
382
  if (force) await fsp.rm(file.path, { force: true });
@@ -442,86 +417,58 @@ var Certificate = class Certificate extends Service {
442
417
  this.log(err, "DEBUG");
443
418
  }
444
419
  }
445
- generateKeys() {
446
- return this.forgeLib.pki.rsa.generateKeyPair(this.certOptions.selfSigned.size);
420
+ /** Génère la paire RSA (taille configurée) hors de la boucle d'événements. */
421
+ async generateKeys() {
422
+ return generateKeyPairAsync("rsa", { modulusLength: this.certOptions.selfSigned.size });
447
423
  }
424
+ /** Clé privée en PEM PKCS#1 (`BEGIN RSA PRIVATE KEY`). */
448
425
  generatePrivateKeyPem() {
449
- if (this.keysPair) return Buffer.from(this.forgeLib.pki.privateKeyToPem(this.keysPair.privateKey));
450
- throw new Error(`pki.rsa.KeyPair not found`);
426
+ if (this.keysPair) return Buffer.from(this.keysPair.privateKey.export({
427
+ type: "pkcs1",
428
+ format: "pem"
429
+ }));
430
+ throw new Error(`KeyPair not found`);
451
431
  }
432
+ /** Clé publique en PEM SPKI (`BEGIN PUBLIC KEY`). */
452
433
  generatePublickeyPem() {
453
- if (this.keysPair) return Buffer.from(this.forgeLib.pki.publicKeyToPem(this.keysPair.publicKey));
454
- throw new Error(`pki.rsa.KeyPair not found`);
434
+ if (this.keysPair) return Buffer.from(this.keysPair.publicKey.export({
435
+ type: "spki",
436
+ format: "pem"
437
+ }));
438
+ throw new Error(`KeyPair not found`);
455
439
  }
456
- generateCertPem() {
457
- if (this.certForge) return Buffer.from(this.forgeLib.pki.certificateToPem(this.certForge));
458
- throw new Error(`pki.Certificate not found`);
459
- }
460
- createCertificate() {
440
+ /**
441
+ * Fabrique et signe le certificat auto-signé avec la paire courante.
442
+ *
443
+ * Série aléatoire unique (RFC 5280 §4.1.2.2), `notBefore` reculé (décalage
444
+ * d'horloge du client), SAN DNS + IP (config explicite sinon dérivé), signé
445
+ * avec le hachage configuré — jamais SHA-1 (collision SHAttered 2017,
446
+ * CA/Browser Forum depuis 2016).
447
+ *
448
+ * @returns le certificat en PEM
449
+ * @throws Si la paire de clés n'a pas été générée
450
+ */
451
+ async createCertificate() {
461
452
  if (!this.keysPair) throw new Error(`KeyPair not found`);
462
453
  const o = this.certOptions.selfSigned;
463
- const cert = this.forgeLib.pki.createCertificate();
464
- cert.publicKey = this.keysPair.publicKey;
465
- cert.serialNumber = Certificate.generateSerialHex();
466
- const backdateMs = o.backdateMinutes * 6e4;
467
- const start = Date.now() - backdateMs;
468
- cert.validity.notBefore = new Date(start);
469
- cert.validity.notAfter = new Date(start + o.validityDays * 864e5);
470
- cert.setSubject(o.attrs);
471
- cert.setIssuer(o.attrs);
472
- return cert;
473
- }
474
- /** subjectAltName de l'auto-signé : DNS + IP (config explicite sinon dérivé). */
475
- altNames() {
476
- const out = [];
477
- for (const dns of this.sanDnsNames()) out.push({
478
- type: 2,
479
- value: dns
480
- });
481
- for (const ip of this.sanIps()) out.push({
482
- type: 7,
483
- ip
454
+ const start = Date.now() - o.backdateMinutes * 6e4;
455
+ const pem = await createSelfSignedCertificate({
456
+ privateKey: this.keysPair.privateKey,
457
+ publicKey: this.keysPair.publicKey,
458
+ serialHex: Certificate.generateSerialHex(),
459
+ notBefore: new Date(start),
460
+ notAfter: new Date(start + o.validityDays * 864e5),
461
+ attributes: o.attrs,
462
+ dns: this.sanDnsNames(),
463
+ ip: this.sanIps(),
464
+ hash: this.allowedHash(o.hash)
484
465
  });
485
- return out;
486
- }
487
- setExtension() {
488
- if (!this.certForge) throw new Error(`pki.Certificate not found`);
489
- this.certForge.setExtensions([
490
- {
491
- name: "basicConstraints",
492
- cA: false
493
- },
494
- {
495
- name: "keyUsage",
496
- digitalSignature: true,
497
- keyEncipherment: true
498
- },
499
- {
500
- name: "extKeyUsage",
501
- serverAuth: true,
502
- clientAuth: true
503
- },
504
- {
505
- name: "subjectAltName",
506
- altNames: this.altNames()
507
- },
508
- { name: "subjectKeyIdentifier" }
509
- ]);
510
- }
511
- /**
512
- * Signe le certificat avec le hachage configuré (SHA-256 par défaut).
513
- * SHA-1 est INTERDIT (collision SHAttered 2017, CA/Browser Forum depuis 2016) :
514
- * `node-forge` signe en SHA-1 par défaut si on ne passe pas de digest → on en
515
- * passe toujours un.
516
- */
517
- sign() {
518
- if (!this.certForge || !this.keysPair) throw new Error(`pki.rsa.KeyPair or pki.Certificate not found`);
519
- this.certForge.sign(this.keysPair.privateKey, this.digestFor(this.certOptions.selfSigned.hash));
466
+ return Buffer.from(pem);
520
467
  }
521
468
  /**
522
469
  * Résumé introspectable du certificat serveur courant — réutilisé par la
523
470
  * commande CLI `certificates` et un futur endpoint d'admin Studio (parité
524
- * CLI ↔ Web). Parse le certificat chargé (charge node-forge à la demande).
471
+ * CLI ↔ Web).
525
472
  */
526
473
  async describe() {
527
474
  const info = {
@@ -535,34 +482,30 @@ var Certificate = class Certificate extends Service {
535
482
  info.caPath = this.caPath;
536
483
  } catch {}
537
484
  if (!this.cert) return info;
538
- const { pki } = await this.loadForge();
539
485
  try {
540
- const cert = pki.certificateFromPem(this.cert.toString());
541
- info.serial = cert.serialNumber;
542
- info.validFrom = cert.validity.notBefore.toISOString();
543
- info.validTo = cert.validity.notAfter.toISOString();
544
- info.signatureAlgorithm = this.oidName(cert.signatureOid, pki);
545
- const cn = cert.subject.getField("CN");
546
- if (cn) info.commonName = String(cn.value);
547
- const ext = cert.getExtension("subjectAltName");
548
- if (ext?.altNames) info.san = ext.altNames.map((a) => a.type === 7 ? a.ip ?? "" : a.value ?? "");
486
+ const cert = new X509Certificate(this.cert);
487
+ info.serial = cert.serialNumber.toLowerCase();
488
+ info.validFrom = cert.validFromDate.toISOString();
489
+ info.validTo = cert.validToDate.toISOString();
490
+ info.signatureAlgorithm = signatureAlgorithmName(signatureAlgorithmOid(cert.raw));
491
+ const cn = cert.subject ? distinguishedNameField(cert.subject, "CN") : null;
492
+ if (cn !== null) info.commonName = cn;
493
+ const san = parseSubjectAltName(cert.subjectAltName);
494
+ if (san.dns.length > 0 || san.ip.length > 0) info.san = [...san.dns, ...san.ip];
549
495
  } catch {}
550
496
  return info;
551
497
  }
552
- /** Nom lisible de l'OID d'algorithme de signature. */
553
- oidName(oid, pki) {
554
- return pki.oids[oid] ?? oid;
555
- }
556
- /** Digest node-forge correspondant au hachage configuré (jamais SHA-1). */
557
- digestFor(hash) {
558
- const { md } = this.forgeLib;
498
+ /** Hachage configuré, ramené à SHA-256 s'il sort du contrat (jamais SHA-1). */
499
+ allowedHash(hash) {
559
500
  switch (hash) {
560
- case "sha512": return md.sha512.create();
561
- case "sha384": return md.sha384.create();
562
- case "sha256": return md.sha256.create();
563
- default:
564
- this.log(`Hachage '${hash}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
565
- return md.sha256.create();
501
+ case "sha512":
502
+ case "sha384":
503
+ case "sha256": return hash;
504
+ default: {
505
+ const refused = hash;
506
+ this.log(`Hachage '${refused}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
507
+ return "sha256";
508
+ }
566
509
  }
567
510
  }
568
511
  };
@@ -111,7 +111,7 @@ function toValidationFields(error) {
111
111
  function toValidationError(error, fields) {
112
112
  const summary = fields.map((f) => f.field ? `${f.field}: ${f.message}` : f.message).join(" · ");
113
113
  const validation = new nodefonyError(`Validation failed — ${summary}`, VALIDATION_STATUS);
114
- validation.stack = error.stack;
114
+ if (error.stack !== void 0) validation.stack = error.stack;
115
115
  validation.fields = fields;
116
116
  return validation;
117
117
  }
@@ -206,8 +206,11 @@ function malformedIdentifierOf(error) {
206
206
  const candidate = current;
207
207
  if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
208
208
  if (String(candidate.code) === "22P02") {
209
- const match = PG_INVALID_INPUT.exec(candidate.message ?? "");
210
- if (match && IDENTIFIER_COLUMN_TYPES.has(match[1])) return match[2];
209
+ const match = PG_INVALID_INPUT.exec(candidate.message);
210
+ if (match) {
211
+ const [, type = "", value = null] = match;
212
+ if (IDENTIFIER_COLUMN_TYPES.has(type)) return value;
213
+ }
211
214
  }
212
215
  current = candidate.cause;
213
216
  }
@@ -307,7 +310,7 @@ function schemaMismatchOf(error) {
307
310
  for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
308
311
  const candidate = current;
309
312
  if (candidate.code !== void 0 && SCHEMA_MISMATCH_CODES.has(String(candidate.code)) || candidate.errno !== void 0 && SCHEMA_MISMATCH_CODES.has(String(candidate.errno))) return "certain";
310
- if (String(candidate.code) === "SQLITE_ERROR" && SQLITE_SCHEMA_PREFIXES.some((p) => candidate.message?.startsWith(p))) return "probable";
313
+ if (String(candidate.code) === "SQLITE_ERROR" && SQLITE_SCHEMA_PREFIXES.some((p) => candidate.message.startsWith(p))) return "probable";
311
314
  current = candidate.cause;
312
315
  }
313
316
  return null;
@@ -373,7 +376,7 @@ var DefaultErrorRenderer = class {
373
376
  const httpError = this.toHttpError(error, context);
374
377
  const rawCode = httpError.code;
375
378
  let code = typeof rawCode === "number" && Number.isInteger(rawCode) ? rawCode : 500;
376
- if (context && context.rejected === false) code = toWsCloseCode(code);
379
+ if (context.rejected === false) code = toWsCloseCode(code);
377
380
  else if (code > 599) code = 500;
378
381
  const internal = code === 1011 || code >= 500 && code <= 599;
379
382
  const reason = isProduction() && internal ? OPAQUE_SERVER_ERROR : httpError.message;
@@ -392,20 +395,20 @@ var DefaultErrorRenderer = class {
392
395
  }
393
396
  if (isUniqueViolation(error)) {
394
397
  const conflict = new nodefonyError(UNIQUE_VIOLATION_MESSAGE, CONFLICT_STATUS);
395
- conflict.stack = error.stack;
398
+ if (error.stack !== void 0) conflict.stack = error.stack;
396
399
  return new HttpError(conflict, CONFLICT_STATUS, context);
397
400
  }
398
401
  const referencedBy = referencedByOf(error);
399
402
  if (referencedBy !== null) {
400
403
  const conflict = new nodefonyError(`Conflict — this resource is still referenced by ${referencedBy}`, CONFLICT_STATUS);
401
- conflict.stack = error.stack;
404
+ if (error.stack !== void 0) conflict.stack = error.stack;
402
405
  return new HttpError(conflict, CONFLICT_STATUS, context);
403
406
  }
404
407
  const malformed = malformedIdentifierOf(error);
405
408
  if (malformed !== null) {
406
409
  const status = malformedIdentifierStatus(malformed, context?.url);
407
410
  const refused = new nodefonyError(status === 404 ? "Not Found — this identifier cannot designate any resource" : "Bad Request — malformed identifier", status);
408
- refused.stack = error.stack;
411
+ if (error.stack !== void 0) refused.stack = error.stack;
409
412
  return new HttpError(refused, status, context);
410
413
  }
411
414
  const code = error.code;