@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
@@ -29,7 +29,7 @@ function parseTraceparent(header) {
29
29
  if (typeof header !== "string") return null;
30
30
  const m = TRACEPARENT_RE.exec(header.trim().toLowerCase());
31
31
  if (!m) return null;
32
- const [, version, traceId, parentId, flags] = m;
32
+ const [, version = "", traceId = "", parentId = "", flags = ""] = m;
33
33
  if (version === "ff") return null;
34
34
  if (/^0+$/.test(traceId)) return null;
35
35
  if (/^0+$/.test(parentId)) return null;
@@ -66,7 +66,8 @@ function resolveTraceparent(header) {
66
66
  const newSpanId = randomHex(8);
67
67
  return `${parsed.version}-${parsed.traceId}-${newSpanId}-${parsed.flags}`;
68
68
  }
69
- return `00-${randomHex(16)}-${randomHex(8)}-01`;
69
+ const hex = randomHex(24);
70
+ return `00-${hex.slice(0, 32)}-${hex.slice(32)}-01`;
70
71
  }
71
72
  //#endregion
72
73
  export { parseTraceparent, resolveTraceparent };
@@ -1,7 +1,7 @@
1
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
3
3
  import http_kernel_default from "../http-kernel.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateParam.js";
5
5
  import { Cli, FileClass, Module, Service, inject } from "nodefony";
6
6
  import fs from "node:fs";
7
7
  import fsp from "node:fs/promises";
