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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +4 -2
  3. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  4. package/dist/nodefony/command/certificatesCommand.js +1 -1
  5. package/dist/nodefony/command/networkCommand.js +1 -1
  6. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  7. package/dist/nodefony/config/config.js +22 -1
  8. package/dist/nodefony/interfaces/IRouting.js +1 -0
  9. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  10. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  11. package/dist/nodefony/service/audit-logger.js +16 -13
  12. package/dist/nodefony/service/certificates.js +25 -13
  13. package/dist/nodefony/service/error-renderer.js +11 -8
  14. package/dist/nodefony/service/http-kernel.js +340 -163
  15. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  16. package/dist/nodefony/service/servers/clientError.js +2 -2
  17. package/dist/nodefony/service/servers/server-http.js +20 -20
  18. package/dist/nodefony/service/servers/server-https.js +38 -39
  19. package/dist/nodefony/service/servers/server-static.js +13 -17
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
  21. package/dist/nodefony/service/servers/server-websocket.js +8 -7
  22. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  23. package/dist/nodefony/service/sessions/sessions-service.js +19 -12
  24. package/dist/nodefony/service/trace.js +1 -1
  25. package/dist/nodefony/service/upload/upload-service.js +8 -7
  26. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  27. package/dist/nodefony/src/context/Context.js +34 -19
  28. package/dist/nodefony/src/context/forwarded.js +4 -4
  29. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  30. package/dist/nodefony/src/context/http/Request.js +93 -44
  31. package/dist/nodefony/src/context/http/Response.js +107 -51
  32. package/dist/nodefony/src/context/http/parser.js +9 -10
  33. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  34. package/dist/nodefony/src/context/http2/Response.js +53 -25
  35. package/dist/nodefony/src/context/metaData.js +1 -1
  36. package/dist/nodefony/src/context/trustProxy.js +4 -3
  37. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  38. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  39. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  40. package/dist/nodefony/src/cookies/cookie.js +3 -3
  41. package/dist/nodefony/src/errors/httpError.js +6 -6
  42. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  43. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  44. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  45. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  46. package/dist/nodefony/src/servers/portBinder.js +8 -21
  47. package/dist/nodefony/src/session/session.js +18 -10
  48. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  49. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  50. package/dist/types/index.d.ts +16 -0
  51. package/dist/types/nodefony/config/config.d.ts +22 -0
  52. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  53. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  54. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  55. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  56. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  57. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  58. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  59. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  60. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  61. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  62. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  63. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  64. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  65. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  66. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  67. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  68. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  69. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  71. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  72. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  73. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  74. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  76. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  77. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  78. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  79. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  80. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  81. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  82. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  83. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  84. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  85. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  86. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  87. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  88. package/docs/cookies.md +20 -20
  89. package/docs/observabilite.md +25 -25
  90. package/docs/rate-limit.md +18 -18
  91. package/docs/servers.md +52 -52
  92. package/docs/session.md +65 -60
  93. package/docs/upload.md +36 -30
  94. package/package.json +7 -7
@@ -1,20 +1,43 @@
1
1
  import HttpResponse from "../http/Response.js";
2
- import { extend } from "nodefony";
2
+ import { thenMaybe } from "nodefony";
3
3
  import http from "node:http";
4
4
  import http2 from "node:http2";
5
5
  //#region nodefony/src/context/http2/Response.ts
6
6
  const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
