@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,5 @@
1
1
  import Cookie from "../../cookies/cookie.js";
2
- import { typeOf } from "nodefony";
2
+ import { thenMaybe, typeOf } from "nodefony";
3
3
  import http from "node:http";
4
4
  import mime from "mime-types";
5
5
  //#region nodefony/src/context/http/Response.ts
@@ -14,12 +14,30 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
14
14
  307,
15
15
  308
16
16
  ]);
17
- const NO_CONTENT_LENGTH_METHODS = /* @__PURE__ */ new Set([
18
- "HEAD",
19
- "OPTIONS",
20
- "TRACE"
21
- ]);
22
17
  const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
18
+ const CONTENT_TYPE_CACHE_MAX = 64;
19
+ const contentTypeCache = /* @__PURE__ */ new Map();
20
+ /**
21
+ * Résout `type` (extension ou type MIME) comme `mime.contentType`, avec
22
+ * mémorisation bornée des résolutions positives.
23
+ *
24
+ * @param type - extension (`json`) ou type MIME (`text/html`).
25
+ * @returns la résolution, ou `null` si `mime-types` ne connaît pas le type.
26
+ */
27
+ function resolveContentType(type) {
28
+ const hit = contentTypeCache.get(type);
29
+ if (hit !== void 0) return hit;
30
+ const full = mime.contentType(type);
31
+ if (!full) return null;
32
+ const semi = full.indexOf(";");
33
+ const resolved = {
34
+ full,
35
+ bare: semi === -1 ? full : full.slice(0, semi),
36
+ charset: mime.charset(full)
37
+ };
38
+ if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
39
+ return resolved;
40
+ }
23
41
  var HttpResponse = class {
24
42
  context;
25
43
  response;
@@ -27,7 +45,9 @@ var HttpResponse = class {
27
45
  statusMessage = "";
28
46
  flushing = false;
29
47
  encoding = "utf-8";
30
- body = null;
48
+ #text = null;
49
+ #textEncoding = "utf-8";
50
+ #buffer = null;
31
51
  contentType = "application/octet-stream";
32
52
  headers = {};
33
53
  timeout;
@@ -35,7 +55,7 @@ var HttpResponse = class {
35
55
  constructor(response, context) {
36
56
  this.context = context;
37
57
  this.response = response;
38
- this.timeout = this.context?.httpKernel?.responseTimeout[this.context.type];
58
+ this.timeout = this.context.httpKernel?.responseTimeout[this.context.type];
39
59
  }
40
60
  /**
41
61
  * Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
@@ -46,6 +66,33 @@ var HttpResponse = class {
46
66
  ensureContentTypeHeader() {
47
67
  if (this.response && !this.response.hasHeader("content-type")) this.response.setHeader("content-type", this.contentType);
48
68
  }
69
+ /**
70
+ * Corps de la réponse en octets. Un corps posé en texte n'est encodé qu'à la
71
+ * première lecture de cette propriété — l'envoi, lui, écrit le texte tel quel.
72
+ */
73
+ get body() {
74
+ if (this.#text !== null) {
75
+ this.#buffer = Buffer.from(this.#text, this.#textEncoding);
76
+ this.#text = null;
77
+ }
78
+ return this.#buffer;
79
+ }
80
+ set body(value) {
81
+ this.#text = null;
82
+ this.#buffer = value;
83
+ }
84
+ /**
85
+ * Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
86
+ * les octets — vide légal si rien n'a été posé (`write(null)` jetterait
87
+ * ERR_STREAM_NULL_VALUES).
88
+ */
89
+ get payload() {
90
+ return this.#text ?? this.#buffer ?? "";
91
+ }
92
+ /** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
93
+ get payloadEncoding() {
94
+ return this.#text !== null ? this.#textEncoding : this.encoding;
95
+ }
49
96
  clean() {
50
97
  this.response = null;
51
98
  this.body = null;
@@ -68,7 +115,7 @@ var HttpResponse = class {
68
115
  }
69
116
  deleteCookie(cookie) {
70
117
  if (cookie instanceof Cookie) {
71
- if (this.cookies[cookie.name]) {
118
+ if (Object.hasOwn(this.cookies, cookie.name)) {
72
119
  delete this.cookies[cookie.name];
73
120
  return true;
74
121
  }
@@ -77,19 +124,20 @@ var HttpResponse = class {
77
124
  throw new Error("Response delCookie not valid cookies");
78
125
  }
79
126
  deleteCookieByName(name) {
80
- if (this.cookies[name]) {
127
+ if (Object.hasOwn(this.cookies, name)) {
81
128
  delete this.cookies[name];
82
129
  return true;
83
130
  }
84
131
  return false;
85
132
  }
86
133
  setCookies() {
87
- const names = Object.keys(this.cookies);
88
- if (names.length === 0) return;
89
- if (names.length === 1) return this.setCookie(this.cookies[names[0]]);
134
+ const cookies = Object.values(this.cookies);
135
+ const first = cookies.at(0);
136
+ if (first === void 0) return;
137
+ if (cookies.length === 1) return this.setCookie(first);
90
138
  const serialized = [];
91
- for (const name of names) {
92
- const s = this.cookies[name].serialize();
139
+ for (const cookie of cookies) {
140
+ const s = cookie.serialize();
93
141
  this.log(`ADD COOKIE ==> ${s}`, "DEBUG");
94
142
  serialized.push(s);
95
143
  }
@@ -105,7 +153,8 @@ var HttpResponse = class {
105
153
  if (this.flushing) {
106
154
  const obj = {};
107
155
  obj[name] = value;
108
- return this.addTrailers(obj);
156
+ this.addTrailers(obj);
157
+ return;
109
158
  }
110
159
  if (!this.response.headersSent) {
111
160
  const lower = name.toLowerCase();
@@ -125,7 +174,7 @@ var HttpResponse = class {
125
174
  */
126
175
  #mergeVary(value) {
127
176
  const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
128
- const current = this.response?.getHeader?.("vary");
177
+ const current = this.response?.getHeader("vary");
129
178
  const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
130
179
  if (all.some((t) => t === "*")) return "*";
131
180
  const seen = /* @__PURE__ */ new Set();
@@ -151,9 +200,9 @@ var HttpResponse = class {
151
200
  }
152
201
  setContentType(type, encoding) {
153
202
  if (type && encoding) {
154
- let mytype = mime.contentType(type);
155
- if (mytype) {
156
- mytype = mytype.split(";")[0];
203
+ const resolved = resolveContentType(type);
204
+ if (resolved) {
205
+ const mytype = resolved.bare;
157
206
  this.contentType = mytype;
158
207
  this.encoding = encoding;
159
208
  if (mytype === "application/json" || mytype.endsWith("+json")) return this.setHeader("Content-Type", mytype);
@@ -161,10 +210,11 @@ var HttpResponse = class {
161
210
  }
162
211
  }
163
212
  if (type && !encoding) {
164
- const mytype = mime.contentType(type);
165
- if (mytype) {
213
+ const resolved = resolveContentType(type);
214
+ if (resolved) {
215
+ const mytype = resolved.full;
166
216
  this.contentType = mytype;
167
- let charset = mime.charset(this.contentType);
217
+ const charset = resolved.charset;
168
218
  if (charset) this.encoding = charset;
169
219
  return this.setHeader("Content-Type", mytype);
170
220
  }
@@ -230,30 +280,45 @@ var HttpResponse = class {
230
280
  if (this.response) return this.statusMessage || this.response.statusMessage || http.STATUS_CODES[this.statusCode];
231
281
  return this.statusMessage || http.STATUS_CODES[this.statusCode];
232
282
  }
283
+ /**
284
+ * Pose le corps de la réponse : texte gardé tel quel, octets copiés sur leur
285
+ * seule fenêtre, tout autre valeur sérialisée en JSON.
286
+ *
287
+ * @param ele - corps à envoyer.
288
+ * @param encoding - encodage d'un corps texte (défaut : `this.encoding`).
289
+ */
233
290
  setBody(ele, encoding) {
234
- if (typeof ele === "string") this.body = Buffer.from(ele, encoding || this.encoding);
291
+ if (typeof ele === "string") this.#setText(ele, encoding || this.encoding);
235
292
  else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
236
293
  else if (ArrayBuffer.isView(ele) && ele.buffer instanceof ArrayBuffer) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
237
- else try {
238
- this.body = Buffer.from(JSON.stringify(ele));
239
- } catch (e) {
240
- this.body = Buffer.from(String(ele));
294
+ else {
295
+ let text;
296
+ try {
297
+ text = JSON.stringify(ele);
298
+ } catch {
299
+ text = String(ele);
300
+ }
301
+ this.#setText(text, "utf-8");
241
302
  }
242
- return this.body;
303
+ }
304
+ #setText(text, encoding) {
305
+ this.#buffer = null;
306
+ this.#text = text;
307
+ this.#textEncoding = encoding;
243
308
  }
244
309
  setLength(body) {
245
310
  if (this.response?.headersSent) throw new Error("Headers already sended");
246
- const actualBody = body || this.body;
247
- const isChunked = this.getHeader("Transfer-Encoding") === "chunked";
248
- if (!NO_CONTENT_LENGTH_METHODS.has(this.context?.method) && !NO_CONTENT_LENGTH_STATUS.has(this.statusCode) && !isChunked) {
249
- if (actualBody) {
250
- const length = Buffer.byteLength(actualBody);
251
- this.setHeader("Content-Length", String(length));
252
- return length;
253
- }
311
+ if (this.getHeader("Transfer-Encoding") === "chunked") {
312
+ this.response?.removeHeader("Content-Length");
254
313
  return 0;
255
314
  }
256
- if (!NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) this.setHeader("Content-Length", "0");
315
+ if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
316
+ const actualBody = body || this.#text || this.#buffer;
317
+ if (actualBody) {
318
+ const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
319
+ this.setHeader("Content-Length", String(length));
320
+ return length;
321
+ }
257
322
  return 0;
258
323
  }
259
324
  writeHead(statusCode, headers) {
@@ -264,8 +329,8 @@ var HttpResponse = class {
264
329
  if (this.statusCode > 599) this.statusCode = 500;
265
330
  }
266
331
  this.statusMessage = this.getStatusMessage();
267
- if (this.context.requestId && !this.response.headersSent) this.response.setHeader("x-request-id", this.context.requestId);
268
- if (this.context.traceparent && !this.response.headersSent) this.response.setHeader("traceparent", this.context.traceparent);
332
+ if (this.context.requestId) this.response.setHeader("x-request-id", this.context.requestId);
333
+ if (this.context.traceparent) this.response.setHeader("traceparent", this.context.traceparent);
269
334
  this.setLength();
270
335
  this.ensureContentTypeHeader();
271
336
  const std = http.STATUS_CODES[this.statusCode];
@@ -287,18 +352,29 @@ var HttpResponse = class {
287
352
  this.setHeader("Transfer-Encoding", "chunked");
288
353
  return this.send(chunk, encoding, true);
289
354
  }
290
- async send(chunk, encoding, _flush = false) {
355
+ /**
356
+ * Écrit le corps sur le flux et, pour une réponse unique, la termine.
357
+ *
358
+ * Synchrone pour une réponse unique ou une redirection : `end(corps)` est un
359
+ * appel synchrone de Node, rien n'y est à attendre (#505). Une promesse
360
+ * seulement en streaming chunké (`flush()`), réglée quand le flux accepte
361
+ * l'écriture suivante (`drain`) — c'est la contre-pression.
362
+ *
363
+ * @param chunk - le corps (posé par `setBody`) ; absent, le corps déjà posé.
364
+ * @param encoding - l'encodage d'un corps texte.
365
+ * @returns la réponse, ou sa promesse en streaming chunké.
366
+ * @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
367
+ */
368
+ send(chunk, encoding, _flush = false) {
291
369
  if (this.context.isRedirect) {
292
370
  if (!this.response?.headersSent) this.writeHead();
293
- await this.end();
294
- return this;
371
+ return thenMaybe(this.end(), () => this);
295
372
  }
296
- if (chunk) this.setBody(chunk);
373
+ if (chunk) this.setBody(chunk, encoding);
297
374
  if (!this.response) throw new Error(`Http Response not found`);
298
- if (this.body === null) this.body = Buffer.alloc(0);
299
375
  const res = this.response;
300
376
  if (!_flush && !this.flushing && !res.writableEnded) {
301
- res.end(this.body, encoding || this.encoding);
377
+ res.end(this.payload, this.payloadEncoding);
302
378
  return this;
303
379
  }
304
380
  return new Promise((resolve) => {
@@ -310,7 +386,7 @@ var HttpResponse = class {
310
386
  }
311
387
  };
312
388
  const onDrain = () => done();
313
- if (res.write(this.body, encoding || this.encoding, (error) => {
389
+ if (res.write(this.payload, this.payloadEncoding, (error) => {
314
390
  if (error) {
315
391
  this.log(error, "ERROR");
316
392
  res.removeListener("drain", onDrain);
@@ -320,17 +396,21 @@ var HttpResponse = class {
320
396
  else res.once("drain", onDrain);
321
397
  });
322
398
  }
323
- async write(chunk, encoding) {
324
- return await this.send(chunk, encoding || this.encoding);
399
+ write(chunk, encoding) {
400
+ return this.send(chunk, encoding || this.encoding);
325
401
  }
326
402
  writeContinue() {
327
403
  return this.response?.writeContinue();
328
404
  }
329
- async end(chunk, encoding) {
330
- return new Promise((resolve, reject) => {
331
- if (this.response) return resolve(this.response.end(chunk, encoding || this.encoding));
332
- return reject(/* @__PURE__ */ new Error(`response not found`));
333
- });
405
+ /**
406
+ * Termine la réponse Node — un appel synchrone, rien n'y est à attendre (#505).
407
+ *
408
+ * @returns le flux terminé (valeur de `ServerResponse.end`).
409
+ * @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
410
+ */
411
+ end(chunk, encoding) {
412
+ if (!this.response) throw new Error(`response not found`);
413
+ return this.response.end(chunk, encoding || this.encoding);
334
414
  }
335
415
  getHeader(name) {
336
416
  return this.response?.getHeader(name);
@@ -19,7 +19,7 @@ var Parser = class {
19
19
  }
20
20
  write(buffer) {
21
21
  if (this.aborted) return buffer;
22
- const max = this.request.maxBodySize ?? 0;
22
+ const max = this.request.maxBodySize;
23
23
  this.received += buffer.length;
24
24
  if (max > 0 && this.received > max) {
25
25
  this.aborted = true;
@@ -78,7 +78,7 @@ var ParserQs = class extends Parser {
78
78
  charset = "utf8";
79
79
  constructor(request) {
80
80
  super(request);
81
- this.parserOptions = this.request.queryStringOptions || {};
81
+ this.parserOptions = this.request.queryStringOptions ?? {};
82
82
  this.charset = this.request.charset;
83
83
  }
84
84
  async parse() {
@@ -151,13 +151,15 @@ const ACCEPT_ANY = [{
151
151
  * corrige aussi la négociation (match du token ENTIER, pas d'une sous-chaîne).
152
152
  */
153
153
  const acceptMatcher = (token) => !token || token === "*" ? /.*/ : new RegExp(`^${escapeRegExp(token)}$`);
154
+ /** Qualité `q` d'une entrée `Accept` : sa valeur si c'est un nombre fini, 1 sinon. */
155
+ const qualityOf = (entry) => typeof entry.q === "number" && Number.isFinite(entry.q) ? entry.q : 1;
154
156
  const acceptParser = function(acc) {
155
157
  if (!acc) return ACCEPT_ANY;
156
158
  const arr = [];
157
159
  try {
158
160
  const types = acc.split(",");
159
- for (let i = 0; i < types.length; i++) {
160
- const type = types[i].split(";");
161
+ for (const segment of types) {
162
+ const type = segment.split(";");
161
163
  const mine = type.shift();
162
164
  if (!mine) continue;
163
165
  const dec = mine.trim().split("/");
@@ -167,8 +169,8 @@ const acceptParser = function(acc) {
167
169
  type: acceptMatcher(ele1),
168
170
  subtype: acceptMatcher(ele2)
169
171
  };
170
- for (let j = 0; j < type.length; j++) {
171
- const params = type[j].split("=");
172
+ for (const param of type) {
173
+ const params = param.split("=");
172
174
  const name = params.shift();
173
175
  const value = params.shift();
174
176
  obj[name] = name === "q" ? parseFloat(value) : value;
@@ -176,10 +178,7 @@ const acceptParser = function(acc) {
176
178
  arr.push(obj);
177
179
  }
178
180
  if (arr.length === 0) return ACCEPT_ANY;
179
- return arr.sort((a, b) => {
180
- const qA = a.q || 1;
181
- return (b.q || 1) - qA;
182
- });
181
+ return arr.sort((a, b) => qualityOf(b) - qualityOf(a));
183
182
  } catch {
184
183
  return ACCEPT_ANY;
185
184
  }
@@ -79,21 +79,45 @@ function isCanonicalAuthority(host, scheme) {
79
79
  if (port.length === 0 || port.charCodeAt(0) === 48) return false;
80
80
  if (scheme === "https" && port === "443" || scheme === "http" && port === "80") return false;
81
81
  }
82
- const labels = name.split(".");
83
- for (const label of labels) if (label.length === 0) return false;
84
- const last = labels[labels.length - 1];
85
- if (isDigits(last)) {
86
- if (labels.length !== 4) return false;
87
- for (const label of labels) {
88
- if (!isDigits(label) || label.length > 3) return false;
89
- if (label.length > 1 && label.charCodeAt(0) === 48) return false;
90
- if (Number(label) > 255) return false;
82
+ let labels = 1;
83
+ let start = 0;
84
+ for (let i = 0; i < name.length; i++) if (name.charCodeAt(i) === DOT) {
85
+ if (i === start) return false;
86
+ labels++;
87
+ start = i + 1;
88
+ }
89
+ if (start === name.length) return false;
90
+ if (isDigitsFrom(name, start)) {
91
+ if (labels !== 4) return false;
92
+ return isCanonicalDottedQuad(name);
93
+ }
94
+ return true;
95
+ }
96
+ /**
97
+ * Les quatre labels (non vides, déjà comptés) forment-ils une dotted-quad
98
+ * canonique ? Chacun : chiffres seuls, 3 au plus, sans zéro de tête, ≤ 255.
99
+ */
100
+ function isCanonicalDottedQuad(name) {
101
+ let start = 0;
102
+ for (let i = 0; i <= name.length; i++) {
103
+ if (i < name.length && name.charCodeAt(i) !== DOT) continue;
104
+ const len = i - start;
105
+ if (len > 3) return false;
106
+ if (len > 1 && name.charCodeAt(start) === 48) return false;
107
+ let value = 0;
108
+ for (let k = start; k < i; k++) {
109
+ const c = name.charCodeAt(k);
110
+ if (c < 48 || c > 57) return false;
111
+ value = value * 10 + (c - 48);
91
112
  }
113
+ if (value > 255) return false;
114
+ start = i + 1;
92
115
  }
93
116
  return true;
94
117
  }
95
- function isDigits(s) {
96
- for (let i = 0; i < s.length; i++) {
118
+ /** `s` ne contient-elle que des chiffres ASCII à partir de `from` ? */
119
+ function isDigitsFrom(s, from) {
120
+ for (let i = from; i < s.length; i++) {
97
121
  const c = s.charCodeAt(i);
98
122
  if (c < 48 || c > 57) return false;
99
123
  }
@@ -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() {