@nodefony/http 10.0.0-alpha.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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,415 @@
1
+ import { buildMetaData } from "./metaData.js";
2
+ import Cookie, { cookiesParser } from "../cookies/cookie.js";
3
+ import { RequestContext, Service, logColor } from "nodefony";
4
+ import { randomFillSync, randomUUID } from "node:crypto";
5
+ import { performance } from "node:perf_hooks";
6
+ import { AsyncResource } from "node:async_hooks";
7
+ //#region nodefony/src/context/Context.ts
8
+ const colorLogEvent = () => logColor.cyanBgBlack("EVENT CONTEXT");
9
+ 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
+ let lifecycleEventLogging = null;
17
+ /**
18
+ * Bascule runtime du gate des logs d'events lifecycle. Réservé HORS hot path :
19
+ * tests et futur « Audit à chaud » (fenêtre bornée prod→verbeux, auto-revert
20
+ * serveur) — le canal Studio `nodefony:syslog` retrouve alors les events sans
21
+ * redémarrage. `null` → re-résolution depuis l'env au prochain event.
22
+ */
23
+ function setLifecycleEventLogging(value) {
24
+ lifecycleEventLogging = value;
25
+ }
26
+ const EMPTY_PHASES = Object.freeze([]);
27
+ const NONCE_POOL_SIZE = 4096;
28
+ const NONCE_BYTES = 16;
29
+ const noncePool = Buffer.allocUnsafe(NONCE_POOL_SIZE);
30
+ let noncePoolOffset = NONCE_POOL_SIZE;
31
+ /**
32
+ * Rend le prochain nonce CSP (16 octets CSPRNG, base64) depuis le pool amorti.
33
+ *
34
+ * Exporté UNIQUEMENT pour le banc d'épuisement du pool (unicité à travers
35
+ * plusieurs refills) — le code produit passe par `context.cspNonce`.
36
+ */
37
+ function nextCspNonce() {
38
+ if (noncePoolOffset + NONCE_BYTES > NONCE_POOL_SIZE) {
39
+ randomFillSync(noncePool);
40
+ noncePoolOffset = 0;
41
+ }
42
+ const nonce = noncePool.toString("base64", noncePoolOffset, noncePoolOffset + NONCE_BYTES);
43
+ noncePoolOffset += NONCE_BYTES;
44
+ return nonce;
45
+ }
46
+ var Context = class extends Service {
47
+ secure = false;
48
+ security = null;
49
+ cleaned = false;
50
+ isControlledAccess = false;
51
+ validDomain = false;
52
+ finished = false;
53
+ pushAllowed = false;
54
+ requestEnded = false;
55
+ requested = false;
56
+ sessionStarting = false;
57
+ domain = "";
58
+ type;
59
+ server;
60
+ httpKernel;
61
+ request = null;
62
+ response = null;
63
+ url = "";
64
+ method = null;
65
+ remoteAddress = null;
66
+ _originUrl = null;
67
+ get originUrl() {
68
+ return this._originUrl;
69
+ }
70
+ set originUrl(value) {
71
+ this._originUrl = value;
72
+ }
73
+ cookies = {};
74
+ error = null;
75
+ sessionService;
76
+ session = null;
77
+ cookieSession = null;
78
+ user = null;
79
+ waitAsync = false;
80
+ isJson = false;
81
+ _isHtml = null;
82
+ get isHtml() {
83
+ return this._isHtml ?? (this._isHtml = this.resolveIsHtml());
84
+ }
85
+ set isHtml(value) {
86
+ this._isHtml = value;
87
+ }
88
+ /**
89
+ * Défaut paresseux d'{@link isHtml} — overridé par HttpContext (négociation
90
+ * `Accept`). Base et WS : jamais HTML.
91
+ */
92
+ resolveIsHtml() {
93
+ return false;
94
+ }
95
+ crossDomain = false;
96
+ router = this.get("router");
97
+ resolver = null;
98
+ /**
99
+ * Intent de session de la route courante (posé par le Resolver depuis
100
+ * `@UseSession` / paramètre `@Session`). Pilote le point d'activation unique
101
+ * (HTTP + WS). `null` = aucune session sauf cookie existant (reprise L1).
102
+ */
103
+ sessionIntent = null;
104
+ /**
105
+ * Directives CSP additionnelles de la route courante (`@Csp`), posées par le
106
+ * Resolver au match. `null` = la route n'en déclare pas (cas courant) → le
107
+ * firewall compose le CSP standard (0 surcoût). Lu par `applySecurityHeaders`
108
+ * APRÈS le resolve. Forme `directive → sources` (compatible `CspFragment`).
109
+ */
110
+ cspDirectives = null;
111
+ /**
112
+ * CSRF per-route (`@CsrfProtect`/`@CsrfExempt`), posés par le Resolver au match,
113
+ * lus par `Firewall.enforceCsrf` (post-resolve). `csrfProtect` exige le
114
+ * synchronizer token sur la mutation ; `csrfExempt` sort la route de la défense
115
+ * CSRF (auth conservée). `csrfToken` = token émis par le firewall (cookie lisible
116
+ * + ce champ) sur une requête sûre vers une route protégée, à surfacer côté vue.
117
+ */
118
+ csrfProtect = false;
119
+ csrfExempt = false;
120
+ csrfToken = null;
121
+ requestId = randomUUID();
122
+ #cspNonce = null;
123
+ get cspNonce() {
124
+ return this.#cspNonce ??= nextCspNonce();
125
+ }
126
+ traceparent = null;
127
+ profilerQueries = null;
128
+ profiling = false;
129
+ securityTrace = null;
130
+ _timingEnabled = false;
131
+ _timingVerbose = false;
132
+ phases = EMPTY_PHASES;
133
+ _phaseIndex = null;
134
+ _afterResponseFns = null;
135
+ _afterResponseFired = false;
136
+ _abortController = null;
137
+ metaData = {
138
+ nodefony: {},
139
+ result: null
140
+ };
141
+ scheme;
142
+ webSocketState = null;
143
+ constructor(container, type) {
144
+ super(`${type}`, container);
145
+ if (PERF_PROBE_SUB) {
146
+ const p = globalThis.__nfPerfProbe;
147
+ if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
148
+ }
149
+ this.type = type;
150
+ this.set("context", this);
151
+ this.httpKernel = this.get("HttpKernel");
152
+ this.sessionService = this.get("sessions");
153
+ this.setMetaData();
154
+ const explicit = (this.kernel?.options)?.timing?.enabled;
155
+ if (typeof explicit === "boolean") this._timingEnabled = explicit;
156
+ else this._timingEnabled = this.kernel?.environment !== "production";
157
+ if (this._timingEnabled) {
158
+ this.phases = [];
159
+ const verbose = (this.kernel?.options)?.timing?.verbose;
160
+ this._timingVerbose = verbose === true;
161
+ }
162
+ this.scheme = "https";
163
+ switch (this.type) {
164
+ case "http": {
165
+ this.scheme = "http";
166
+ const server = this.get("server-http");
167
+ this.server = server?.server;
168
+ break;
169
+ }
170
+ case "http2":
171
+ case "https": {
172
+ this.scheme = "https";
173
+ const server = this.get("server-https");
174
+ this.server = server?.server;
175
+ break;
176
+ }
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";
185
+ const server = this.get("server-websocket");
186
+ this.server = server?.server;
187
+ break;
188
+ }
189
+ case "websocket-secure": {
190
+ this.scheme = "wss";
191
+ const server = this.get("server-websocket-secure");
192
+ this.server = server?.server;
193
+ break;
194
+ }
195
+ }
196
+ if (PERF_PROBE_SUB) {
197
+ const p = globalThis.__nfPerfProbe;
198
+ if (p && p.t0 !== 0n) p.ctxBaseNs += Number(process.hrtime.bigint() - p.t0);
199
+ }
200
+ }
201
+ /**
202
+ * Assemble l'enveloppe `metaData` de la requête (`nodefony.*` + `route`
203
+ * snapshot per-requête + overrides appelant). Délègue au builder monomorphe
204
+ * pur {@link buildMetaData} — pas de `extend(true, …)` (deep-clone + dispatch
205
+ * polymorphe inutiles : `this.metaData` est per-requête, jamais partagé). `this`
206
+ * satisfait structurellement {@link IMetaDataSource} (kernel/request/scheme/
207
+ * requestId/resolver) → zéro alloc d'objet intermédiaire.
208
+ */
209
+ setMetaData(obj = {}) {
210
+ return buildMetaData(this.metaData, this, obj);
211
+ }
212
+ setScheme() {
213
+ return "https";
214
+ }
215
+ /**
216
+ * Le timing est-il actif sur CETTE requête ? (dev/test par défaut, opt-out en
217
+ * production — cf constructeur.)
218
+ *
219
+ * Exposé pour que les appelants qui devraient **payer** pour mesurer (un
220
+ * `try/finally` autour d'une chaîne de promesses = une microtask de plus par
221
+ * requête) puissent garder leur chemin nominal INTACT quand la mesure est
222
+ * éteinte : `if (!ctx.timingEnabled) return fastPath()`. Les autres n'ont rien
223
+ * à tester — {@link phaseStart}/{@link phaseEnd} sont déjà des no-ops.
224
+ */
225
+ get timingEnabled() {
226
+ return this._timingEnabled;
227
+ }
228
+ phaseStart(name) {
229
+ if (!this._timingEnabled) return;
230
+ if (this._phaseIndex === null) this._phaseIndex = /* @__PURE__ */ new Map();
231
+ const idx = this.phases.length;
232
+ this.phases.push({
233
+ name,
234
+ startMs: performance.now()
235
+ });
236
+ this._phaseIndex.set(name, idx);
237
+ }
238
+ phaseEnd(name) {
239
+ if (!this._timingEnabled || this._phaseIndex === null) return;
240
+ const idx = this._phaseIndex.get(name);
241
+ if (idx === void 0) return;
242
+ const p = this.phases[idx];
243
+ if (p.endMs !== void 0) return;
244
+ p.endMs = performance.now();
245
+ p.durationMs = p.endMs - p.startMs;
246
+ }
247
+ onAfterResponse(fn) {
248
+ const boundFn = AsyncResource.bind(fn);
249
+ if (this._afterResponseFired) {
250
+ Promise.resolve().then(() => boundFn(this)).catch((e) => this.log(e, "ERROR", "onAfterResponse(late)"));
251
+ return;
252
+ }
253
+ if (this._afterResponseFns === null) this._afterResponseFns = [];
254
+ this._afterResponseFns.push(boundFn);
255
+ }
256
+ async _runAfterResponse() {
257
+ if (this._afterResponseFired) return;
258
+ this._afterResponseFired = true;
259
+ const fns = this._afterResponseFns;
260
+ if (fns === null || fns.length === 0) return;
261
+ this._afterResponseFns = null;
262
+ for (const fn of fns) try {
263
+ await fn(this);
264
+ } catch (e) {
265
+ this.log(e, "ERROR", "onAfterResponse");
266
+ }
267
+ }
268
+ get signal() {
269
+ if (this._abortController === null) {
270
+ this._abortController = new AbortController();
271
+ const req = this.request;
272
+ if (req && typeof req.once === "function") {
273
+ const onAborted = () => {
274
+ if (this._abortController && !this._abortController.signal.aborted && req.complete === false) this._abortController.abort(/* @__PURE__ */ new Error("Request aborted by client"));
275
+ };
276
+ if (req.destroyed && req.complete === false) onAborted();
277
+ else req.once("close", onAborted);
278
+ }
279
+ }
280
+ return this._abortController.signal;
281
+ }
282
+ _abortIfPending(reason) {
283
+ if (this._abortController && !this._abortController.signal.aborted) this._abortController.abort(new Error(reason ?? "Context aborted"));
284
+ }
285
+ log(pci, severity, msgid, msg) {
286
+ if (!msgid) msgid = this.type;
287
+ if (this.requestId && RequestContext.getRequestId() === void 0) return RequestContext.run({ requestId: this.requestId }, () => super.log(pci, severity, msgid, msg));
288
+ return super.log(pci, severity, msgid, msg);
289
+ }
290
+ clean() {
291
+ this.cleaned = true;
292
+ this.httpKernel = null;
293
+ return super.clean();
294
+ }
295
+ /**
296
+ * Log d'un event du cycle de vie — gate boot-time (V2.1) : en production le
297
+ * Pdu n'est PAS construit (return avant toute allocation). Hors prod, jalon
298
+ * notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
299
+ */
300
+ logEvent(event) {
301
+ if (lifecycleEventLogging === null) lifecycleEventLogging = this.kernel?.environment !== "production";
302
+ if (!lifecycleEventLogging) return;
303
+ this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
304
+ }
305
+ fire(event, ...args) {
306
+ this.logEvent(event);
307
+ return super.fire(event, ...args);
308
+ }
309
+ emit(event, ...args) {
310
+ this.logEvent(event);
311
+ return super.emit(event, ...args);
312
+ }
313
+ emitAsync(event, ...args) {
314
+ this.logEvent(event);
315
+ return super.emitAsync(event, ...args);
316
+ }
317
+ fireAsync(event, ...args) {
318
+ this.logEvent(event);
319
+ return super.emitAsync(event, ...args);
320
+ }
321
+ logRequest(httpError) {
322
+ try {
323
+ const err = httpError ?? this.error ?? void 0;
324
+ if (err) this.error = err;
325
+ const logger = this.httpKernel?.getRequestLogger();
326
+ if (!logger) return;
327
+ if (logger.shouldSample && !logger.shouldSample(this, err)) return;
328
+ const entry = logger.renderHttp(this, err);
329
+ return this.log(entry.text, entry.severity, entry.msgid);
330
+ } catch {}
331
+ }
332
+ /**
333
+ * P3.7 — Émet au teardown un log DEBUG détaillant la durée de chaque phase du
334
+ * pipeline (`parse · action · firewall · …` + total). Opt-in via
335
+ * `kernel.options.timing.verbose`. Triple gate perf-first : `_timingVerbose`
336
+ * résolu 1× (false en prod/par défaut) → timing actif → phases non vides ;
337
+ * hors mode verbose, early-return AVANT toute allocation/`toFixed` (coût nul).
338
+ */
339
+ logPhasesVerbose() {
340
+ if (!this._timingVerbose) return;
341
+ const phases = this.phases;
342
+ const n = phases.length;
343
+ if (n === 0) return;
344
+ let line = "";
345
+ let total = 0;
346
+ for (let i = 0; i < n; i++) {
347
+ const p = phases[i];
348
+ const d = p.durationMs ?? (p.endMs !== void 0 ? p.endMs - p.startMs : void 0);
349
+ if (i > 0) line += " · ";
350
+ if (d !== void 0) {
351
+ line += `${p.name}=${d.toFixed(2)}ms`;
352
+ total += d;
353
+ } else line += `${p.name}=…`;
354
+ }
355
+ this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
356
+ }
357
+ addRequestCookie(cookie) {
358
+ if (cookie instanceof Cookie) return this.cookies[cookie.name] = cookie;
359
+ else {
360
+ const error = /* @__PURE__ */ new Error("addCookie cookie not valid !!");
361
+ this.log(cookie, "ERROR");
362
+ throw error;
363
+ }
364
+ }
365
+ getRequestCookies(name) {
366
+ if (name) return this.cookies[name] || null;
367
+ return this.cookies;
368
+ }
369
+ setCookie(cookie) {
370
+ if (cookie) return this.response?.addCookie(cookie);
371
+ }
372
+ getRequest() {
373
+ return this.request;
374
+ }
375
+ getResponse() {
376
+ return this.response;
377
+ }
378
+ isValidDomain() {
379
+ if (!this.httpKernel) throw new Error(`Http Kernel not ready`);
380
+ return this.httpKernel.isValidDomain(this);
381
+ }
382
+ async saveSession() {
383
+ if (this.sessionService) return this.sessionService.saveSession(this);
384
+ throw new Error(`sessionService not found `);
385
+ }
386
+ hasSession() {
387
+ return Boolean(this.cookieSession);
388
+ }
389
+ getCookieSession(name) {
390
+ if (this.cookies[name]) return this.cookies[name];
391
+ return null;
392
+ }
393
+ /**
394
+ * Nom effectif du cookie de session pour CE transport. Sur TLS (https/wss) on
395
+ * applique le préfixe **`__Host-`** (RFC 6265bis §4.1.3 / OWASP : recommandé
396
+ * pour les identifiants de session — impose Secure + Path=/ + interdit Domain,
397
+ * anti session-fixation cross-subdomain). En clair (http/ws) le préfixe est
398
+ * omis (le navigateur le rejetterait sans Secure) → dégradation gracieuse,
399
+ * notamment derrière un proxy qui termine le TLS. Lecture **et** écriture du
400
+ * cookie passent par ce nom unique → cohérence de la reprise (L1).
401
+ */
402
+ getSessionCookieName() {
403
+ const base = this.sessionService?.defaultSessionName ?? "nodefony";
404
+ const mode = (this.sessionService?.options?.cookie)?.hostPrefix ?? "auto";
405
+ const tls = this.scheme === "https" || this.scheme === "wss";
406
+ return mode === true || mode === "auto" && tls ? `__Host-${base}` : base;
407
+ }
408
+ parseCookies() {
409
+ return cookiesParser(this);
410
+ }
411
+ setContextJson(_encoding = "utf-8") {}
412
+ setContextHtml(_encoding = "utf-8") {}
413
+ };
414
+ //#endregion
415
+ export { Context as default, nextCspNonce, setLifecycleEventLogging };
@@ -0,0 +1,88 @@
1
+ import { escapeRegExp } from "nodefony";
2
+ //#region nodefony/src/context/domainMatcher.ts
3
+ /**
4
+ * Matching de domaine (`Host`) — fonctions PURES, politique UNIQUE partagée par
5
+ * les deux étages de Nodefony :
6
+ *
7
+ * - **`trustedHosts` (kernel, sécu)** : barrière testée AVANT le routing. Quels
8
+ * `Host` ce serveur accepte de traiter (anti Host-header injection). Host non
9
+ * trusté → 401 (`HttpKernel.checkValidDomain`). Grossier, optionnel (en prod,
10
+ * typiquement délégué au reverse-proxy via `trustedHosts: true`).
11
+ * - **`@Domain` (route, routing)** : restreint une route / un contrôleur à un
12
+ * vhost. Source de vérité du « qui sert quoi ». Testé PENDANT le routing
13
+ * (`Route.matchHostname`) → 403 si la route ne sert pas ce domaine.
14
+ *
15
+ * Politique de compilation (sûre, ancrée, ReDoS-free) :
16
+ * - string sans `*` → match EXACT ancré (`^...$`, le `.` est littéral).
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`).
19
+ * - `RegExp` → reprise telle quelle (l'auteur assume l'ancrage).
20
+ *
21
+ * Compilation faite UNE fois (boot / enregistrement de route) ; le test par
22
+ * requête est un simple `RegExp.test` sur une liste pré-compilée (zéro alloc hot-path).
23
+ */
24
+ const DEV_LOOPBACK = [
25
+ "localhost",
26
+ "127.0.0.1",
27
+ "[::1]"
28
+ ];
29
+ /**
30
+ * Compile UN pattern de domaine en `RegExp` selon la politique sûre.
31
+ *
32
+ * @param pattern - string (exact / `*`-wildcard) ou `RegExp` (reprise telle quelle).
33
+ * @returns une `RegExp` ancrée (pour les string) ou le `RegExp` fourni.
34
+ */
35
+ function compileDomainPattern(pattern) {
36
+ if (pattern instanceof RegExp) return pattern;
37
+ const body = pattern.split("*").map(escapeRegExp).join("[^.]+");
38
+ return new RegExp(`^${body}$`, "u");
39
+ }
40
+ /**
41
+ * Compile une liste de patterns. Les string vides sont ignorées (un `^$` ne
42
+ * sert à rien et masque une coquille de config).
43
+ *
44
+ * @param patterns - un pattern ou un tableau de patterns.
45
+ * @returns liste de `RegExp` à tester contre le `Host` entrant.
46
+ */
47
+ function compileDomainPatterns(patterns) {
48
+ const list = Array.isArray(patterns) ? patterns : [patterns];
49
+ const out = [];
50
+ for (const p of list) if (p instanceof RegExp) out.push(p);
51
+ else if (typeof p === "string" && p) out.push(compileDomainPattern(p));
52
+ return out;
53
+ }
54
+ /**
55
+ * Construit la liste `RegExp` de la barrière `trustedHosts` (kernel, avant routing).
56
+ *
57
+ * Toujours : le domaine canonique (`kernel.domain`). En development : + loopback
58
+ * (`localhost`/`127.0.0.1`/`[::1]`) pour que l'URL tapée (nom OU IP) passe.
59
+ * `trustedHosts: true` → bypass total (un seul `/^.*$/`).
60
+ *
61
+ * @param domain - domaine canonique du serveur (`kernel.domain`).
62
+ * @param trusted - config `http.trustedHosts` (optionnelle).
63
+ * @param isDev - vrai en environnement `development` (ajoute le loopback).
64
+ * @returns liste de `RegExp` pour {@link isDomainAllowed}.
65
+ */
66
+ function compileTrustedHosts(domain, trusted, isDev) {
67
+ if (trusted === true) return [/^.*$/u];
68
+ const patterns = [domain];
69
+ if (isDev) patterns.push(...DEV_LOOPBACK);
70
+ if (trusted) {
71
+ if (Array.isArray(trusted)) patterns.push(...trusted);
72
+ else patterns.push(trusted);
73
+ }
74
+ return compileDomainPatterns(patterns);
75
+ }
76
+ /**
77
+ * Teste un `Host` entrant contre une liste de `RegExp` pré-compilée.
78
+ *
79
+ * @param regAlias - sortie de {@link compileTrustedHosts} ou {@link compileDomainPatterns}.
80
+ * @param domain - `Host` de la requête (`context.domain`, port déjà strippé).
81
+ * @returns `true` dès le premier match (court-circuit), `false` sinon.
82
+ */
83
+ function isDomainAllowed(regAlias, domain) {
84
+ for (const reg of regAlias) if (reg.test(domain)) return true;
85
+ return false;
86
+ }
87
+ //#endregion
88
+ export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed };
@@ -0,0 +1,185 @@
1
+ import { extractClientIp, resolveFromRight } from "./trustProxy.js";
2
+ import { isIPv4 } from "node:net";
3
+ //#region nodefony/src/context/forwarded.ts
4
+ /**
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
7
+ * séparateur à l'intérieur de guillemets (ex. un host quoté, un IPv6) n'est PAS
8
+ * un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
9
+ */
10
+ function splitTopLevel(input, sep) {
11
+ if (input.indexOf("\"") === -1) return input.split(sep);
12
+ const out = [];
13
+ let buf = "";
14
+ let inQuote = false;
15
+ let escaped = false;
16
+ for (let i = 0; i < input.length; i += 1) {
17
+ const c = input[i];
18
+ if (escaped) {
19
+ buf += c;
20
+ escaped = false;
21
+ continue;
22
+ }
23
+ if (c === "\\" && inQuote) {
24
+ buf += c;
25
+ escaped = true;
26
+ continue;
27
+ }
28
+ if (c === "\"") {
29
+ inQuote = !inQuote;
30
+ buf += c;
31
+ continue;
32
+ }
33
+ if (c === sep && !inQuote) {
34
+ out.push(buf);
35
+ buf = "";
36
+ continue;
37
+ }
38
+ buf += c;
39
+ }
40
+ out.push(buf);
41
+ return out;
42
+ }
43
+ /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
44
+ function unquote(value) {
45
+ const v = value.trim();
46
+ if (v.length >= 2 && v[0] === "\"" && v[v.length - 1] === "\"") return v.slice(1, -1).replace(/\\(.)/gu, "$1");
47
+ return v;
48
+ }
49
+ /**
50
+ * Parse le header `Forwarded` (RFC 7239 §4) en liste de `forwarded-element`,
51
+ * dans l'ordre **gauche→droite** (le plus à gauche = le plus proche du client).
52
+ *
53
+ * Grammaire honorée : `Forwarded = 1#forwarded-element`,
54
+ * `forwarded-element = [ forwarded-pair ] *( ";" [ forwarded-pair ] )`,
55
+ * `forwarded-pair = token "=" value`, `value = token / quoted-string`.
56
+ * Les noms de paramètres sont insensibles à la casse (§7.1) → normalisés.
57
+ *
58
+ * @param raw - valeur brute du header (string, ou string[] si répété), ou `undefined`.
59
+ * @returns les éléments parsés, ou `null` si le header est absent/vide.
60
+ */
61
+ function parseForwarded(raw) {
62
+ if (!raw) return null;
63
+ const header = Array.isArray(raw) ? raw.join(",") : raw;
64
+ if (!header.trim()) return null;
65
+ const elements = [];
66
+ for (const part of splitTopLevel(header, ",")) {
67
+ if (!part.trim()) continue;
68
+ const element = {};
69
+ for (const pair of splitTopLevel(part, ";")) {
70
+ const eq = pair.indexOf("=");
71
+ if (eq === -1) continue;
72
+ const key = pair.slice(0, eq).trim().toLowerCase();
73
+ const rawValue = pair.slice(eq + 1);
74
+ switch (key) {
75
+ case "by":
76
+ element.by = unquote(rawValue);
77
+ break;
78
+ case "for":
79
+ element.for = unquote(rawValue);
80
+ break;
81
+ case "host":
82
+ element.host = unquote(rawValue);
83
+ break;
84
+ case "proto": element.proto = unquote(rawValue).toLowerCase();
85
+ }
86
+ }
87
+ elements.push(element);
88
+ }
89
+ return elements.length ? elements : null;
90
+ }
91
+ /**
92
+ * Extrait l'**IP nue** d'un node identifier RFC 7239 §6
93
+ * (`node = nodename [ ":" node-port ]`).
94
+ *
95
+ * - IPv6 entre crochets (`[2001:db8::1]`, éventuel `]:port`) → l'IPv6 sans crochets.
96
+ * - IPv4 (`192.0.2.43`, éventuel `:port`) → l'IPv4 sans le port.
97
+ * - `unknown` et identifiants obfusqués (`obfnode` commençant par `_`, §6.3) →
98
+ * `null` : non comparables à une politique de confiance (barrière from-right).
99
+ *
100
+ * @param node - valeur d'un paramètre `for`/`by`, déjà déquotée.
101
+ * @returns l'IP exploitable, ou `null` si absente/obfusquée/illisible.
102
+ */
103
+ function forwardedNodeIp(node) {
104
+ if (!node) return null;
105
+ const n = node.trim();
106
+ if (!n) return null;
107
+ if (n[0] === "[") {
108
+ const end = n.indexOf("]");
109
+ return end > 1 ? n.slice(1, end) : null;
110
+ }
111
+ if (n === "unknown" || n[0] === "_") return null;
112
+ const colon = n.indexOf(":");
113
+ const host = colon === -1 ? n : n.slice(0, colon);
114
+ return isIPv4(host) ? host : host || null;
115
+ }
116
+ /** Premier élément (le plus à gauche = côté client) définissant `key`. */
117
+ function firstDefined(elements, key) {
118
+ for (const element of elements) {
119
+ const value = element[key];
120
+ if (value !== void 0) return value;
121
+ }
122
+ }
123
+ /**
124
+ * Premier token d'un en-tête `X-Forwarded-*` multi-valeurs (`"a, b"` → `"a"`).
125
+ * `indexOf` + `slice` (pas de `split`) → 0 array alloué (hot path prod).
126
+ */
127
+ function firstToken(value, lower = false) {
128
+ if (!value) return;
129
+ const s = Array.isArray(value) ? value[0] : value;
130
+ if (!s) return;
131
+ const comma = s.indexOf(",");
132
+ const first = (comma === -1 ? s : s.slice(0, comma)).trim();
133
+ if (!first) return;
134
+ return lower ? first.toLowerCase() : first;
135
+ }
136
+ /**
137
+ * Résout, en **une seule passe**, le scheme, le host et l'IP cliente effectifs à
138
+ * partir des en-têtes forwarded — header standard `Forwarded` (RFC 7239)
139
+ * **prioritaire**, repli sur les `X-Forwarded-*` de-facto.
140
+ *
141
+ * Le scheme/host effectifs sont pris sur l'élément le plus à GAUCHE (côté client) ;
142
+ * l'IP cliente est résolue **from-right** ({@link resolveFromRight}) en partant du
143
+ * socket réel, donc non spoofable. La priorité unique `Forwarded.proto` >
144
+ * `x-forwarded-proto` > `x-forwarded-scheme` supprime l'ancienne double source.
145
+ *
146
+ * À n'appeler que derrière un proxy de confiance (RFC 7239 §8.1) : la décision
147
+ * `isTrusted(socket)` est prise par l'appelant (lazy : pas d'allocation hors proxy).
148
+ *
149
+ * @param headers - en-têtes de la requête.
150
+ * @param socketAddress - `socket.remoteAddress` (connexion TCP réelle).
151
+ * @param checker - politique de confiance ({@link buildTrustProxy}).
152
+ * @returns la résolution canonique unifiée.
153
+ */
154
+ function resolveForwarded(headers, socketAddress, checker) {
155
+ const elements = parseForwarded(headers.forwarded);
156
+ if (elements) {
157
+ const chain = elements.map((element) => forwardedNodeIp(element.for));
158
+ const forwardedFor = elements.map((element) => element.for).filter((value) => Boolean(value)).join(", ") || void 0;
159
+ return {
160
+ proto: firstDefined(elements, "proto"),
161
+ host: firstDefined(elements, "host"),
162
+ forwardedFor,
163
+ clientIp: socketAddress ? resolveFromRight(chain, socketAddress, checker) : null,
164
+ fromStandard: true
165
+ };
166
+ }
167
+ const xff = headers["x-forwarded-for"];
168
+ return {
169
+ proto: firstToken(headers["x-forwarded-proto"], true) ?? firstToken(headers["x-forwarded-scheme"], true),
170
+ host: firstToken(headers["x-forwarded-host"]),
171
+ forwardedFor: typeof xff === "string" ? xff : xff?.join(", "),
172
+ clientIp: extractClientIp(xff, socketAddress, checker),
173
+ fromStandard: false
174
+ };
175
+ }
176
+ /**
177
+ * Au moins un en-tête forwarded (standard ou de-facto) est-il présent ? Permet à
178
+ * l'appelant de n'allouer la résolution que quand c'est utile (hot path : 0 alloc
179
+ * sur une requête directe sans proxy).
180
+ */
181
+ function hasForwardingHeaders(headers) {
182
+ return Boolean(headers.forwarded || headers["x-forwarded-for"] || headers["x-forwarded-proto"] || headers["x-forwarded-scheme"] || headers["x-forwarded-host"]);
183
+ }
184
+ //#endregion
185
+ export { forwardedNodeIp, hasForwardingHeaders, parseForwarded, resolveForwarded };