7
+ /**
8
+ * Compose les en-têtes d'une réponse HTTP/2 : ceux posés sur la réponse, puis
9
+ * ceux passés à `writeHead`, qui gagnent.
10
+ *
11
+ * Les clés sont ramenées en MINUSCULES : `getHeaders()` rend `content-type`,
12
+ * un contrôleur passe `Content-Type` — fusionnées telles quelles, les deux
13
+ * clés partaient ensemble et `stream.respond()` refusait la réponse
14
+ * (`ERR_HTTP2_HEADER_SINGLE_VALUE`), soit une 500 sur tout média servi en
15
+ * HTTP/2. HTTP/2 n'a de toute façon que des noms en minuscules (RFC 9113 §8.2.1).
16
+ *
17
+ * @param statusMessage - message de statut, recopié en `x-status-message`
18
+ * @param current - en-têtes déjà posés sur la réponse
19
+ * @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
20
+ * @returns les en-têtes à transmettre à `respond()`
21
+ */
22
+ function mergeResponseHeaders(statusMessage, current, extra) {
23
+ const merged = { "x-status-message": statusMessage };
24
+ for (const key in current) merged[key.toLowerCase()] = current[key];
25
+ if (Array.isArray(extra)) for (let i = 0; i + 1 < extra.length; i += 2) merged[String(extra[i]).toLowerCase()] = extra[i + 1];
26
+ else if (extra) for (const key in extra) {
27
+ const value = extra[key];
28
+ if (value !== void 0) merged[key.toLowerCase()] = value;
29
+ }
30
+ return merged;
31
+ }
7
32
  var Http2Response = class extends HttpResponse {
8
33
  statusCode = 200;
9
34
  stream = null;
10
35
  streamId;
11
36
  constructor(response, context) {
12
37
  super(response, context);
13
- if (response) {
14
- this.stream = response.stream;
15
- this.streamId = this.stream.id;
16
- }
17
- if (this.stream && this.stream.pushAllowed) this.context.pushAllowed = true;
38
+ this.stream = response.stream;
39
+ this.streamId = this.stream.id;
40
+ if (this.stream.pushAllowed) this.context.pushAllowed = true;
18
41
  }
19
42
  isHeaderSent() {
20
43
  if (this.stream) return this.stream.headersSent;
@@ -35,7 +58,7 @@ var Http2Response = class extends HttpResponse {
35
58
  this.statusMessage = this.getStatusMessage();
36
59
  this.setLength();
37
60
  this.ensureContentTypeHeader();
38
- this.headers = extend(true, { "X-Status-Message": this.statusMessage }, this.getHeaders(), headers);
61
+ this.headers = mergeResponseHeaders(this.statusMessage, this.getHeaders(), headers);
39
62
  this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
40
63
  const requestId = this.context.requestId;
41
64
  if (requestId) this.headers["x-request-id"] = requestId;
@@ -45,39 +68,44 @@ var Http2Response = class extends HttpResponse {
45
68
  } else this.log("Headers already sent !!", "WARNING");
46
69
  } else return super.writeHead(statusCode, headers);
47
70
  }
48
- async send(chunk, encoding, _flush = false) {
71
+ /**
72
+ * Écrit le corps sur le flux HTTP/2. Synchrone pour une redirection, un flux
73
+ * fermé ou le repli HTTP/1 ; une promesse quand le corps est écrit sur le
74
+ * flux, réglée par le rappel d'écriture (#505).
75
+ *
76
+ * @returns la réponse, ou sa promesse.
77
+ */
78
+ send(chunk, encoding, _flush = false) {
49
79
  if (this.context.isRedirect) {
50
80
  if (this.stream && !this.stream.headersSent) this.writeHead();
51
- await this.end();
52
- return this;
81
+ return thenMaybe(this.end(), () => this);
53
82
  }
54
83
  const stream = this.stream;
55
- if (!stream) return await super.send(chunk, encoding);
84
+ if (!stream) return super.send(chunk, encoding);
56
85
  if (stream.destroyed || !stream.writable) {
57
86
  this.log("HTTP/2 stream not writable before send — skip", "DEBUG");
58
87
  return this;
59
88
  }
60
89
  if (chunk) this.setBody(chunk);
61
- if (this.body === null) this.body = Buffer.alloc(0);
62
90
  return new Promise((resolve, reject) => {
63
- stream.write(this.body, encoding || this.encoding, (error) => {
91
+ stream.write(this.payload, this.payloadEncoding, (error) => {
64
92
  if (error) return reject(error);
65
93
  return resolve(this);
66
94
  });
67
95
  });
68
96
  }
97
+ /**
98
+ * Termine le flux HTTP/2 (ou la réponse de repli) — synchrone (#505).
99
+ *
100
+ * @returns le flux terminé.
101
+ * @throws de façon SYNCHRONE ce que lève la fin du flux.
102
+ */
69
103
  end(chunk, encoding) {
70
- return new Promise((resolve, reject) => {
71
- try {
72
- if (this.stream) {
73
- if (this.stream.destroyed || this.stream.closed) return resolve(this.stream);
74
- return resolve(this.stream.end(chunk, encoding || this.encoding));
75
- }
76
- return resolve(super.end(chunk, encoding));
77
- } catch (e) {
78
- return reject(e);
79
- }
80
- });
104
+ if (this.stream) {
105
+ if (this.stream.destroyed || this.stream.closed) return this.stream;
106
+ return this.stream.end(chunk, encoding || this.encoding);
107
+ }
108
+ return super.end(chunk, encoding);
81
109
  }
82
110
  getStatusMessage(code) {
83
111
  if (code) {
@@ -94,4 +122,4 @@ var Http2Response = class extends HttpResponse {
94
122
  }
95
123
  };
96
124
  //#endregion
97
- export { Http2Response as default };
125
+ export { Http2Response as default, mergeResponseHeaders };
@@ -31,7 +31,7 @@ function buildMetaData(target, src, override) {
31
31
  nf.scheme = src.scheme;
32
32
  nf.requestId = src.requestId;
33
33
  const r = src.resolver;
34
- nf.route = r && r.route ? {
34
+ nf.route = r?.route ? {
35
35
  name: r.route.name,
36
36
  path: r.route.path,
37
37
  variablesMap: r.getMatchedParams()
@@ -54,7 +54,8 @@ const TRUST_NONE = { isTrusted: () => false };
54
54
  function addEntry(list, raw) {
55
55
  const entry = raw.trim();
56
56
  if (!entry) return;
57
- const preset = PRESETS[entry.toLowerCase()];
57
+ const key = entry.toLowerCase();
58
+ const preset = Object.hasOwn(PRESETS, key) ? PRESETS[key] : void 0;
58
59
  if (preset) {
59
60
  for (const [net, prefix, family] of preset) list.addSubnet(net, prefix, family);
60
61
  return;
@@ -124,7 +125,7 @@ function resolveFromRight(chain, socketAddress, checker) {
124
125
  let candidate = socketAddress;
125
126
  for (let i = chain.length - 1; i >= 0; i -= 1) {
126
127
  if (!checker.isTrusted(candidate)) return candidate;
127
- const next = chain[i];
128
+ const next = chain[i] ?? null;
128
129
  if (next === null) return candidate;
129
130
  candidate = next;
130
131
  }
@@ -156,7 +157,7 @@ function extractClientIp(xff, socketAddress, checker) {
156
157
  if (!socketAddress) return null;
157
158
  if (!xff) return socketAddress;
158
159
  const s = Array.isArray(xff) ? xff.join(",") : xff;
159
- if (s.indexOf(",") === -1) {
160
+ if (!s.includes(",")) {
160
161
  const ip = s.trim();
161
162
  if (!ip) return socketAddress;
162
163
  return checker.isTrusted(socketAddress) ? ip : socketAddress;
@@ -1,7 +1,8 @@
1
1
  import Cookie from "../../cookies/cookie.js";
2
2
  import { decideSend, readBackpressureOptions } from "./wsBackpressure.js";
3
- import Ws from "ws";
3
+ import { toWsCloseReason } from "./wsCloseReason.js";
4
4
  import http from "node:http";
5
+ import Ws from "ws";
5
6
  //#region nodefony/src/context/websocket/Response.ts
6
7
  /**
7
8
  * Codes d'erreur d'écriture « le client est PARTI » : reload de page, onglet
@@ -57,7 +58,7 @@ var WebsocketResponse = class {
57
58
  this.connection = connection;
58
59
  }
59
60
  log(pci, severity, msgid, msg) {
60
- const syslog = this.context?.container?.get("syslog");
61
+ const syslog = this.context.container?.get("syslog");
61
62
  if (!msgid) msgid = "WEBSOCKET RESPONSE";
62
63
  return syslog?.log(pci, severity, msgid, msg);
63
64
  }
@@ -70,7 +71,7 @@ var WebsocketResponse = class {
70
71
  if (payload == null) throw new Error("no data");
71
72
  return new Promise((resolve, reject) => {
72
73
  if (!this.connection || this.connection.readyState !== Ws.OPEN) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
73
- const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context?.server);
74
+ const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context.server);
74
75
  const decision = decideSend(this.connection, max, policy, closeAfterDrops);
75
76
  if (decision !== "send") {
76
77
  this.logFirstDrop(this.connection, decision, max);
@@ -93,7 +94,7 @@ var WebsocketResponse = class {
93
94
  broadcast(data, type) {
94
95
  const payload = data ?? this.body;
95
96
  if (!payload) return;
96
- const wss = this.context?.server;
97
+ const wss = this.context.server;
97
98
  if (!wss) return;
98
99
  const sendData = type === "binary" && Buffer.isBuffer(payload) ? payload : payload instanceof Buffer ? payload.toString(this.encoding) : payload;
99
100
  const { max, policy, closeAfterDrops } = readBackpressureOptions(wss);
@@ -124,11 +125,17 @@ var WebsocketResponse = class {
124
125
  return this.body;
125
126
  }
126
127
  drop(reasonCode, description) {
127
- if (this.connection && this.connection.readyState === Ws.OPEN) return this.connection.close(reasonCode ?? this.statusCode, description);
128
+ if (this.connection && this.connection.readyState === Ws.OPEN) {
129
+ this.connection.close(reasonCode ?? this.statusCode, toWsCloseReason(description));
130
+ return;
131
+ }
128
132
  throw new Error("Connection already closed");
129
133
  }
130
134
  close(reasonCode, description) {
131
- if (this.connection && this.connection.readyState === Ws.OPEN) return this.connection.close(reasonCode ?? this.statusCode, description ?? "closed");
135
+ if (this.connection && this.connection.readyState === Ws.OPEN) {
136
+ this.connection.close(reasonCode ?? this.statusCode, toWsCloseReason(description ?? "closed"));
137
+ return;
138
+ }
132
139
  throw new Error("Connection already closed");
133
140
  }
134
141
  getStatus() {
@@ -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 };