@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,5 +1,6 @@
1
1
  import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
2
2
  import Context from "../Context.js";
3
+ import { toWsCloseReason } from "./wsCloseReason.js";
3
4
  import WebsocketResponse from "./Response.js";
4
5
  import { sanitizeRequestId } from "../requestId.js";
5
6
  import { FrameProfile } from "../../profiler/FrameProfile.js";
@@ -10,6 +11,24 @@ import { AsyncResource } from "node:async_hooks";
10
11
  import url, { URL } from "node:url";
11
12
  //#region nodefony/src/context/websocket/WebsocketContext.ts
12
13
  /**
14
+ * Appelle l'action résolue et rend TOUJOURS une promesse.
15
+ *
16
+ * `callController` rend une valeur ou une promesse, et peut lever de façon
17
+ * synchrone (#505). Démarré dans l'exécuteur d'une promesse, l'appel transforme
18
+ * une exception synchrone en REJET : les deux sites WS la traitent comme avant
19
+ * — le handshake par son `catch` de fermeture RFC 6455, le message en la
20
+ * rendant à l'appelant sans passer par le `catch` 1011.
21
+ *
22
+ * @param resolver - le résolveur de la connexion.
23
+ * @param data - les arguments de l'action (message reçu).
24
+ * @returns la promesse du rendu, rompue par toute erreur de l'action.
25
+ */
26
+ function callAsPromise(resolver, data) {
27
+ return new Promise((resolve) => {
28
+ resolve(resolver.callController(data));
29
+ });
30
+ }
31
+ /**
13
32
  * Coerce un code (applicatif / HTTP / WS) en code de fermeture WebSocket VALIDE
14
33
  * et conforme RFC 6455 §7.4, en PRÉFÉRANT les codes standard §7.4.1 quand le
15
34
  * sens existe :
@@ -65,7 +84,7 @@ var WebsocketContext = class extends Context {
65
84
  this.method = this.getMethod();
66
85
  this.origin = req.headers.origin ?? "";
67
86
  const checker = this.httpKernel?.getTrustProxyChecker();
68
- const socketAddress = req.socket?.remoteAddress;
87
+ const socketAddress = req.socket.remoteAddress;
69
88
  if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
70
89
  this.remoteAddress = this.getRemoteAddress();
71
90
  this.acceptedProtocol = req.headers["sec-websocket-protocol"];
@@ -104,7 +123,7 @@ var WebsocketContext = class extends Context {
104
123
  proxyHost: fwd.host ?? req.headers["x-forwarded-host"],
105
124
  proxyVia: req.headers.via
106
125
  };
107
- this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy?.proxyVia}`, "DEBUG");
126
+ this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy.proxyVia}`, "DEBUG");
108
127
  }
109
128
  }
110
129
  log(pci, severity, msgid, msg) {
@@ -117,9 +136,11 @@ var WebsocketContext = class extends Context {
117
136
  if (!logger) return;
118
137
  const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
119
138
  const pdu = this.log(entry.text, entry.severity, entry.msgid);
120
- if (pdu && pdu.requestId === void 0) pdu.requestId = this.requestId;
139
+ pdu.requestId ??= this.requestId;
121
140
  return pdu;
122
- } catch {}
141
+ } catch {
142
+ return;
143
+ }
123
144
  }
124
145
  async connect() {
125
146
  if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
@@ -140,37 +161,38 @@ var WebsocketContext = class extends Context {
140
161
  this.webSocketState = "error";
141
162
  throw new Error("Nodefony Websocket rejected");
142
163
  }
143
- if (!this.resolver) this.resolver = this.router?.resolve(this);
164
+ if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
144
165
  else try {
145
166
  this.resolver.match(this.resolver.route, this);
146
167
  } catch (e) {
147
- if (!this.rejected) this.reject(e.code ?? void 0, e.message);
168
+ if (!this.isRejected()) this.reject(e.code ?? void 0, e.message);
148
169
  throw e;
149
170
  }
150
171
  await this.fireAsync("onRequest", this, this.resolver);
151
172
  await this.kernel?.fireAsync("onRequest", this, this.resolver);
152
- if (this.resolver && this.resolver.resolve) {
173
+ if (this.resolver?.resolve) {
153
174
  this.setMetaData({ nodefony: { websocket: {
154
175
  state: this.webSocketState,
155
176
  protocol: this.acceptedProtocol
156
177
  } } });
157
- await this.resolver.callController(data).then(async () => {
178
+ await callAsPromise(this.resolver, data).then(async () => {
158
179
  await this.saveSession().then((session) => {
159
180
  if (session) this.log(`SAVE SESSION ID : ${session.id}`, "DEBUG");
160
- }).catch((e) => {
161
- throw e;
162
181
  });
163
182
  return this;
164
183
  }).catch((error) => {
165
184
  if (!this.rejected) {
166
- if (this.requestEnded) throw this.close(error.code, error.message);
185
+ if (this.requestEnded) {
186
+ this.close(error.code, error.message);
187
+ throw error;
188
+ }
167
189
  this.reject(error.code ?? void 0, error.message);
168
190
  this.rejected = true;
169
191
  this.webSocketState = "error";
170
192
  throw error;
171
193
  }
172
194
  });
173
- } else if (!this.rejected) {
195
+ } else if (!this.isRejected()) {
174
196
  this.reject(4004, "Not Found");
175
197
  this.rejected = true;
176
198
  this.webSocketState = "error";
@@ -204,7 +226,8 @@ var WebsocketContext = class extends Context {
204
226
  this.logMessageContent("BROADCAST", payload);
205
227
  this.fire("onMessage", payload, this, "BROADCAST");
206
228
  this.fire("onBroadcast", payload, this);
207
- return this.response.broadcast(payload, encoding);
229
+ this.response.broadcast(payload, encoding);
230
+ return;
208
231
  }
209
232
  }
210
233
  return null;
@@ -219,7 +242,7 @@ var WebsocketContext = class extends Context {
219
242
  * @param data - charge utile (string, Buffer binaire, objet, ou null).
220
243
  */