@@ -21,18 +21,19 @@ let upload = class upload extends Service {
21
21
  path;
22
22
  module;
23
23
  constructor(module, httpKernel) {
24
- super("upload", httpKernel?.container, httpKernel.notificationsCenter);
24
+ super("upload", httpKernel.container, httpKernel.notificationsCenter);
25
25
  this.httpKernel = httpKernel;
26
26
  this.module = module;
27
27
  this.kernel?.once("onBoot", async () => {
28
28
  this.options = this.httpKernel.options;
29
- if (path.isAbsolute(this.options.upload.uploadDir)) this.path = this.options.upload.uploadDir;
30
- else this.path = path.resolve(`${this.kernel?.path}/${this.options.upload.uploadDir}`);
29
+ const uploadCfg = this.httpKernel.options.upload;
30
+ if (path.isAbsolute(uploadCfg.uploadDir)) this.path = uploadCfg.uploadDir;
31
+ else this.path = path.resolve(`${this.kernel?.path}/${uploadCfg.uploadDir}`);
31
32
  try {
32
33
  await fsp.mkdir(this.path, { recursive: true });
33
34
  } catch (e) {
34
35
  this.path = "/tmp";
35
- this.options.upload.uploadDir = this.path;
36
+ uploadCfg.uploadDir = this.path;
36
37
  this.log(e, "DEBUG");
37
38
  }
38
39
  });
@@ -102,8 +103,7 @@ var UploadedFile = class UploadedFile extends FileClass {
102
103
  return this.parsedFile.originalFilename || name || this.parsedFile.newFilename;
103
104
  }
104
105
  getMimeType() {
105
- if (this.parsedFile) return this.parsedFile.mimetype || super.getMimeType(this.filename);
106
- return super.getMimeType();
106
+ return this.parsedFile.mimetype || super.getMimeType(this.filename);
107
107
  }
108
108
  /**
109
109
  * Nom de fichier SÛR à poser dans un dossier de destination.
@@ -141,7 +141,8 @@ var UploadedFile = class UploadedFile extends FileClass {
141
141
  return super.move(name);
142
142
  } else return super.move(target);
143
143
  }
144
- throw fs.lstatSync(dirname);
144
+ fs.lstatSync(dirname);
145
+ throw new Error(`Upload target directory not found: ${dirname}`);
145
146
  }
146
147
  /**
147
148
  * Variante **async** de `move()` — déplace le fichier uploadé sans bloquer
@@ -0,0 +1,324 @@
1
+ import { isIPv4, isIPv6 } from "node:net";
2
+ import { createHash, sign } from "node:crypto";
3
+ //#region nodefony/service/x509.ts
4
+ /**
5
+ * Fabrique et lecture de certificats X.509 par `node:crypto` seul — aucune
6
+ * dépendance tierce.
7
+ *
8
+ * `node:crypto` sait LIRE un certificat (`X509Certificate`) et SIGNER, pas
9
+ * en FABRIQUER un : ce module encode le `TBSCertificate` en DER (RFC 5280
10
+ * §4.1), le signe avec `crypto.sign`, puis l'assemble. Seul le sous-ensemble
11
+ * nécessaire à un certificat serveur auto-signé est couvert. Les fonctions
12
+ * sont PURES (aucun disque, aucun kernel) : elles s'éprouvent sans décor.
13
+ *
14
+ * Pourquoi pas node-forge : sa vérification de signature RSA PKCS#1 v1.5
15
+ * accepte des signatures forgées (GHSA-86w9-cpqp-85rv), sans aucune version
16
+ * corrigée. Nous ne vérifions aucune signature, mais `npm audit` signale le
17
+ * paquet pour tous ceux qui installent `@nodefony/http`.
18
+ */
19
+ const SIGNATURE_OIDS = {
20
+ sha1: "1.2.840.113549.1.1.5",
21
+ sha256: "1.2.840.113549.1.1.11",
22
+ sha384: "1.2.840.113549.1.1.12",
23
+ sha512: "1.2.840.113549.1.1.13"
24
+ };
25
+ /** OID de la signature RSA-SHA1, que le service régénère. */
26
+ const SHA1_WITH_RSA_OID = SIGNATURE_OIDS.sha1;
27
+ /** Noms lisibles des algorithmes de signature courants (rendu de `describe`). */
28
+ const SIGNATURE_NAMES = Object.assign(Object.create(null), {
29
+ [SIGNATURE_OIDS.sha1]: "sha1WithRSAEncryption",
30
+ [SIGNATURE_OIDS.sha256]: "sha256WithRSAEncryption",
31
+ [SIGNATURE_OIDS.sha384]: "sha384WithRSAEncryption",
32
+ [SIGNATURE_OIDS.sha512]: "sha512WithRSAEncryption",
33
+ "1.2.840.113549.1.1.10": "RSASSA-PSS",
34
+ "1.2.840.10045.4.3.2": "ecdsa-with-SHA256",
35
+ "1.2.840.10045.4.3.3": "ecdsa-with-SHA384",
36
+ "1.2.840.10045.4.3.4": "ecdsa-with-SHA512",
37
+ "1.3.101.112": "Ed25519"
38
+ });
39
+ const ATTRIBUTE_OIDS = Object.assign(Object.create(null), {
40
+ commonName: "2.5.4.3",
41
+ CN: "2.5.4.3",
42
+ surname: "2.5.4.4",
43
+ SN: "2.5.4.4",
44
+ serialNumber: "2.5.4.5",
45
+ countryName: "2.5.4.6",
46
+ C: "2.5.4.6",
47
+ localityName: "2.5.4.7",
48
+ L: "2.5.4.7",
49
+ stateOrProvinceName: "2.5.4.8",
50
+ ST: "2.5.4.8",
51
+ streetAddress: "2.5.4.9",
52
+ street: "2.5.4.9",
53
+ organizationName: "2.5.4.10",
54
+ O: "2.5.4.10",
55
+ organizationalUnitName: "2.5.4.11",
56
+ OU: "2.5.4.11",
57
+ title: "2.5.4.12",
58
+ givenName: "2.5.4.42",
59
+ emailAddress: "1.2.840.113549.1.9.1",
60
+ E: "1.2.840.113549.1.9.1"
61
+ });
62
+ const OID_COUNTRY = "2.5.4.6";
63
+ const OID_EMAIL = "1.2.840.113549.1.9.1";
64
+ const OID_BASIC_CONSTRAINTS = "2.5.29.19";
65
+ const OID_KEY_USAGE = "2.5.29.15";
66
+ const OID_EXT_KEY_USAGE = "2.5.29.37";
67
+ const OID_SUBJECT_ALT_NAME = "2.5.29.17";
68
+ const OID_SUBJECT_KEY_ID = "2.5.29.14";
69
+ const OID_SERVER_AUTH = "1.3.6.1.5.5.7.3.1";
70
+ const OID_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
71
+ function derLength(length) {
72
+ if (length < 128) return Buffer.from([length]);
73
+ const bytes = [];
74
+ for (let rest = length; rest > 0; rest = Math.floor(rest / 256)) bytes.unshift(rest & 255);
75
+ return Buffer.from([128 | bytes.length, ...bytes]);
76
+ }
77
+ function tlv(tag, content) {
78
+ return Buffer.concat([
79
+ Buffer.from([tag]),
80
+ derLength(content.length),
81
+ content
82
+ ]);
83
+ }
84
+ const sequence = (...items) => tlv(48, Buffer.concat(items));
85
+ const set = (...items) => tlv(49, Buffer.concat(items));
86
+ const explicit = (n, content) => tlv(160 | n, content);
87
+ const octetString = (content) => tlv(4, content);
88
+ const bitString = (content) => tlv(3, Buffer.concat([Buffer.from([0]), content]));
89
+ const DER_NULL = Buffer.from([5, 0]);
90
+ const DER_TRUE = tlv(1, Buffer.from([255]));
91
+ function objectIdentifier(dotted) {
92
+ const [first = 0, second = 0, ...rest] = dotted.split(".").map(Number);
93
+ const bytes = [first * 40 + second];
94
+ for (const arc of rest) {
95
+ const chunk = [arc & 127];
96
+ for (let value = Math.floor(arc / 128); value > 0; value = Math.floor(value / 128)) chunk.unshift(value & 127 | 128);
97
+ bytes.push(...chunk);
98
+ }
99
+ return tlv(6, Buffer.from(bytes));
100
+ }
101
+ /** Entier positif depuis sa forme hexadécimale (octet 0x00 ajouté si besoin). */
102
+ function positiveInteger(hex) {
103
+ let bytes = Buffer.from(hex.length % 2 === 0 ? hex : `0${hex}`, "hex");
104
+ let start = 0;
105
+ while (start < bytes.length - 1 && bytes[start] === 0) start++;
106
+ bytes = bytes.subarray(start);
107
+ if (bytes.length === 0) bytes = Buffer.from([0]);
108
+ if ((bytes[0] ?? 0) & 128) bytes = Buffer.concat([Buffer.from([0]), bytes]);
109
+ return tlv(2, bytes);
110
+ }
111
+ /** RFC 5280 §4.1.2.5 : UTCTime jusqu'en 2049, GeneralizedTime au-delà. */
112
+ function time(date) {
113
+ const iso = date.toISOString();
114
+ const digits = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
115
+ const year = date.getUTCFullYear();
116
+ if (year >= 1950 && year < 2050) return tlv(23, Buffer.from(digits.slice(2), "ascii"));
117
+ return tlv(24, Buffer.from(digits, "ascii"));
118
+ }
119
+ function attributeOid(attribute) {
120
+ if (attribute.type && /^\d+(\.\d+)+$/.test(attribute.type)) return attribute.type;
121
+ const key = attribute.name ?? attribute.shortName ?? attribute.type ?? "";
122
+ const oid = ATTRIBUTE_OIDS[key];
123
+ if (!oid) throw new Error(`Attribut de certificat inconnu : '${key}'. Noms reconnus : ${Object.keys(ATTRIBUTE_OIDS).join(", ")}.`);
124
+ return oid;
125
+ }
126
+ /** Nom distinctif : un RDN par attribut (RFC 5280 §4.1.2.4). */
127
+ function distinguishedName(attributes) {
128
+ const rdns = attributes.map((attribute) => {
129
+ const oid = attributeOid(attribute);
130
+ if (typeof attribute.value !== "string") throw new Error(`Attribut de certificat '${oid}' sans valeur.`);
131
+ const value = Buffer.from(attribute.value, "utf8");
132
+ const tag = oid === OID_COUNTRY ? 19 : oid === OID_EMAIL ? 22 : 12;
133
+ return set(sequence(objectIdentifier(oid), tlv(tag, value)));
134
+ });
135
+ return sequence(...rdns);
136
+ }
137
+ /** Octets d'une adresse IP pour un SAN `iPAddress` (4 ou 16 octets). */
138
+ function ipAddressBytes(ip) {
139
+ if (isIPv4(ip)) return Buffer.from(ip.split(".").map(Number));
140
+ if (!isIPv6(ip) || ip.includes("%")) throw new Error(`Adresse IP invalide pour un SAN : '${ip}'.`);
141
+ const toGroups = (part) => {
142
+ if (part === "") return [];
143
+ const groups = [];
144
+ for (const piece of part.split(":")) if (piece.includes(".")) {
145
+ const [a = 0, b = 0, c = 0, d = 0] = piece.split(".").map(Number);
146
+ groups.push(a << 8 | b, c << 8 | d);
147
+ } else groups.push(parseInt(piece, 16));
148
+ return groups;
149
+ };
150
+ const [head = "", tail] = ip.split("::");
151
+ const left = toGroups(head);
152
+ const right = tail === void 0 ? [] : toGroups(tail);
153
+ const zeros = new Array(8 - left.length - right.length).fill(0);
154
+ const out = Buffer.alloc(16);
155
+ [
156
+ ...left,
157
+ ...zeros,
158
+ ...right
159
+ ].forEach((group, i) => out.writeUInt16BE(group, i * 2));
160
+ return out;
161
+ }
162
+ /**
163
+ * Octets d'un SAN `dNSName` — IA5String (RFC 5280 §4.2.1.6).
164
+ *
165
+ * 🔴 Jamais `Buffer.from(nom, "ascii")` : Node y garde l'octet BAS de chaque
166
+ * caractère, et `evil\u012Elocalhost` devient `evil.localhost` — un certificat
167
+ * qui couvre un nom que personne n'a configuré. Hors ASCII imprimable, le nom
168
+ * est REFUSÉ (un IDN s'écrit en punycode, `xn--…`).
169
+ */
170
+ function dnsNameBytes(dns) {
171
+ if (!/^[\x20-\x7e]+$/u.test(dns)) throw new Error(`Nom DNS invalide pour un SAN : ${JSON.stringify(dns)} — ASCII imprimable seulement (un nom international s'écrit en punycode, xn--…).`);
172
+ return Buffer.from(dns, "ascii");
173
+ }
174
+ function extension(oid, critical, value) {
175
+ return critical ? sequence(objectIdentifier(oid), DER_TRUE, octetString(value)) : sequence(objectIdentifier(oid), octetString(value));
176
+ }
177
+ function readNode(der, offset) {
178
+ const tag = der[offset];
179
+ let length = der[offset + 1];
180
+ if (tag === void 0 || length === void 0) throw new Error("DER tronqué.");
181
+ let start = offset + 2;
182
+ if (length & 128) {
183
+ const count = length & 127;
184
+ length = 0;
185
+ for (let i = 0; i < count; i++) length = length * 256 + (der[start + i] ?? 0);
186
+ start += count;
187
+ }
188
+ const end = start + length;
189
+ if (end > der.length) throw new Error("DER tronqué.");
190
+ return {
191
+ tag,
192
+ start,
193
+ end
194
+ };
195
+ }
196
+ function children(der, node) {
197
+ const out = [];
198
+ for (let offset = node.start; offset < node.end;) {
199
+ const child = readNode(der, offset);
200
+ out.push(child);
201
+ offset = child.end;
202
+ }
203
+ return out;
204
+ }
205
+ function decodeObjectIdentifier(bytes) {
206
+ const first = bytes[0] ?? 0;
207
+ const arcs = [Math.min(Math.floor(first / 40), 2), first - Math.min(Math.floor(first / 40), 2) * 40];
208
+ let value = 0;
209
+ for (let i = 1; i < bytes.length; i++) {
210
+ const byte = bytes[i] ?? 0;
211
+ value = value * 128 + (byte & 127);
212
+ if (!(byte & 128)) {
213
+ arcs.push(value);
214
+ value = 0;
215
+ }
216
+ }
217
+ return arcs.join(".");
218
+ }
219
+ /**
220
+ * OID de l'algorithme de signature d'un certificat DER (champ
221
+ * `signatureAlgorithm`, RFC 5280 §4.1.1.2). Lu dans le DER plutôt que par
222
+ * `X509Certificate.signatureAlgorithmOid`, absent des Node du plancher.
223
+ *
224
+ * @param der - certificat encodé DER (`X509Certificate.raw`)
225
+ * @throws Si le DER n'a pas la forme d'un certificat
226
+ */
227
+ function signatureAlgorithmOid(der) {
228
+ const [, algorithm] = children(der, readNode(der, 0));
229
+ const oid = algorithm ? children(der, algorithm)[0] : void 0;
230
+ if (oid?.tag !== 6) throw new Error("Certificat DER sans algorithme de signature.");
231
+ return decodeObjectIdentifier(der.subarray(oid.start, oid.end));
232
+ }
233
+ /** Nom lisible d'un OID de signature (l'OID lui-même s'il est inconnu). */
234
+ function signatureAlgorithmName(oid) {
235
+ return SIGNATURE_NAMES[oid] ?? oid;
236
+ }
237
+ /**
238
+ * Entrées DNS et IP d'un `subjectAltName` tel que le rend
239
+ * `X509Certificate.subjectAltName` (`DNS:a, IP Address:1.2.3.4`).
240
+ */
241
+ function parseSubjectAltName(value) {
242
+ const dns = [];
243
+ const ip = [];
244
+ if (!value) return {
245
+ dns,
246
+ ip
247
+ };
248
+ for (const raw of value.split(/,\s*/)) {
249
+ const colon = raw.indexOf(":");
250
+ if (colon < 0) continue;
251
+ const kind = raw.slice(0, colon);
252
+ let entry = raw.slice(colon + 1);
253
+ if (entry.startsWith("\"")) try {
254
+ entry = String(JSON.parse(entry));
255
+ } catch {
256
+ continue;
257
+ }
258
+ if (kind === "DNS") dns.push(entry);
259
+ else if (kind === "IP Address") ip.push(entry);
260
+ }
261
+ return {
262
+ dns,
263
+ ip
264
+ };
265
+ }
266
+ /**
267
+ * Valeur d'un champ d'un nom distinctif tel que le rend
268
+ * `X509Certificate.subject` / `.issuer` (une ligne `CLÉ=valeur` par champ).
269
+ */
270
+ function distinguishedNameField(dn, key) {
271
+ for (const line of dn.split("\n")) if (line.startsWith(`${key}=`)) return line.slice(key.length + 1);
272
+ return null;
273
+ }
274
+ /** Contenu de la BIT STRING `subjectPublicKey` d'une SPKI DER. */
275
+ function subjectPublicKeyBits(spki) {
276
+ const [, key] = children(spki, readNode(spki, 0));
277
+ if (key?.tag !== 3) throw new Error("Clé publique SPKI sans subjectPublicKey.");
278
+ return spki.subarray(key.start + 1, key.end);
279
+ }
280
+ function signAsync(hash, data, key) {
281
+ return new Promise((resolve, reject) => {
282
+ sign(hash, data, key, (error, signature) => {
283
+ if (error) reject(error);
284
+ else resolve(signature);
285
+ });
286
+ });
287
+ }
288
+ function toPem(der) {
289
+ return `-----BEGIN CERTIFICATE-----\n${der.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "")}\n-----END CERTIFICATE-----\n`;
290
+ }
291
+ /**
292
+ * Fabrique un certificat serveur auto-signé et le rend en PEM.
293
+ *
294
+ * Contenu : version 3, sujet = émetteur, `basicConstraints` cA=false,
295
+ * `keyUsage` (digitalSignature, keyEncipherment — critique),
296
+ * `extKeyUsage` (serverAuth, clientAuth), `subjectAltName` (critique si le
297
+ * sujet est vide, RFC 5280 §4.2.1.6) et `subjectKeyIdentifier` (SHA-1 de la
298
+ * clé publique, méthode 1 de RFC 5280 §4.2.1.2).
299
+ *
300
+ * @param spec - clés, série, validité, noms et hachage
301
+ * @returns le certificat encodé PEM
302
+ * @throws Si un attribut ou une adresse IP est invalide
303
+ */
304
+ async function createSelfSignedCertificate(spec) {
305
+ const spki = spec.publicKey.export({
306
+ type: "spki",
307
+ format: "der"
308
+ });
309
+ const algorithm = sequence(objectIdentifier(SIGNATURE_OIDS[spec.hash]), DER_NULL);
310
+ const name = distinguishedName(spec.attributes);
311
+ const altNames = [...spec.dns.map((dns) => tlv(130, dnsNameBytes(dns))), ...spec.ip.map((ip) => tlv(135, ipAddressBytes(ip)))];
312
+ const extensions = [
313
+ extension(OID_BASIC_CONSTRAINTS, false, sequence()),
314
+ extension(OID_KEY_USAGE, true, tlv(3, Buffer.from([5, 160]))),
315
+ extension(OID_EXT_KEY_USAGE, false, sequence(objectIdentifier(OID_SERVER_AUTH), objectIdentifier(OID_CLIENT_AUTH)))
316
+ ];
317
+ if (altNames.length > 0) extensions.push(extension(OID_SUBJECT_ALT_NAME, spec.attributes.length === 0, sequence(...altNames)));
318
+ extensions.push(extension(OID_SUBJECT_KEY_ID, false, octetString(createHash("sha1").update(subjectPublicKeyBits(spki)).digest())));
319
+ const tbs = sequence(explicit(0, tlv(2, Buffer.from([2]))), positiveInteger(spec.serialHex), algorithm, name, sequence(time(spec.notBefore), time(spec.notAfter)), name, spki, explicit(3, sequence(...extensions)));
320
+ const signature = await signAsync(spec.hash, tbs, spec.privateKey);
321
+ return toPem(sequence(tbs, algorithm, bitString(signature)));
322
+ }
323
+ //#endregion
324
+ export { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, ipAddressBytes, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid };
@@ -99,7 +99,7 @@ var PrebuiltUi = class {
99
99
  return true;
100
100
  }
101
101
  kernel?.once("onReady", () => {
102
- (container?.get?.("server-static"))?.addMount?.(this.publicPath, this.distDir);
102
+ (container?.get?.("server-static"))?.addMount(this.publicPath, this.distDir);
103
103
  });
104
104
  return false;
105
105
  }
@@ -141,7 +141,7 @@ var Context = class extends Service {
141
141
  scheme;
142
142
  webSocketState = null;
143
143
  constructor(container, type) {
144
- super(`${type}`, container);
144
+ super(type, container);
145
145
  if (PERF_PROBE_SUB) {
146
146
  const p = globalThis.__nfPerfProbe;
147
147
  if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
@@ -227,7 +227,7 @@ var Context = class extends Service {
227
227
  }
228
228
  phaseStart(name) {
229
229
  if (!this._timingEnabled) return;
230
- if (this._phaseIndex === null) this._phaseIndex = /* @__PURE__ */ new Map();
230
+ this._phaseIndex ??= /* @__PURE__ */ new Map();
231
231
  const idx = this.phases.length;
232
232
  this.phases.push({
233
233
  name,
@@ -239,8 +239,8 @@ var Context = class extends Service {
239
239
  if (!this._timingEnabled || this._phaseIndex === null) return;
240
240
  const idx = this._phaseIndex.get(name);
241
241
  if (idx === void 0) return;
242
- const p = this.phases[idx];
243
- if (p.endMs !== void 0) return;
242
+ const p = this.phases.at(idx);
243
+ if (p === void 0 || p.endMs !== void 0) return;
244
244
  p.endMs = performance.now();
245
245
  p.durationMs = p.endMs - p.startMs;
246
246
  }
@@ -250,15 +250,28 @@ var Context = class extends Service {
250
250
  Promise.resolve().then(() => boundFn(this)).catch((e) => this.log(e, "ERROR", "onAfterResponse(late)"));
251
251
  return;
252
252
  }
253
- if (this._afterResponseFns === null) this._afterResponseFns = [];
253
+ this._afterResponseFns ??= [];
254
254
  this._afterResponseFns.push(boundFn);
255
255
  }
256
- async _runAfterResponse() {
257
- if (this._afterResponseFired) return;
256
+ /**
257
+ * Exécute les hooks `onAfterResponse`, une seule fois, dans l'ordre
258
+ * d'enregistrement ; un hook qui lève est journalisé sans arrêter les suivants.
259
+ *
260
+ * @returns `undefined` quand aucun hook n'est enregistré (le cas nominal) ou
261
+ * qu'ils ont déjà tourné : rien à attendre, le démontage continue sans
262
+ * suspension. Une `Promise` sinon, réglée quand le dernier hook a fini — elle
263
+ * ne rejette jamais.
264
+ */
265
+ _runAfterResponse() {
266
+ if (this._afterResponseFired) return void 0;
258
267
  this._afterResponseFired = true;
259
268
  const fns = this._afterResponseFns;
260
- if (fns === null || fns.length === 0) return;
269
+ if (fns === null || fns.length === 0) return void 0;
261
270
  this._afterResponseFns = null;
271
+ return this.runAfterResponseHooks(fns);
272
+ }
273
+ /** Boucle des hooks `onAfterResponse` — séquentielle, erreurs journalisées. */
274
+ async runAfterResponseHooks(fns) {
262
275
  for (const fn of fns) try {
263
276
  await fn(this);
264
277
  } catch (e) {
@@ -298,7 +311,7 @@ var Context = class extends Service {
298
311
  * notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
299
312
  */
300
313
  logEvent(event) {
301
- if (lifecycleEventLogging === null) lifecycleEventLogging = this.kernel?.environment !== "production";
314
+ lifecycleEventLogging ??= this.kernel?.environment !== "production";
302
315
  if (!lifecycleEventLogging) return;
303
316
  this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
304
317
  }
@@ -327,7 +340,9 @@ var Context = class extends Service {
327
340
  if (logger.shouldSample && !logger.shouldSample(this, err)) return;
328
341
  const entry = logger.renderHttp(this, err);
329
342
  return this.log(entry.text, entry.severity, entry.msgid);
330
- } catch {}
343
+ } catch {
344
+ return;
345
+ }
331
346
  }
332
347
  /**
333
348
  * P3.7 — Émet au teardown un log DEBUG détaillant la durée de chaque phase du
@@ -339,18 +354,18 @@ var Context = class extends Service {
339
354
  logPhasesVerbose() {
340
355
  if (!this._timingVerbose) return;
341
356
  const phases = this.phases;
342
- const n = phases.length;
343
- if (n === 0) return;
357
+ if (phases.length === 0) return;
344
358
  let line = "";
345
359
  let total = 0;
346
- for (let i = 0; i < n; i++) {
347
- const p = phases[i];
360
+ let i = 0;
361
+ for (const p of phases) {
348
362
  const d = p.durationMs ?? (p.endMs !== void 0 ? p.endMs - p.startMs : void 0);
349
363
  if (i > 0) line += " · ";
350
364
  if (d !== void 0) {
351
365
  line += `${p.name}=${d.toFixed(2)}ms`;
352
366
  total += d;
353
367
  } else line += `${p.name}=…`;
368
+ i++;
354
369
  }
355
370
  this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
356
371
  }
@@ -363,11 +378,12 @@ var Context = class extends Service {
363
378
  }
364
379
  }
365
380
  getRequestCookies(name) {
366
- if (name) return this.cookies[name] || null;
381
+ if (name) return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
367
382
  return this.cookies;
368
383
  }
369
384
  setCookie(cookie) {
370
- if (cookie) return this.response?.addCookie(cookie);
385
+ const received = cookie;
386
+ if (received) return this.response?.addCookie(received);
371
387
  }
372
388
  getRequest() {
373
389
  return this.request;
@@ -387,8 +403,7 @@ var Context = class extends Service {
387
403
  return Boolean(this.cookieSession);
388
404
  }
389
405
  getCookieSession(name) {
390
- if (this.cookies[name]) return this.cookies[name];
391
- return null;
406
+ return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
392
407
  }
393
408
  /**
394
409
  * Nom effectif du cookie de session pour CE transport. Sur TLS (https/wss) on
@@ -401,7 +416,7 @@ var Context = class extends Service {
401
416
  */
402
417
  getSessionCookieName() {
403
418
  const base = this.sessionService?.defaultSessionName ?? "nodefony";
404
- const mode = (this.sessionService?.options?.cookie)?.hostPrefix ?? "auto";
419
+ const mode = (this.sessionService?.options.cookie)?.hostPrefix ?? "auto";
405
420
  const tls = this.scheme === "https" || this.scheme === "wss";
406
421
  return mode === true || mode === "auto" && tls ? `__Host-${base}` : base;
407
422
  }
@@ -8,7 +8,7 @@ import { isIPv4 } from "node:net";
8
8
  * un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
9
9
  */
10
10
  function splitTopLevel(input, sep) {
11
- if (input.indexOf("\"") === -1) return input.split(sep);
11
+ if (!input.includes("\"")) return input.split(sep);
12
12
  const out = [];
13
13
  let buf = "";
14
14
  let inQuote = false;
@@ -43,7 +43,7 @@ function splitTopLevel(input, sep) {
43
43
  /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
44
44
  function unquote(value) {
45
45
  const v = value.trim();
46
- if (v.length >= 2 && v[0] === "\"" && v[v.length - 1] === "\"") return v.slice(1, -1).replace(/\\(.)/gu, "$1");
46
+ if (v.length >= 2 && v.startsWith("\"") && v.endsWith("\"")) return v.slice(1, -1).replace(/\\(.)/gu, "$1");
47
47
  return v;
48
48
  }
49
49
  /**
@@ -104,11 +104,11 @@ function forwardedNodeIp(node) {
104
104
  if (!node) return null;
105
105
  const n = node.trim();
106
106
  if (!n) return null;
107
- if (n[0] === "[") {
107
+ if (n.startsWith("[")) {
108
108
  const end = n.indexOf("]");
109
109
  return end > 1 ? n.slice(1, end) : null;
110
110
  }
111
- if (n === "unknown" || n[0] === "_") return null;
111
+ if (n === "unknown" || n.startsWith("_")) return null;
112
112
  const colon = n.indexOf(":");
113
113
  const host = colon === -1 ? n : n.slice(0, colon);
114
114
  return isIPv4(host) ? host : host || null;