@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4

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 (139) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +19 -11
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +45 -6
  9. package/dist/nodefony/config/defineModuleConfig.js +2 -3
  10. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -3
  12. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  13. package/dist/nodefony/service/audit-logger.js +22 -16
  14. package/dist/nodefony/service/certificates.js +228 -19
  15. package/dist/nodefony/service/error-renderer.js +30 -13
  16. package/dist/nodefony/service/http-kernel.js +331 -82
  17. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +336 -0
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +7 -10
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +18 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
  29. package/dist/nodefony/service/sessions/sessions-service.js +34 -20
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +51 -32
  36. package/dist/nodefony/src/context/domainMatcher.js +21 -4
  37. package/dist/nodefony/src/context/forwarded.js +3 -4
  38. package/dist/nodefony/src/context/http/HttpContext.js +98 -46
  39. package/dist/nodefony/src/context/http/Request.js +34 -38
  40. package/dist/nodefony/src/context/http/Response.js +39 -7
  41. package/dist/nodefony/src/context/http/SseStream.js +349 -0
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +22 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/responseEnded.js +26 -0
  49. package/dist/nodefony/src/context/transport.js +45 -0
  50. package/dist/nodefony/src/context/trustProxy.js +1 -2
  51. package/dist/nodefony/src/context/websocket/Response.js +42 -6
  52. package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
  53. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  54. package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
  55. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  57. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  58. package/dist/nodefony/src/cookies/cookie.js +3 -7
  59. package/dist/nodefony/src/errors/httpError.js +1 -2
  60. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  61. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  62. package/dist/nodefony/src/profiler/Profiler.js +108 -30
  63. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  64. package/dist/nodefony/src/proxy/forward.js +500 -0
  65. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  66. package/dist/nodefony/src/proxy/rules.js +188 -0
  67. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  68. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  69. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  70. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  71. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  72. package/dist/nodefony/src/servers/portBinder.js +41 -10
  73. package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
  74. package/dist/nodefony/src/session/session.js +30 -15
  75. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  76. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  79. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  80. package/dist/types/index.d.ts +12 -3
  81. package/dist/types/nodefony/config/config.d.ts +16 -0
  82. package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
  83. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  84. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  85. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  86. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  87. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  88. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  89. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  90. package/dist/types/nodefony/service/certificates.d.ts +147 -5
  91. package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
  92. package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
  93. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  94. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  95. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  96. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  97. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  98. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
  99. package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
  100. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  101. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  102. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  103. package/dist/types/nodefony/src/context/Context.d.ts +35 -6
  104. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  105. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
  106. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  107. package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
  108. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  109. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  110. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  111. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  112. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  113. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  118. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  119. package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
  120. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  121. package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
  122. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  123. package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
  124. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  125. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  126. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  127. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
  128. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  129. package/docs/cookies.md +85 -78
  130. package/docs/index.md +5 -1
  131. package/docs/observabilite.md +81 -81
  132. package/docs/rate-limit.md +83 -83
  133. package/docs/reverse-proxy.md +522 -0
  134. package/docs/servers.md +378 -230
  135. package/docs/session.md +180 -180
  136. package/docs/sse.md +287 -0
  137. package/docs/upload.md +120 -117
  138. package/package.json +10 -11
  139. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,6 +1,6 @@
1
1
  import { stripTrailingSlashes } from "nodefony";
2
2
  import { join } from "node:path";
3
- //#region nodefony/src/assets/collectAssets.ts
3
+ // nodefony/src/assets/collectAssets.ts
4
4
  /** Normalise un préfixe en segment de chemin (`/_assets/x/` → `_assets/x`). */
5
5
  const prefixToSegment = (prefix) => stripTrailingSlashes(prefix.replace(/^\/+/, ""));