221
244
  logMessageContent(dir, data) {
222
- if (wsContentLogging === null) wsContentLogging = this.kernel?.environment !== "production";
245
+ wsContentLogging ??= this.kernel?.environment !== "production";
223
246
  if (!wsContentLogging) return;
224
247
  this.log(formatWsLogContent(data), "DEBUG", `WS ${dir}`);
225
248
  }
@@ -292,23 +315,22 @@ var WebsocketContext = class extends Context {
292
315
  this.webSocketState = "message";
293
316
  const message = isBinary ? data : data.toString();
294
317
  this.logMessageContent("RECEIVE", message);
295
- if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data.toString());
318
+ if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data);
296
319
  try {
297
- if (!this.resolver) this.resolver = this.router?.resolve(this);
298
- else this.resolver.match(this.resolver?.route, this);
320
+ if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
321
+ else this.resolver.match(this.resolver.route, this);
299
322
  await this.fireAsync("onMessage", message, this, "RECEIVE");
300
- if (this.resolver.resolve) {
323
+ if (this.resolver?.resolve) {
301
324
  this.setMetaData({ nodefony: { websocket: {
302
325
  state: this.webSocketState,
303
326
  protocol: this.acceptedProtocol
304
327
  } } });
305
- return this.resolver.callController([message]).catch((e) => {
306
- throw e;
307
- });
328
+ return callAsPromise(this.resolver, [message]);
308
329
  } else if (!this.rejected) {
309
330
  this.reject(4004, "Not Found");
310
331
  this.rejected = true;
311
332
  }
333
+ return;
312
334
  } catch (e) {
313
335
  this.reject(1011, "Internal Error");
314
336
  throw e;
@@ -338,7 +360,7 @@ var WebsocketContext = class extends Context {
338
360
  */
339
361
  onConnectionError(error) {
340
362
  this.webSocketState = "error";
341
- this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error?.message}`, "ERROR", `${this.type} ${logColor.red("SOCKET ERROR")} ${this.method}`);
363
+ this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.type} ${logColor.red("SOCKET ERROR")} ${this.method}`);
342
364
  this.fire("onError", error, this);
343
365
  }
344
366
  setScheme() {
@@ -346,7 +368,7 @@ var WebsocketContext = class extends Context {
346
368
  }
347
369
  getRemoteAddress() {
348
370
  if (this.forwarded) return this.forwarded.clientIp;
349
- return this.request?.socket?.remoteAddress ?? null;
371
+ return this.request?.socket.remoteAddress ?? null;
350
372
  }
351
373
  getHost() {
352
374
  return this.request?.headers.host;
@@ -372,15 +394,26 @@ var WebsocketContext = class extends Context {
372
394
  super.clean();
373
395
  }
374
396
  close(reasonCode, description) {
375
- if (this.response) return this.response.close(toWsCloseCode(reasonCode), description);
397
+ if (this.response) this.response.close(toWsCloseCode(reasonCode), description);
376
398
  }
377
399
  drop(reasonCode, description) {
378
- if (this.response) return this.response.drop(reasonCode, description);
400
+ if (this.response) this.response.drop(reasonCode, description);
401
+ }
402
+ /**
403
+ * Relit `rejected` sans le rétrécissement de TypeScript : après la garde
404
+ * d'entrée de {@link handle}, le compilateur le croit `false` pour toute la
405
+ * méthode, alors qu'un écouteur `onRequest` (pare-feu) ou le `match` du
406
+ * resolver peut avoir rejeté entre-temps.
407
+ *
408
+ * @returns `true` si la connexion a été rejetée.
409
+ */
410
+ isRejected() {
411
+ return this.rejected;
379
412
  }
380
413
  reject(code, message) {
381
414
  if (this.connection && this.connection.readyState === Ws.OPEN) {
382
415
  const raw = typeof code === "string" ? parseInt(code, 10) : code;
383
- this.connection.close(toWsCloseCode(raw), message ?? "Rejected");
416
+ this.connection.close(toWsCloseCode(raw), toWsCloseReason(message ?? "Rejected"));
384
417
  }
385
418
  this.rejected = true;
386
419
  }
@@ -0,0 +1,41 @@
1
+ //#region nodefony/src/context/websocket/wsCloseReason.ts
2
+ /**
3
+ * Taille maximale, en octets UTF-8, de la raison d'une trame Close (RFC 6455
4
+ * §5.5 : charge d'une trame de contrôle ≤ 125 octets, dont 2 pour le code).
5
+ */
6
+ const MAX_WS_CLOSE_REASON_BYTES = 123;
7
+ /** Marqueur de troncature : « … » (U+2026) pèse 3 octets en UTF-8. */
8
+ const ELLIPSIS = "…";
9
+ /**
10
+ * Borne la raison d'une fermeture WebSocket à 123 octets UTF-8, coupée sur une
11
+ * frontière de caractère.
12
+ *
13
+ * `ws.close(code, reason)` LÈVE une `RangeError` au-delà de 123 octets, et la
14
+ * connexion n'est alors PAS fermée : jusqu'en `ws` 8.21 elle restait en
15
+ * `CLOSING` sans trame Close ni minuterie — hors de portée de toute garde
16
+ * `readyState === OPEN` et du heartbeat, donc ouverte jusqu'à la mort TCP ;
17
+ * depuis 8.22 elle reste `OPEN`. Or la raison porte souvent un message
18
+ * d'erreur, dont rien ne borne la longueur.
19
+ *
20
+ * @param reason - raison candidate (message d'erreur, libellé…)
21
+ * @returns la raison telle quelle si elle tient, sinon tronquée et terminée
22
+ * par « … » ; chaîne vide si elle n'est pas une chaîne.
23
+ */
24
+ function toWsCloseReason(reason) {
25
+ if (typeof reason !== "string") return "";
26
+ if (reason.length <= 41) return reason;
27
+ if (Buffer.byteLength(reason, "utf8") <= 123) return reason;
28
+ const budget = 120;
29
+ let bytes = 0;
30
+ let end = 0;
31
+ for (const char of reason) {
32
+ const cp = char.codePointAt(0);
33
+ const size = cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
34
+ if (bytes + size > budget) break;
35
+ bytes += size;
36
+ end += char.length;
37
+ }
38
+ return reason.slice(0, end) + ELLIPSIS;
39
+ }
40
+ //#endregion
41
+ export { MAX_WS_CLOSE_REASON_BYTES, toWsCloseReason };
@@ -25,7 +25,7 @@ function cookiesParser(context) {
25
25
  case "http":
26
26
  case "https":
27
27
  case "http2":
28
- if (context.request.request && context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
28
+ if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
29
29
  if (cookies) {
30
30
  const obj = parser(cookies);
31
31
  for (const cookie in obj) {
@@ -37,7 +37,7 @@ function cookiesParser(context) {
37
37
  break;
38
38
  case "websocket":
39
39
  case "websocket-secure": {
40
- const cookieHeader = context.request?.headers?.cookie;
40
+ const cookieHeader = context.request?.headers.cookie;
41
41
  if (cookieHeader) {
42
42
  const parsed = parser(cookieHeader);
43
43
  for (const name in parsed) {
@@ -65,7 +65,7 @@ var Cookie = class Cookie {
65
65
  sameSite;
66
66
  priority;
67
67
  constructor(cookiesOrName, value, options) {
68
- this.options = extend({}, cookieDefaultSettings, options || {});
68
+ this.options = extend({}, cookieDefaultSettings, options ?? {});
69
69
  if (!cookiesOrName) throw new Error("cookie must have name");
70
70
  this.name = cookiesOrName;
71
71
  if (typeof cookiesOrName === "string") {
@@ -32,16 +32,16 @@ var HttpError = class extends nodefonyError {
32
32
  constructor(message, code, context) {
33
33
  super(message, code);
34
34
  this.context = context;
35
- this.response = context?.response;
36
- this.request = context?.request;
35
+ const res = context?.response;
36
+ this.response = res ?? void 0;
37
+ this.request = context?.request ?? void 0;
37
38
  this.url = this.context?.url;
38
- if (this.response && code) this.response.statusCode = code;
39
- if (!this.message) this.message = (context?.response)?.body?.toString() || (context?.response)?.statusMessage || (context?.response)?.getStatusMessage();
39
+ if (res && code) res.statusCode = code;
40
+ if (!this.message && res) this.message = res.body?.toString() || res.statusMessage || res.getStatusMessage();
40
41
  const resolver = context?.resolver;
41
42
  this.controller = resolver?.controller?.name ?? void 0;
42
43
  this.action = resolver?.actionName ?? void 0;
43
- const res = context?.response;
44
- if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage ?? ""}`.trim();
44
+ if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage}`.trim();
45
45
  }
46
46
  toString() {
47
47
  if (this.context) return `${clc.red(this.message)}
@@ -0,0 +1,27 @@
1
+ //#region nodefony/src/errors/thrownMessage.ts
2
+ /**
3
+ * Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
4
+ *
5
+ * Même rendu que `String(value)` pour une `Error` (son `message`) et pour les
6
+ * primitives ; un objet quelconque rend son étiquette (`[object Object]`) sans
7
+ * appeler un `toString` arbitraire ni sérialiser son contenu — ce texte peut
8
+ * finir dans une réponse ou un journal, il ne doit rien divulguer.
9
+ *
10
+ * @param value - la valeur levée.
11
+ * @returns un texte court, jamais vide pour une valeur définie.
12
+ */
13
+ function thrownMessage(value) {
14
+ if (value instanceof Error) return value.message;
15
+ switch (typeof value) {
16
+ case "string": return value;
17
+ case "number":
18
+ case "boolean":
19
+ case "bigint":
20
+ case "symbol":
21
+ case "undefined": return String(value);
22
+ case "object":
23
+ case "function": return value === null ? "null" : Object.prototype.toString.call(value);
24
+ }
25
+ }
26
+ //#endregion
27
+ export { thrownMessage };
@@ -1,3 +1,4 @@
1
+ import { thrownMessage } from "../errors/thrownMessage.js";
1
2
  //#region nodefony/src/profiler/FrameProfile.ts
2
3
  /**
3
4
  * Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
@@ -72,8 +73,8 @@ var FrameProfile = class {
72
73
  phaseEnd(name) {
73
74
  if (!this.timing) return;
74
75
  for (let i = this.phases.length - 1; i >= 0; i--) {
75
- const p = this.phases[i];
76
- if (p.name === name && p.endMs === void 0) {
76
+ const p = this.phases.at(i);
77
+ if (p?.name === name && p.endMs === void 0) {
77
78
  p.endMs = performance.now();
78
79
  p.durationMs = p.endMs - p.startMs;
79
80
  return;
@@ -88,7 +89,7 @@ var FrameProfile = class {
88
89
  */
89
90
  finish(status, error) {
90
91
  this.response = { statusCode: status };
91
- if (error !== void 0 && error !== null) this.error = { message: error instanceof Error ? error.message : String(error) };
92
+ if (error !== void 0 && error !== null) this.error = { message: thrownMessage(error) };
92
93
  }
93
94
  };
94
95
  //#endregion
@@ -2,8 +2,8 @@
2
2
  /** Durée totale = (fin de la dernière phase terminée) − (début de la 1ère). */
3
3
  function totalDuration(phases) {
4
4
  if (phases.length === 0) return null;
5
- const first = phases[0];
6
- if (typeof first.startMs !== "number") return null;
5
+ const first = phases.at(0);
6
+ if (first === void 0 || typeof first.startMs !== "number") return null;
7
7
  let end = first.startMs;
8
8
  for (const p of phases) {
9
9
  const e = p.startMs + (p.durationMs ?? 0);
@@ -71,8 +71,8 @@ var Profiler = class {
71
71
  recent(limit = 60) {
72
72
  const out = [];
73
73
  const entries = [...this._buf.values()];
74
- for (let i = entries.length - 1; i >= 0 && out.length < limit; i--) {
75
- const e = entries[i];
74
+ for (const e of entries.reverse()) {
75
+ if (out.length >= limit) break;
76
76
  out.push({
77
77
  requestId: e.requestId,
78
78
  ts: e.ts,
@@ -0,0 +1,29 @@
1
+ //#region nodefony/src/servers/kernelServers.ts
2
+ /**
3
+ * Rétrécit la section `servers` de la configuration du kernel (lue dans le sac
4
+ * de config non typé du cœur) en {@link ServersConfig}, sans propager `any`.
5
+ *
6
+ * Seule lecture de `kernel.options.servers` dans `@nodefony/http` : les serveurs
7
+ * et le `HttpKernel` passent tous par ici.
8
+ *
9
+ * @param servers - la valeur brute de `kernel.options.servers`.
10
+ * @returns la section typée, ou `undefined` si elle est absente ou n'est pas un objet.
11
+ */
12
+ function asServersConfig(servers) {
13
+ return servers !== null && typeof servers === "object" ? servers : void 0;
14
+ }
15
+ /**
16
+ * Port DEMANDÉ pour un serveur (`servers.<type>.port`), ou `undefined` si le
17
+ * serveur est désactivé, absent, ou sans port numérique.
18
+ *
19
+ * @param servers - section `servers` déjà rétrécie ({@link asServersConfig}).
20
+ * @param type - le serveur visé.
21
+ */
22
+ function configuredServerPort(servers, type) {
23
+ const entry = servers?.[type];
24
+ if (!entry) return void 0;
25
+ const port = entry.port;
26
+ return typeof port === "number" ? port : void 0;
27
+ }
28
+ //#endregion
29
+ export { asServersConfig, configuredServerPort };
@@ -1,25 +1,53 @@
1
- import { isPortListening } from "nodefony";
1
+ import { isPortListening, resolvePortPolicy } from "nodefony";
2
+ import cluster from "node:cluster";
2
3
  //#region nodefony/src/servers/portBinder.ts
3
- /** Nombre de ports essayés après le désiré, en `auto`. */
4
- const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
5
4
  /**
6
- * Politique de port effective.
7
- *
8
- * Le défaut dépend de l'environnement, et ce n'est pas de la coquetterie :
9
- * - **production** → `strict`. Le port y est un contrat (service k8s, ingress,
10
- * sonde de santé). Un bind silencieux ailleurs donnerait un pod déclaré sain
11
- * que personne n'atteint : une panne invisible, le pire des deux mondes.
12
- * - **test** → `strict`. Un port occupé veut dire « un serveur est resté debout » ;
13
- * le banc doit s'arrêter, pas viser à côté (il taperait le serveur du voisin).
14
- * - **développement** → `auto`. Ici un port pris n'est qu'une nuisance.
15
- *
16
- * @param environment - `kernel.environment` (normalisé `development`/`production`/`test`).
17
- * @param explicit - `servers.portPolicy` s'il est configuré (il gagne toujours).
5
+ * **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
6
+ *
7
+ * Pourquoi ce fichier existe : lancer deux apps Nodefony côte à côte (le repo et
8
+ * une app générée, deux projets, un banc et une démo…) faisait mourir la seconde
9
+ * en `EADDRINUSE`. Le port désiré est une PRÉFÉRENCE en développement ; il reste
10
+ * un CONTRAT en production.
11
+ *
12
+ * ## Pourquoi retenter au `listen()` plutôt que sonder d'abord
13
+ *
14
+ * La tentation est d'appeler « le port 5151 est-il libre ? » puis de binder. C'est
15
+ * une **course** (TOCTOU) : entre la réponse et le bind, un autre process peut
16
+ * prendre le port — et on échoue quand même, après avoir cru le contraire. Le
17
+ * `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
18
+ * `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
19
+ *
20
+ * ## Pourquoi une SONDE malgré cela
21
+ *
22
+ * Le `listen()` est atomique, mais il ne dit pas tout : un port peut être **déjà
23
+ * servi** et le noyau l'accorder quand même. Lier `0.0.0.0:P` alors qu'un autre
24
+ * processus tient `127.0.0.1:P` **réussit** sur macOS et les BSD — ce sont deux
25
+ * liaisons distinctes, et les connexions locales partent à la PLUS SPÉCIFIQUE.
26
+ * Entre familles d'adresses (`::1` tenu, `0.0.0.0` demandé), même linux
27
+ * l'accorde. L'application annonce alors `READY`, publie ses ports… et ne reçoit
28
+ * rien : une panne muette, et un banc qui interroge le serveur du VOISIN.
29
+ *
30
+ * On constate donc, AVANT de binder, qu'aucun serveur ne répond déjà là où notre
31
+ * liaison va recevoir (`isPortListening` — une connexion en boucle locale, aucun
32
+ * outil système, même verdict sur les trois plateformes). Ce n'est pas la sonde
33
+ * « ce port est-il libre ? » écartée plus haut : elle ne remplace pas le repli
34
+ * atomique, qui reste le seul à trancher la course — elle ajoute le seul conflit
35
+ * que le noyau n'exprime JAMAIS. La course résiduelle (un tiers prend le port
36
+ * entre la sonde et le bind) retombe donc sur `EADDRINUSE`, inchangé.
37
+ *
38
+ * ## Pourquoi sauter les ports réservés
39
+ *
40
+ * HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
41
+ * voler 5152 à HTTPS — qui se décalerait à son tour, en cascade. Les ports que les
42
+ * AUTRES serveurs convoitent sont donc sautés d'emblée.
43
+ *
44
+ * ## Fail-loud
45
+ *
46
+ * Un décalage est TOUJOURS annoncé (`onShift`) : une app qui écoute ailleurs que
47
+ * là où on l'attend sans le dire est une dégradation silencieuse.
18
48
  */
19
- function resolvePortPolicy(environment, explicit) {
20
- if (explicit === "auto" || explicit === "strict") return explicit;
21
- return environment === "development" ? "auto" : "strict";
22
- }
49
+ /** Nombre de ports essayés après le désiré, en `auto`. */
50
+ const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
23
51
  /** Port configuré d'un serveur, ou `0` (désactivé / non précisé → choix noyau). */
24
52
  function configuredPort(entry) {
25
53
  if (!entry) return 0;
@@ -106,6 +134,8 @@ function portConflictError(port, host) {
106
134
  error.code = "EADDRINUSE";
107
135
  return error;
108
136
  }
137
+ /** Plus grand numéro de port TCP (RFC 793) : au-delà, `listen` lève un `RangeError`. */
138
+ const MAX_PORT = 65535;
109
139
  /** Prochain candidat : incrémente, en sautant ce que les autres serveurs veulent. */
110
140
  function nextCandidate(from, reserved) {
111
141
  let port = from + 1;
@@ -143,12 +173,21 @@ function isPortUnavailable(code, port) {
143
173
  * handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
144
174
  * verrait passer les `EADDRINUSE` de repli et croirait à une panne).
145
175
  *
176
+ * @param server - serveur à mettre en écoute.
177
+ * @param host - hôte passé à `listen`.
178
+ * @param plan - port désiré et nombre de replis permis.
179
+ * @param probe - sonde de présence (injectée par le banc).
180
+ * @param sharedListener - vrai dans un worker `node:cluster` : le port y est
181
+ * PARTAGÉ par le maître, et un voisin qui y répond est un frère, pas un
182
+ * conflit. Sans cette exception, `nodefony cluster -w N` ne gardait qu'un
183
+ * worker en vie — les autres mouraient sur « port déjà servi » et
184
+ * bouclaient en relance.
146
185
  * @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
147
186
  * @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
148
187
  * (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
149
188
  * indisponible après épuisement des essais (le fallback n'est PAS infini).
150
189
  */
151
- async function bindWithFallback(server, host, plan, probe = isPortListening) {
190
+ async function bindWithFallback(server, host, plan, probe = isPortListening, sharedListener = cluster.isWorker) {
152
191
  let candidate = plan.desired;
153
192
  let used = 0;
154
193
  /**
@@ -170,16 +209,19 @@ async function bindWithFallback(server, host, plan, probe = isPortListening) {
170
209
  server.listen(port, host);
171
210
  });
172
211
  for (;;) {
173
- const conflict = plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
212
+ const conflict = sharedListener || plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
174
213
  const failure = conflict ? portConflictError(candidate, conflict) : await listenOnce(candidate);
175
214
  if (failure === null) return {
176
215
  address: server.address(),
177
216
  shiftedFrom: candidate === plan.desired ? null : plan.desired
178
217
  };
179
- if (!conflict && !isPortUnavailable(failure.code, candidate)) throw failure;
218
+ const code = failure.code;
219
+ if (!conflict && !isPortUnavailable(typeof code === "string" ? code : void 0, candidate)) throw failure;
180
220
  if (plan.desired === 0 || used >= plan.attempts) throw failure;
181
221
  used += 1;
182
- candidate = nextCandidate(candidate, plan.reserved);
222
+ const next = nextCandidate(candidate, plan.reserved);
223
+ if (next > MAX_PORT) throw failure;
224
+ candidate = next;
183
225
  }
184
226
  }
185
227
  //#endregion
@@ -57,8 +57,9 @@ var Session = class {
57
57
  constructor(name, options, manager) {
58
58
  this.options = extend({}, defaultSessionOptions, options);
59
59
  this.manager = manager;
60
- this.storage = this.manager.storage;
61
- if (!this.storage) this.status = "disabled";
60
+ const storage = this.manager.storage;
61
+ this.storage = storage;
62
+ if (!storage) this.status = "disabled";
62
63
  this.setName(name);
63
64
  this.strategy = this.manager.sessionStrategy;
64
65
  }
@@ -195,7 +196,7 @@ var Session = class {
195
196
  return true;
196
197
  }
197
198
  setCookieSession(leftTime, options = {}) {
198
- if (this.context && this.context.response) {
199
+ if (this.context?.response) {
199
200
  const settings = extend({}, this.options.cookie, options);
200
201
  if (leftTime) settings.maxAge = leftTime;
201
202
  const cookie = new Cookie(this.context.getSessionCookieName(), this.id, settings);
@@ -207,7 +208,7 @@ var Session = class {
207
208
  return null;
208
209
  }
209
210
  deleteCookieSession() {
210
- if (this.context && this.context.response) {
211
+ if (this.context?.response) {
211
212
  let cookie = this.cookieSession;
212
213
  if (cookie) cookie.expires = /* @__PURE__ */ new Date(0);
213
214
  else cookie = new Cookie(this.context.getSessionCookieName(), "", { expires: /* @__PURE__ */ new Date(0) });
@@ -219,11 +220,17 @@ var Session = class {
219
220
  return this.cookieSession;
220
221
  }
221
222
  isValidSession(_data, context) {
222
- if (this.options.refererCheck) try {
223
- return this.checkSecureReferer(context);
224
- } catch {
225
- this.log(`SESSION REFERER MISMATCH ==> ${this.name} : ${this.id}`, "WARNING");
226
- return false;
223
+ if (this.options.refererCheck) {
224
+ let sameHost = false;
225
+ try {
226
+ sameHost = this.checkSecureReferer(context);
227
+ } catch {
228
+ sameHost = false;
229
+ }
230
+ if (!sameHost) {
231
+ this.log(`SESSION REFERER MISMATCH ==> ${this.name} : ${this.id}`, "WARNING");
232
+ return false;
233
+ }
227
234
  }
228
235
  const now = Date.now();
229
236
  if (this.options.absoluteTimeoutS && this.created) {
@@ -347,7 +354,8 @@ var Session = class {
347
354
  user: user ?? this.user ?? ""
348
355
  };
349
356
  }
350
- deSerialize(data) {
357
+ deSerialize(serialized) {
358
+ const data = serialized;
351
359
  if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
352
360
  if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
353
361
  if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
@@ -1,3 +1,4 @@
1
+ import { thrownMessage } from "../errors/thrownMessage.js";
1
2
  //#region nodefony/src/session/sessionStoreFailure.ts
2
3
  /**
3
4
  * Reconnaît une table de session absente, quel que soit le dialecte.
@@ -18,7 +19,7 @@ function looksLikeMissingTable(text) {
18
19
  * @returns le statut, le corps à servir, et la ligne de journal.
19
20
  */
20
21
  function describeSessionStoreFailure(error) {
21
- const cause = error instanceof Error ? error.message : String(error ?? "cause inconnue");
22
+ const cause = error === null || error === void 0 ? "cause inconnue" : thrownMessage(error);
22
23
  return {
23
24
  statusCode: 500,
24
25
  body: "Internal Server Error — session store unavailable",
@@ -82,14 +82,29 @@ var RevocationGuardStorage = class {
82
82
  constructor(inner) {
83
83
  this.inner = inner;
84
84
  this.sortableFields = inner.sortableFields;
85
- if (typeof inner.listAll === "function") this.listAll = (filter) => inner.listAll(filter);
86
- if (typeof inner.listPage === "function") this.listPage = (query) => inner.listPage(query);
87
- if (typeof inner.countSessions === "function") this.countSessions = (query) => inner.countSessions(query);
88
- if (typeof inner.countDistinctUsers === "function") this.countDistinctUsers = (query) => inner.countDistinctUsers(query);
89
- if (typeof inner.touch === "function") this.touch = (id, idleSeconds) => {
90
- if (this.#isRevoked(id)) return Promise.resolve();
91
- return inner.touch(id, idleSeconds);
92
- };
85
+ if (typeof inner.listAll === "function") {
86
+ const src = inner;
87
+ this.listAll = (filter) => src.listAll(filter);
88
+ }
89
+ if (typeof inner.listPage === "function") {
90
+ const src = inner;
91
+ this.listPage = (query) => src.listPage(query);
92
+ }
93
+ if (typeof inner.countSessions === "function") {
94
+ const src = inner;
95
+ this.countSessions = (query) => src.countSessions(query);
96
+ }
97
+ if (typeof inner.countDistinctUsers === "function") {
98
+ const src = inner;
99
+ this.countDistinctUsers = (query) => src.countDistinctUsers(query);
100
+ }
101
+ if (typeof inner.touch === "function") {
102
+ const src = inner;
103
+ this.touch = (id, idleSeconds) => {
104
+ if (this.#isRevoked(id)) return Promise.resolve();
105
+ return src.touch(id, idleSeconds);
106
+ };
107
+ }
93
108
  }
94
109
  read(id) {
95
110
  return this.inner.read(id);
@@ -117,7 +132,7 @@ var RevocationGuardStorage = class {
117
132
  #revoke(id) {
118
133
  const now = Date.now();
119
134
  this.#purgeExpired(now);
120
- if (this.#tombstones === null) this.#tombstones = /* @__PURE__ */ new Map();
135
+ this.#tombstones ??= /* @__PURE__ */ new Map();
121
136
  this.#tombstones.set(id, now + TOMBSTONE_TTL_MS);
122
137
  }
123
138
  #isRevoked(id) {