6
6
  /**
@@ -30,5 +30,4 @@ function planAssetPublish(sources, outDir) {
30
30
  }
31
31
  return plan;
32
32
  }
33
- //#endregion
34
33
  export { planAssetPublish };
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- //#region nodefony/src/assets/prebuiltUi.ts
3
+ // nodefony/src/assets/prebuiltUi.ts
4
4
  /**
5
5
  * Résout le mode de livraison de l'UI embarquée d'un module.
6
6
  *
@@ -121,5 +121,4 @@ var PrebuiltUi = class {
121
121
  return this.template.replaceAll("<script", `<script nonce="${nonce}"`);
122
122
  }
123
123
  };
124
- //#endregion
125
124
  export { PrebuiltUi, resolveUiDelivery };
@@ -1,18 +1,13 @@
1
1
  import { buildMetaData } from "./metaData.js";
2
+ import { protocolNameOf } from "./transport.js";
2
3
  import Cookie, { cookiesParser } from "../cookies/cookie.js";
3
4
  import { RequestContext, Service, logColor } from "nodefony";
4
5
  import { randomFillSync, randomUUID } from "node:crypto";
5
6
  import { performance } from "node:perf_hooks";
6
7
  import { AsyncResource } from "node:async_hooks";
7
- //#region nodefony/src/context/Context.ts
8
+ // nodefony/src/context/Context.ts
8
9
  const colorLogEvent = () => logColor.cyanBgBlack("EVENT CONTEXT");
9
10
  const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
10
- const EVENT_SEVERITY = {
11
- onRequest: "INFO",
12
- onSend: "INFO",
13
- onConnect: "INFO",
14
- onClose: "INFO"
15
- };
16
11
  let lifecycleEventLogging = null;
17
12
  /**
18
13
  * Bascule runtime du gate des logs d'events lifecycle. Réservé HORS hot path :
@@ -47,7 +42,6 @@ var Context = class extends Service {
47
42
  secure = false;
48
43
  security = null;
49
44
  cleaned = false;
50
- isControlledAccess = false;
51
45
  validDomain = false;
52
46
  finished = false;
53
47
  pushAllowed = false;
@@ -55,7 +49,15 @@ var Context = class extends Service {
55
49
  requested = false;
56
50
  sessionStarting = false;
57
51
  domain = "";
52
+ /** Famille traitée (`network.protocol.name` d'OpenTelemetry) — union ouverte. */
58
53
  type;
54
+ /** Serveur qui a reçu la requête — choisit la configuration par serveur. */
55
+ serverType;
56
+ /**
57
+ * Protocole du saut local (registre IANA ALPN) — union ouverte. Une connexion
58
+ * WebSocket naît d'un upgrade HTTP/1.1 ; `HttpContext` le constate sur la requête.
59
+ */
60
+ alpnProtocol = "http/1.1";
59
61
  server;
60
62
  httpKernel;
61
63
  request = null;
@@ -76,6 +78,14 @@ var Context = class extends Service {
76
78
  session = null;
77
79
  cookieSession = null;
78
80
  user = null;
81
+ /**
82
+ * Identifiant de l'appelant AUTHENTIFIÉ, posé par le pare-feu ; `null` hors
83
+ * zone ou pour un anonyme. Lisible au démontage, hors de la bulle ALS : c'est
84
+ * ce que lisent le journal d'audit et le profileur. Distinct de `user`, qui
85
+ * lie la session à son principal de SESSION — y écrire l'identité d'un jeton
86
+ * rattacherait la session d'un autre compte.
87
+ */
88
+ userIdentifier = null;
79
89
  waitAsync = false;
80
90
  isJson = false;
81
91
  _isHtml = null;
@@ -133,6 +143,7 @@ var Context = class extends Service {
133
143
  _phaseIndex = null;
134
144
  _afterResponseFns = null;
135
145
  _afterResponseFired = false;
146
+ requestStore = null;
136
147
  _abortController = null;
137
148
  metaData = {
138
149
  nodefony: {},
@@ -140,13 +151,14 @@ var Context = class extends Service {
140
151
  };
141
152
  scheme;
142
153
  webSocketState = null;
143
- constructor(container, type) {
144
- super(type, container);
154
+ constructor(container, serverType) {
155
+ super(serverType, container);
145
156
  if (PERF_PROBE_SUB) {
146
157
  const p = globalThis.__nfPerfProbe;
147
158
  if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
148
159
  }
149
- this.type = type;
160
+ this.serverType = serverType;
161
+ this.type = protocolNameOf(serverType);
150
162
  this.set("context", this);
151
163
  this.httpKernel = this.get("HttpKernel");
152
164
  this.sessionService = this.get("sessions");
@@ -159,35 +171,24 @@ var Context = class extends Service {
159
171
  const verbose = (this.kernel?.options)?.timing?.verbose;
160
172
  this._timingVerbose = verbose === true;
161
173
  }
162
- this.scheme = "https";
163
- switch (this.type) {
174
+ this.scheme = serverType;
175
+ switch (serverType) {
164
176
  case "http": {
165
- this.scheme = "http";
166
177
  const server = this.get("server-http");
167
178
  this.server = server?.server;
168
179
  break;
169
180
  }
170
- case "http2":
171
181
  case "https": {
172
- this.scheme = "https";
173
182
  const server = this.get("server-https");
174
183
  this.server = server?.server;
175
184
  break;
176
185
  }
177
- case "http3": {
178
- this.scheme = "https";
179
- const server = this.get("server-http3");
180
- this.server = server?.server ?? null;
181
- break;
182
- }
183
- case "websocket": {
184
- this.scheme = "ws";
186
+ case "ws": {
185
187
  const server = this.get("server-websocket");
186
188
  this.server = server?.server;
187
189
  break;
188
190
  }
189
- case "websocket-secure": {
190
- this.scheme = "wss";
191
+ case "wss": {
191
192
  const server = this.get("server-websocket-secure");
192
193
  this.server = server?.server;
193
194
  break;
@@ -296,24 +297,43 @@ var Context = class extends Service {
296
297
  if (this._abortController && !this._abortController.signal.aborted) this._abortController.abort(new Error(reason ?? "Context aborted"));
297
298
  }
298
299
  log(pci, severity, msgid, msg) {
299
- if (!msgid) msgid = this.type;
300
+ if (!msgid) msgid = this.serverType;
300
301
  if (this.requestId && RequestContext.getRequestId() === void 0) return RequestContext.run({ requestId: this.requestId }, () => super.log(pci, severity, msgid, msg));
301
302
  return super.log(pci, severity, msgid, msg);
302
303
  }
304
+ /**
305
+ * Confie au contexte le magasin `RequestContext` de son unité de travail
306
+ * (requête HTTP, connexion WebSocket), pour qu'il le VIDE à son nettoyage.
307
+ *
308
+ * Un minuteur ou une promesse armé pendant l'unité de travail capture ce
309
+ * magasin et le garde au-delà de la réponse — avec le contexte, l'identité
310
+ * et le jeton du pare-feu. Le vidage ({@link RequestContext.release}) n'y
311
+ * laisse que la corrélation et le scope refermé.
312
+ *
313
+ * @param store - le magasin passé à `RequestContext.run`
314
+ */
315
+ holdRequestStore(store) {
316
+ this.requestStore = store;
317
+ }
303
318
  clean() {
304
319
  this.cleaned = true;
305
320
  this.httpKernel = null;
321
+ const store = this.requestStore;
322
+ if (store) {
323
+ RequestContext.release(store);
324
+ this.requestStore = null;
325
+ }
306
326
  return super.clean();
307
327
  }
308
328
  /**
309
329
  * Log d'un event du cycle de vie — gate boot-time (V2.1) : en production le
310
- * Pdu n'est PAS construit (return avant toute allocation). Hors prod, jalon
311
- * notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
330
+ * Pdu n'est PAS construit (return avant toute allocation). Hors prod : DEBUG,
331
+ * pour le Suivi de requête de Studio — la ligne `req` porte le bilan.
312
332
  */
313
333
  logEvent(event) {
314
334
  lifecycleEventLogging ??= this.kernel?.environment !== "production";
315
335
  if (!lifecycleEventLogging) return;
316
- this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
336
+ this.log(`${colorLogEvent()} ${event}`, "DEBUG");
317
337
  }
318
338
  fire(event, ...args) {
319
339
  this.logEvent(event);
@@ -367,7 +387,7 @@ var Context = class extends Service {
367
387
  } else line += `${p.name}=…`;
368
388
  i++;
369
389
  }
370
- this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
390
+ this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.serverType} TIMING`);
371
391
  }
372
392
  addRequestCookie(cookie) {
373
393
  if (cookie instanceof Cookie) return this.cookies[cookie.name] = cookie;
@@ -426,5 +446,4 @@ var Context = class extends Service {
426
446
  setContextJson(_encoding = "utf-8") {}
427
447
  setContextHtml(_encoding = "utf-8") {}
428
448
  };
429
- //#endregion
430
449
  export { Context as default, nextCspNonce, setLifecycleEventLogging };
@@ -1,5 +1,5 @@
1
1
  import { escapeRegExp } from "nodefony";
2
- //#region nodefony/src/context/domainMatcher.ts
2
+ // nodefony/src/context/domainMatcher.ts
3
3
  /**
4
4
  * Matching de domaine (`Host`) — fonctions PURES, politique UNIQUE partagée par
5
5
  * les deux étages de Nodefony :
@@ -15,12 +15,30 @@ import { escapeRegExp } from "nodefony";
15
15
  * Politique de compilation (sûre, ancrée, ReDoS-free) :
16
16
  * - string sans `*` → match EXACT ancré (`^...$`, le `.` est littéral).
17
17
  * - string avec `*` → wildcard UN label (`*.example.com` → `^[^.]+\.example\.com$`,
18
- * RFC 6125 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
18
+ * RFC 9525 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
19
19
  * - `RegExp` → reprise telle quelle (l'auteur assume l'ancrage).
20
20
  *
21
21
  * Compilation faite UNE fois (boot / enregistrement de route) ; le test par
22
22
  * requête est un simple `RegExp.test` sur une liste pré-compilée (zéro alloc hot-path).
23
23
  */
24
+ /**
25
+ * Nom d'hôte d'une autorité brute (`host[:port]`), sans port — la valeur que
26
+ * rendrait `URL.hostname` pour une autorité déjà canonique, sans en payer le
27
+ * parse (`indexOf` + `slice`, #508). Un littéral IPv6 garde ses crochets
28
+ * (`[::1]:5151` → `[::1]`) : couper au premier `:` le réduirait à `[`.
29
+ *
30
+ * @param host - autorité brute (`Host`), éventuellement absente
31
+ * @returns le nom d'hôte, `""` si l'autorité est absente
32
+ */
33
+ function hostnameOf(host) {
34
+ if (!host) return "";
35
+ if (host.charCodeAt(0) === 91) {
36
+ const end = host.indexOf("]");
37
+ return end === -1 ? host : host.slice(0, end + 1);
38
+ }
39
+ const colon = host.indexOf(":");
40
+ return colon === -1 ? host : host.slice(0, colon);
41
+ }
24
42
  const DEV_LOOPBACK = [
25
43
  "localhost",
26
44
  "127.0.0.1",
@@ -117,5 +135,4 @@ function isDomainAllowed(regAlias, domain) {
117
135
  for (const reg of regAlias) if (reg.test(domain)) return true;
118
136
  return false;
119
137
  }
120
- //#endregion
121
- export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed, resolveTrustedHostNames };
138
+ export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, hostnameOf, isDomainAllowed, resolveTrustedHostNames };
@@ -1,9 +1,9 @@
1
1
  import { extractClientIp, resolveFromRight } from "./trustProxy.js";
2
2
  import { isIPv4 } from "node:net";
3
- //#region nodefony/src/context/forwarded.ts
3
+ // nodefony/src/context/forwarded.ts
4
4
  /**
5
5
  * Découpe une chaîne sur `sep` au **niveau supérieur uniquement**, en respectant
6
- * les `quoted-string` RFC 7230 (`value = token / quoted-string`, §4) : un
6
+ * les `quoted-string` RFC 9110 §5.6.4 (`value = token / quoted-string`, RFC 7239 §4) : un
7
7
  * séparateur à l'intérieur de guillemets (ex. un host quoté, un IPv6) n'est PAS
8
8
  * un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
9
9
  */
@@ -40,7 +40,7 @@ function splitTopLevel(input, sep) {
40
40
  out.push(buf);
41
41
  return out;
42
42
  }
43
- /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
43
+ /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 9110 §5.6.4). */
44
44
  function unquote(value) {
45
45
  const v = value.trim();
46
46
  if (v.length >= 2 && v.startsWith("\"") && v.endsWith("\"")) return v.slice(1, -1).replace(/\\(.)/gu, "$1");
@@ -181,5 +181,4 @@ function resolveForwarded(headers, socketAddress, checker) {
181
181
  function hasForwardingHeaders(headers) {
182
182
  return Boolean(headers.forwarded || headers["x-forwarded-for"] || headers["x-forwarded-proto"] || headers["x-forwarded-scheme"] || headers["x-forwarded-host"]);
183
183
  }
184
- //#endregion
185
184
  export { forwardedNodeIp, hasForwardingHeaders, parseForwarded, resolveForwarded };
@@ -1,4 +1,6 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
+ import { responseEnded } from "../responseEnded.js";
3
+ import { alpnProtocolOf } from "../transport.js";
2
4
  import Cookie from "../../cookies/cookie.js";
3
5
  import Context from "../Context.js";
4
6
  import { sanitizeRequestId } from "../requestId.js";
@@ -7,25 +9,63 @@ import HttpResponse from "./Response.js";
7
9
  import Http2Request from "../http2/Request.js";
8
10
  import Http2Response from "../http2/Response.js";
9
11
  import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
10
- import { extend, finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
12
+ import { openSseStream } from "./SseStream.js";
13
+ import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
11
14
  import url, { URL } from "node:url";
12
- //#region nodefony/src/context/http/HttpContext.ts
15
+ // nodefony/src/context/http/HttpContext.ts
16
+ /**
17
+ * Même URL (chemin, requête, hôte) sous un autre schéma et un autre port.
18
+ * `port` absent → port par défaut du schéma (omis de l'URL).
19
+ *
20
+ * @param from - URL de la requête.
21
+ * @param scheme - schéma cible.
22
+ * @param port - port du serveur cible, s'il est connu.
23
+ * @returns l'URL absolue de redirection.
24
+ */
25
+ function switchUrlScheme(from, scheme, port) {
26
+ const target = new URL(from.href);
27
+ target.protocol = `${scheme}:`;
28
+ target.port = port === void 0 ? "" : String(port);
29
+ return target.href;
30
+ }
13
31
  const socketActiveContext = /* @__PURE__ */ new WeakMap();
14
32
  const socketTimeoutArmed = /* @__PURE__ */ new WeakSet();
33
+ /**
34
+ * Gestionnaire `timeout` PERMANENT d'une socket h1, fabriqué hors de toute
35
+ * méthode : une fermeture créée dans `HttpContext.setTimeout()` partageait
36
+ * l'environnement d'une autre qui lit `this`, et retenait ainsi le contexte de
37
+ * la PREMIÈRE requête de la connexion tant que celle-ci vivait (#561).
38
+ *
39
+ * @param socket - la socket h1 dont on route le délai d'inactivité
40
+ * @returns l'écouteur à attacher une fois par socket
41
+ */
42
+ function idleSocketTimeoutHandler(socket) {
43
+ return () => {
44
+ const ctx = socketActiveContext.get(socket);
45
+ if (ctx !== void 0 && !ctx.cleaned && !ctx.response.response?.writableEnded) {
46
+ ctx._onTimeout();
47
+ return;
48
+ }
49
+ socket.destroy();
50
+ };
51
+ }
15
52
  const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
16
53
  var HttpContext = class extends Context {
17
54
  proxy = null;
55
+ /** Socket h1 inscrite dans `socketActiveContext` — retirée par `clean()`. */
56
+ activeSocket = null;
18
57
  isRedirect = false;
19
58
  sended = false;
20
59
  request;
21
60
  response;
22
61
  uploadService;
23
- constructor(container, request, response, type) {
24
- super(container, type);
62
+ constructor(container, request, response, serverType) {
63
+ super(container, serverType);
64
+ this.alpnProtocol = alpnProtocolOf(request);
25
65
  this.uploadService = this.get("upload");
26
66
  const perfSub = PERF_PROBE_SUB ? globalThis.__nfPerfProbe : void 0;
27
67
  if (perfSub && perfSub.t0 !== 0n) perfSub.uploadNs += Number(process.hrtime.bigint() - perfSub.t0);
28
- if (this.type === "http2") {
68
+ if (this.alpnProtocol === "h2" || this.alpnProtocol === "h2c") {
29
69
  this.request = new Http2Request(request, this);
30
70
  this.response = new Http2Response(response, this);
31
71
  } else {
@@ -86,7 +126,7 @@ var HttpContext = class extends Context {
86
126
  _onTimeout() {
87
127
  if (this.listenerCount("onTimeout")) this.fire("onTimeout", this);
88
128
  this._abortIfPending("Request timeout");
89
- let error = null;
129
+ let error;
90
130
  if (this.response.stream) error = new HttpError("Gateway Timeout", 504, this);
91
131
  else error = new HttpError("Request Timeout", 408, this);
92
132
  this.httpKernel?.onError(error, this);
@@ -126,24 +166,39 @@ var HttpContext = class extends Context {
126
166
  if (!res) return;
127
167
  if (this.response.stream) {
128
168
  res.setTimeout(this.response.timeout, () => {
129
- if (!this.response.response?.writableEnded) this._onTimeout();
169
+ if (!responseEnded(res)) this._onTimeout();
130
170
  });
131
171
  return;
132
172
  }
133
173
  const socket = res.socket;
134
174
  if (!socket) return;
135
175
  socketActiveContext.set(socket, this);
176
+ this.activeSocket = socket;
136
177
  const ms = this.response.timeout;
137
178
  if (socket.timeout !== ms) socket.setTimeout(ms);
138
179
  if (!socketTimeoutArmed.has(socket)) {
139
180
  socketTimeoutArmed.add(socket);
140
- socket.on("timeout", () => {
141
- const ctx = socketActiveContext.get(socket);
142
- if (ctx && !ctx.response.response?.writableEnded) ctx._onTimeout();
143
- });
181
+ socket.on("timeout", idleSocketTimeoutHandler(socket));
144
182
  }
145
183
  }
146
184
  /**
185
+ * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
186
+ * persistante survit à la requête, et l'entrée de `socketActiveContext`
187
+ * retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
188
+ * pointe encore sur CE contexte est retirée — une requête suivante sur la
189
+ * même socket a pu la réécrire. Le gestionnaire `timeout` de la socket ne
190
+ * trouve alors rien et prend le chemin « socket inactive → `destroy()` »,
191
+ * celui qu'il prenait déjà pour un contexte nettoyé.
192
+ */
193
+ clean() {
194
+ const socket = this.activeSocket;
195
+ if (socket) {
196
+ if (socketActiveContext.get(socket) === this) socketActiveContext.delete(socket);
197
+ this.activeSocket = null;
198
+ }
199
+ super.clean();
200
+ }
201
+ /**
147
202
  * Sérialise la donnée selon son type (JSON, HTML, texte), pose statut et
148
203
  * en-têtes, puis l'envoie ({@link send}). Synchrone quand l'envoi l'est (#505).
149
204
  *
@@ -229,7 +284,7 @@ var HttpContext = class extends Context {
229
284
  }
230
285
  /** Hook `onSend` (attendu seulement s'il a un écouteur), puis en-têtes et corps. */
231
286
  emitAndWrite(body, encoding) {
232
- if (body) this.response.setBody(body);
287
+ if (body || body === "") this.response.setBody(body);
233
288
  if (this.listenerCount("onSend") > 0) return this.fireAsync("onSend", this.response, this).then(() => this.writeHeadAndBody(body, encoding));
234
289
  return this.writeHeadAndBody(body, encoding);
235
290
  }
@@ -265,6 +320,18 @@ var HttpContext = class extends Context {
265
320
  return this.close();
266
321
  });
267
322
  }
323
+ /**
324
+ * Répond par un flux d'événements serveur (`text/event-stream`).
325
+ *
326
+ * La requête ne se termine qu'à `close()` du flux, ou au départ du client.
327
+ *
328
+ * @param options - battement de cœur, délai `retry:` initial.
329
+ * @returns le flux, ou sa promesse quand une session est à sauver d'abord.
330
+ * @throws HttpError 500 quand la réponse est déjà partie.
331
+ */
332
+ openSse(options) {
333
+ return openSseStream(this, options);
334
+ }
268
335
  flush(chunk, encoding) {
269
336
  return this.response.flush(chunk, encoding);
270
337
  }
@@ -288,41 +355,27 @@ var HttpContext = class extends Context {
288
355
  if (typeof Url === "object") return this.response.redirect(url.format(Url), status, headers);
289
356
  return this.response.redirect(Url, status, headers);
290
357
  }
358
+ /**
359
+ * Redirige vers la même URL en HTTPS, sur le port du serveur HTTPS lié —
360
+ * ou le port par défaut derrière un proxy, dont le port public est inconnu.
361
+ *
362
+ * @param status - code de redirection (défaut de `redirect`).
363
+ * @param headers - en-têtes ajoutés à la réponse.
364
+ */
291
365
  redirectHttps(status, headers) {
292
- if (this.session) {}
293
- let urlExtend = null;
294
- if (this.proxy) urlExtend = {
295
- protocol: "https",
296
- href: "",
297
- host: ""
298
- };
299
- else urlExtend = {
300
- protocol: "https",
301
- port: this.httpKernel?.httpsPort || 443,
302
- href: "",
303
- host: ""
304
- };
305
- const urlChange = extend({}, this.request.url, urlExtend);
306
- const newUrl = url.format(urlChange);
307
- return this.redirect(newUrl, status, headers);
366
+ const port = this.proxy ? void 0 : this.httpKernel?.httpsPort;
367
+ return this.redirect(switchUrlScheme(this.request.url, "https", port), status, headers);
308
368
  }
369
+ /**
370
+ * Redirige vers la même URL en HTTP, sur le port du serveur HTTP lié — ou
371
+ * le port par défaut derrière un proxy.
372
+ *
373
+ * @param status - code de redirection (défaut de `redirect`).
374
+ * @param headers - en-têtes ajoutés à la réponse.
375
+ */
309
376
  redirectHttp(status, headers) {
310
- if (this.session) {}
311
- let urlExtend = null;
312
- if (this.proxy) urlExtend = {
313
- protocol: "http",
314
- href: "",
315
- host: ""
316
- };
317
- else urlExtend = {
318
- protocol: "http",
319
- port: this.httpKernel?.httpPort || 80,
320
- href: "",
321
- host: ""
322
- };
323
- const urlChange = extend({}, this.request.url, urlExtend);
324
- const newUrl = url.format(urlChange);
325
- return this.redirect(newUrl, status, headers);
377
+ const port = this.proxy ? void 0 : this.httpKernel?.httpPort;
378
+ return this.redirect(switchUrlScheme(this.request.url, "http", port), status, headers);
326
379
  }
327
380
  getHostName() {
328
381
  return this.request.getHostName();
@@ -363,5 +416,4 @@ var HttpContext = class extends Context {
363
416
  this.response.setContentType("html", encoding);
364
417
  }
365
418
  };
366
- //#endregion
367
- export { HttpContext as default };
419
+ export { HttpContext as default, switchUrlScheme };