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

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 (103) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +29 -25
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +6 -3
  7. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  8. package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
  9. package/dist/nodefony/service/audit-logger.js +3 -17
  10. package/dist/nodefony/service/error-renderer.js +6 -4
  11. package/dist/nodefony/service/http-kernel.js +191 -40
  12. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  13. package/dist/nodefony/service/request-logger.js +1 -1
  14. package/dist/nodefony/service/reverse-proxy.js +7 -2
  15. package/dist/nodefony/service/servers/clientError.js +28 -4
  16. package/dist/nodefony/service/servers/server-h2c.js +5 -5
  17. package/dist/nodefony/service/servers/server-http.js +5 -3
  18. package/dist/nodefony/service/servers/server-https.js +12 -5
  19. package/dist/nodefony/service/servers/server-static.js +1 -6
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
  21. package/dist/nodefony/service/servers/server-websocket.js +6 -10
  22. package/dist/nodefony/service/servers/serverShutdown.js +63 -4
  23. package/dist/nodefony/service/sessions/sessions-service.js +3 -3
  24. package/dist/nodefony/service/upload/upload-service.js +2 -2
  25. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  26. package/dist/nodefony/src/context/Context.js +56 -13
  27. package/dist/nodefony/src/context/http/ByteStream.js +359 -0
  28. package/dist/nodefony/src/context/http/HttpContext.js +75 -25
  29. package/dist/nodefony/src/context/http/Request.js +15 -6
  30. package/dist/nodefony/src/context/http/Response.js +354 -35
  31. package/dist/nodefony/src/context/http/SseStream.js +81 -146
  32. package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  34. package/dist/nodefony/src/context/http2/Request.js +1 -1
  35. package/dist/nodefony/src/context/http2/Response.js +28 -5
  36. package/dist/nodefony/src/context/requestTrace.js +104 -0
  37. package/dist/nodefony/src/context/timingMode.js +21 -0
  38. package/dist/nodefony/src/context/transport.js +20 -1
  39. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  40. package/dist/nodefony/src/context/websocket/Response.js +4 -2
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
  42. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  43. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  44. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  45. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  46. package/dist/nodefony/src/cookies/cookie.js +54 -13
  47. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  48. package/dist/nodefony/src/profiler/Profiler.js +25 -22
  49. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  50. package/dist/nodefony/src/proxy/forward.js +109 -9
  51. package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
  52. package/dist/nodefony/src/proxy/rules.js +29 -14
  53. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  54. package/dist/types/index.d.ts +8 -0
  55. package/dist/types/nodefony/config/config.d.ts +8 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  57. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  58. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  61. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  62. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  64. package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
  65. package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
  66. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
  68. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  69. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  70. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  71. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
  72. package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
  73. package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
  74. package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
  75. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  76. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  79. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  80. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  81. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
  83. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  84. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  85. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  86. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  87. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  88. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  89. package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
  90. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  91. package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
  92. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  93. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  94. package/docs/byte-stream.md +371 -0
  95. package/docs/cookies.md +13 -3
  96. package/docs/diagnostics-channel.md +303 -0
  97. package/docs/index.md +14 -1
  98. package/docs/observabilite.md +94 -16
  99. package/docs/reverse-proxy.md +33 -19
  100. package/docs/servers.md +26 -15
  101. package/docs/session.md +3 -3
  102. package/docs/sse.md +27 -20
  103. package/package.json +16 -16
package/README.md CHANGED
@@ -43,6 +43,30 @@ Le serveur le pose dans le contexte asynchrone de la requête : `RequestContext.
43
43
  depuis n'importe quel code, et les services `@injectable({ scope: "request" })` y vivent. Détail :
44
44
  [Injection & portées](https://github.com/nodefony/nodefony-core/blob/main/docs/architecture/injection-portees.md).
45
45
 
46
+ ## Réponses en flux
47
+
48
+ Deux formats de réponse qui ne finissent pas d'un coup, en HTTP/1.1 comme en HTTP/2, sur le même
49
+ cycle de vie (réponse prise, session sauvée avant les en-têtes, départ du client vu sur la réponse,
50
+ plafond par IP, révocation, coupure d'un client qui ne lit plus) :
51
+
52
+ | Besoin | Contrôleur | Contexte | Page |
53
+ | ---------------------------------------------------------- | ------------------------ | ------------------------- | -------------------------------------- |
54
+ | Pousser des **événements texte** lus par `EventSource` | `this.renderSse()` | `context.openSse()` | [Flux SSE](./docs/sse.md) |
55
+ | Servir des **octets** : processus, relais `fetch`, archive | `this.renderStream(src)` | `context.openStream(src)` | [Flux d'octets](./docs/byte-stream.md) |
56
+
57
+ ```ts
58
+ const tar = spawn("tar", ["-cz", "-C", "var", "logs"], {
59
+ stdio: ["ignore", "pipe", "ignore"],
60
+ });
61
+ try {
62
+ const { reason, bytes } = await this.renderStream(tar.stdout, {
63
+ headers: { "content-type": "application/gzip" },
64
+ });
65
+ } finally {
66
+ tar.kill("SIGKILL");
67
+ }
68
+ ```
69
+
46
70
  ## Rate-limit général par IP
47
71
 
48
72
  Plafond de trafic **par IP cliente** sur toutes les routes HTTP — protège la capacité du serveur
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.155.0/helpers/esm/decorate.js
1
+ // \0@oxc-project+runtime@0.157.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.155.0/helpers/esm/decorateParam.js
1
+ // \0@oxc-project+runtime@0.157.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
package/dist/index.js CHANGED
@@ -1,30 +1,8 @@
1
- import { normalizePrefix, parseProxyOrigin, proxyMountProblems } from "./nodefony/src/proxy/rules.js";
2
1
  import config, { httpConfigSchema, httpOverlaySchema } from "./nodefony/config/config.js";
3
2
  import { defineHttpConfig, httpConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
4
3
  import { createHttpAdminApi } from "./nodefony/service/HttpAdminApi.js";
5
4
  import { createProfilerAdminApi } from "./nodefony/service/ProfilerAdminApi.js";
6
- import { ACTION_SELF_PHASE, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, TOOLING_URL_PREFIX, aggregateProfiles, createProfileAggregator, isFailedProfile, sqlShape } from "./nodefony/src/profiler/aggregate.js";
7
5
  import { Profiler } from "./nodefony/src/profiler/Profiler.js";
8
- import HttpError from "./nodefony/src/errors/httpError.js";
9
- import { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
10
- import { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "./nodefony/src/context/domainMatcher.js";
11
- import Cookie from "./nodefony/src/cookies/cookie.js";
12
- import Context from "./nodefony/src/context/Context.js";
13
- import { decideSend, readBackpressureOptions } from "./nodefony/src/context/websocket/wsBackpressure.js";
14
- import WebsocketResponse from "./nodefony/src/context/websocket/Response.js";
15
- import { FrameProfile } from "./nodefony/src/profiler/FrameProfile.js";
16
- import WebsocketContext from "./nodefony/src/context/websocket/WebsocketContext.js";
17
- import HttpRequest from "./nodefony/src/context/http/Request.js";
18
- import HttpResponse from "./nodefony/src/context/http/Response.js";
19
- import Http2Request from "./nodefony/src/context/http2/Request.js";
20
- import Http2Response from "./nodefony/src/context/http2/Response.js";
21
- import { SSE_HEARTBEAT_MS, SseStream, acceptsEventStream, openSseStream } from "./nodefony/src/context/http/SseStream.js";
22
- import HttpContext from "./nodefony/src/context/http/HttpContext.js";
23
- import DefaultErrorRenderer, { schemaMismatchOf, toValidationFields } from "./nodefony/service/error-renderer.js";
24
- import JsonAuditLogger from "./nodefony/service/audit-logger.js";
25
- import DefaultRequestLogger from "./nodefony/service/request-logger.js";
26
- import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
27
- import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
28
6
  import http_kernel_default from "./nodefony/service/http-kernel.js";
29
7
  import server_http_default from "./nodefony/service/servers/server-http.js";
30
8
  import server_https_default from "./nodefony/service/servers/server-https.js";
@@ -37,13 +15,39 @@ import Network from "./nodefony/command/networkCommand.js";
37
15
  import Certificates from "./nodefony/command/certificatesCommand.js";
38
16
  import ProxyGenerate from "./nodefony/command/proxyGenerateCommand.js";
39
17
  import AssetsPublish from "./nodefony/command/assetsPublishCommand.js";
40
- import Session from "./nodefony/src/session/session.js";
41
- import { SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS } from "./nodefony/src/session/storage/sessionSort.js";
42
18
  import sessions_service_default from "./nodefony/service/sessions/sessions-service.js";
43
19
  import Certificate, { trustOutboundAuthorities } from "./nodefony/service/certificates.js";
20
+ import Context from "./nodefony/src/context/Context.js";
21
+ import Session from "./nodefony/src/session/session.js";
22
+ import HttpError from "./nodefony/src/errors/httpError.js";
23
+ import HttpResponse, { isContentlessStatus } from "./nodefony/src/context/http/Response.js";
24
+ import Http2Response from "./nodefony/src/context/http2/Response.js";
25
+ import WebsocketResponse from "./nodefony/src/context/websocket/Response.js";
26
+ import Cookie from "./nodefony/src/cookies/cookie.js";
44
27
  import upload_service_default from "./nodefony/service/upload/upload-service.js";
28
+ import DefaultErrorRenderer, { schemaMismatchOf, toValidationFields } from "./nodefony/service/error-renderer.js";
29
+ import DefaultRequestLogger from "./nodefony/service/request-logger.js";
30
+ import JsonAuditLogger from "./nodefony/service/audit-logger.js";
31
+ import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
32
+ import HttpContext from "./nodefony/src/context/http/HttpContext.js";
33
+ import WebsocketContext from "./nodefony/src/context/websocket/WebsocketContext.js";
34
+ import HttpRequest from "./nodefony/src/context/http/Request.js";
35
+ import Http2Request from "./nodefony/src/context/http2/Request.js";
36
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
37
+ import { parseRequestTarget } from "./nodefony/src/context/http/urlFastPath.js";
38
+ import { decideSend, readBackpressureOptions } from "./nodefony/src/context/websocket/wsBackpressure.js";
39
+ import { FRAME_RATE_WINDOW_MS, admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter } from "./nodefony/src/context/websocket/wsFrameRate.js";
45
40
  import { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
41
+ import { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
42
+ import { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "./nodefony/src/context/domainMatcher.js";
43
+ import { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
44
+ import { SSE_HEARTBEAT_MS, SseStream, acceptsEventStream, openSseStream } from "./nodefony/src/context/http/SseStream.js";
45
+ import { openByteStream } from "./nodefony/src/context/http/ByteStream.js";
46
+ import { ACTION_SELF_PHASE, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, TOOLING_URL_PREFIX, aggregateProfiles, createProfileAggregator, isFailedProfile, sqlShape } from "./nodefony/src/profiler/aggregate.js";
47
+ import { FrameProfile } from "./nodefony/src/profiler/FrameProfile.js";
48
+ import { normalizePrefix, parseProxyOrigin, proxyMountProblems } from "./nodefony/src/proxy/rules.js";
46
49
  import { PrebuiltUi, resolveUiDelivery } from "./nodefony/src/assets/prebuiltUi.js";
50
+ import { SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS } from "./nodefony/src/session/storage/sessionSort.js";
47
51
  import { Module, services } from "nodefony";
48
52
  // index.ts
49
53
  let Http = class Http extends Module {
@@ -115,4 +119,4 @@ Http = __decorate([services([
115
119
  upload_service_default
116
120
  ])], Http);
117
121
  var http_default = Http;
118
- export { ACTION_SELF_PHASE, Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, PrebuiltUi, PrettyRequestLogger, Profiler, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, TOOLING_URL_PREFIX, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, aggregateProfiles, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, createProfileAggregator, decideSend, http_default as default, defineHttpConfig, httpConfigJsonSchema, httpConfigSchema, isDomainAllowed, isFailedProfile, isWebsocketContext, normalizePrefix, openSseStream, parseProxyOrigin, proxyMountProblems, readBackpressureOptions, resolveUiDelivery, schemaMismatchOf, sqlShape, toValidationFields };
122
+ export { ACTION_SELF_PHASE, Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FRAME_RATE_WINDOW_MS, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, PrebuiltUi, PrettyRequestLogger, Profiler, REQUEST_TRACE_CHANNEL, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, TOOLING_URL_PREFIX, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, admitFrame, aggregateProfiles, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, createProfileAggregator, decideSend, http_default as default, defineHttpConfig, frameRateRejectedTotal, httpConfigJsonSchema, httpConfigSchema, isContentlessStatus, isDomainAllowed, isFailedProfile, isWebsocketContext, normalizePrefix, openByteStream, openSseStream, parseProxyOrigin, parseRequestTarget, proxyMountProblems, readBackpressureOptions, readFrameRateBound, readFrameRateCloseAfter, resolveUiDelivery, schemaMismatchOf, sqlShape, toValidationFields };
@@ -1,7 +1,7 @@
1
- import { resolveTrustedHostNames } from "../src/context/domainMatcher.js";
2
- import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
3
1
  import { defaultIntrospection, generateHaproxyConfig, generateNginxConfig } from "../src/proxy/generateProxyConfig.js";
4
2
  import { planAssetPublish } from "../src/assets/collectAssets.js";
3
+ import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
4
+ import { resolveTrustedHostNames } from "../src/context/domainMatcher.js";
5
5
  import { Command } from "nodefony";
6
6
  import fsp from "node:fs/promises";
7
7
  import { isAbsolute, resolve } from "node:path";
@@ -101,7 +101,7 @@ const securityHeadersSchema = z.strictObject({
101
101
  }),
102
102
  frameOptions: z.string().nullable().default("DENY").meta({
103
103
  runtimeMutable: true,
104
- description: "X-Frame-Options — anti-clickjacking. `DENY` (recommandé) | `SAMEORIGIN`. Superposé par CSP `frame-ancestors`. `null` = désactivé. Éditable à chaud (recompute HttpKernel)."
104
+ description: "Anti-clickjacking — pilote `X-Frame-Options` ET la directive CSP `frame-ancestors` (posée par @nodefony/security) : `DENY` (défaut, 'none') | `SAMEORIGIN` ('self') | `null` (aucun des deux). Encadrement par une origine tierce : `frame-ancestors <origine>` dans `security.headers.csp`, qui l’emporte. Éditable à chaud."
105
105
  }),
106
106
  strictTransportSecurity: strictTransportSecuritySchema.nullable().default(() => strictTransportSecuritySchema.parse({})).describe("HSTS — voir sous-schéma. `null` = header désactivé.")
107
107
  }).describe("En-têtes de sécurité HTTP — defaults OWASP secure-by-default, posés en amont du pipeline (couvre aussi les statics).");
@@ -224,7 +224,9 @@ const websocketSchema = z.looseObject({
224
224
  allowSynchronousEvents: z.boolean().default(true).describe("Émet les events ('message', 'ping'…) de plusieurs frames lues dans un même chunk réseau de façon synchrone (ws@8.18+). true = meilleur débit ; false = un event par tick d'event loop (latence plus régulière, plus équitable entre connexions)."),
225
225
  maxBackpressure: z.number().int().nonnegative().default(4194304).describe("Seuil (octets) du buffer d'envoi par connexion (`ws.bufferedAmount`) au-delà duquel `backpressurePolicy` s'applique. Borne la RAM d'envoi face à un client LENT À RECEVOIR (anti-OOM) ; broadcast() amplifie (un seul lent peut plomber la diffusion). 0 = désactivé. Défaut 4 MiB."),
226
226
  backpressureCloseAfterDrops: z.number().int().nonnegative().default(1e3).describe("SOLDE de frames refusées au-delà duquel la connexion est FERMÉE (1013), en politique 'drop' : +1 par frame refusée, −1 par frame envoyée. POURQUOI un solde et pas un second seuil d'octets : une fois qu'on jette, plus rien n'alimente la file — elle plafonne au seuil de drop et n'atteint jamais un seuil supérieur, qui serait donc inatteignable (mesuré). Et pas une série remise à zéro au premier envoi : la file d'un client bloqué oscille autour du seuil, chaque envoi effacerait la série et il ne serait jamais fermé. Le solde, lui, distingue le pic passager du client mort. 0 = ne jamais fermer. Défaut 1000."),
227
- backpressurePolicy: z.enum(["drop", "close"]).default("drop").describe("Action quand `ws.bufferedAmount` dépasse `maxBackpressure`. 'drop' = sauter la frame pour ce client (il reste connecté, dégradable — idéal télémétrie/broadcast, défaut). 'close' = fermer le client (RFC 6455 close 1013 « Try Again Later »). La fusion (coalesce) relève de la couche canal realtime, pas du transport.")
227
+ backpressurePolicy: z.enum(["drop", "close"]).default("drop").describe("Action quand `ws.bufferedAmount` dépasse `maxBackpressure`. 'drop' = sauter la frame pour ce client (il reste connecté, dégradable — idéal télémétrie/broadcast, défaut). 'close' = fermer le client (RFC 6455 close 1013 « Try Again Later »). La fusion (coalesce) relève de la couche canal realtime, pas du transport."),
228
+ maxFramesPerSecond: z.number().int().nonnegative().default(0).describe("Trames REÇUES admises par seconde et par connexion (unité : une trame, texte ou binaire — la taille est bornée à part par `maxPayload`). Au-delà, la trame est écartée AVANT tout décodage et comptée (`frameRateRejectedTotal` sur la sonde du hub temps réel) ; la connexion reste ouverte. 0 = désactivée (défaut de toute la série 10). Repère : 50 trames/s d'audio passent avec marge sous une borne de 200."),
229
+ frameRateCloseAfterRejects: z.number().int().nonnegative().default(1e3).describe("SOLDE de trames écartées au-delà duquel la connexion est FERMÉE (1008, violation de politique) : +1 par trame écartée, −1 par trame admise — même règle que `backpressureCloseAfterDrops`. Un pic passager ne ferme pas ; seul un flux soutenu au-delà du DOUBLE de `maxFramesPerSecond` fait monter le solde. Sans effet tant que `maxFramesPerSecond` vaut 0. 0 = ne jamais fermer. Défaut 1000.")
228
230
  }).describe("Serveur WebSocket (`ws@8`). Les options ci-dessus (+ loose : `verifyClient`, `handleProtocols`, `path`, `WebSocket`…) sont transmises à `ws`. GÉRÉ par Nodefony et donc NON exposé : `server`/`host`/`port`/`noServer` (forcé à `noServer` : l'upgrade du serveur HTTP passe d'abord par le proxy inverse, puis par `ws`), `clientTracking` (forcé true — requis par broadcast() + heartbeat), `backlog` (celui du serveur HTTP). `keepalive*`/`closeTimeout` sont des knobs Nodefony, pas des options `ws`.");
229
231
  const staticOptionsSchema = z.looseObject({
230
232
  cacheControl: z.boolean().optional().describe("Active le header Cache-Control."),
@@ -297,7 +299,8 @@ const proxyMountSchema = z.strictObject({
297
299
  preserveHost: z.boolean().default(false).describe("Transmet le `Host` du client au lieu de celui de la cible. Défaut false : l'amont reçoit son propre nom (ce qu'attend un serveur qui filtre les noms d'hôte), le nom d'origine part en `X-Forwarded-Host`."),
298
300
  stripHeaders: z.array(z.string()).default([]).describe("En-têtes de requête à ne PAS transmettre (ex. `[\"cookie\", \"authorization\"]` vers un amont qui n'a pas à voir la session). Les en-têtes de connexion (RFC 9110 §7.6.1) ne le sont jamais."),
299
301
  timeoutMs: z.number().optional().describe("Délai d'inactivité avec l'amont (ms) — dépassé avant la réponse (ou avant le `101` d'un upgrade WebSocket) : 504. Absent = `proxy.timeoutMs`. Ne s'applique pas à un WebSocket établi."),
300
- secure: z.boolean().default(true).describe("Vérifie le certificat d'une cible https. `false` n'est accepté que vers la boucle locale (`127.0.0.0/8`, `localhost`, `::1`) ; ailleurs, fournir l'autorité de l'amont (`certificates.extraCaFiles`, ou `NODE_EXTRA_CA_CERTS`).")
302
+ secure: z.boolean().default(true).describe("Vérifie le certificat d'une cible https. `false` n'est accepté que vers la boucle locale (`127.0.0.0/8`, `localhost`, `::1`) ; ailleurs, fournir l'autorité de l'amont (`certificates.extraCaFiles`, ou `NODE_EXTRA_CA_CERTS`)."),
303
+ accessLog: z.enum(["all", "errors"]).default("all").describe("Ligne d'accès des requêtes relayées : `all` (défaut) les écrit toutes, `errors` seulement celles en échec (statut ≥ 400) — pour un amont qui sert beaucoup de petits fichiers.")
301
304
  }).describe("Un montage du proxy inverse : un préfixe d'URL relayé vers un amont.");
302
305
  const proxySchema = z.strictObject({
303
306
  timeoutMs: z.number().int().positive().default(3e4).describe("Délai d'inactivité par défaut de tous les montages (ms) : un amont muet au-delà rend 504 Gateway Timeout. Défaut 30 s."),
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
+ import { aggregateProfiles } from "../src/profiler/aggregate.js";
1
2
  // nodefony/service/ProfilerAdminApi.ts
2
3
  /**
3
4
  * Producteur `IAdminApi` du **profiler** — exposé sous `/nodefony/profiler/api/*`.
@@ -9,7 +10,11 @@
9
10
  *
10
11
  * Endpoints :
11
12
  * - `GET /nodefony/profiler/api/recent` → derniers profils (résumés, récent → ancien)
12
- * - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId
13
+ * `?connection=<requestId>` : les seules frames de cette socket WebSocket
14
+ * - `GET /nodefony/profiler/api/summary` → les profils de l'anneau agrégés par
15
+ * route (percentiles, phases, formes SQL, N+1, erreurs — `aggregateProfiles`)
16
+ * - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId ;
17
+ * absent → 404 `{ startedAt, clearedAt }` : depuis quand l'anneau sait quelque chose
13
18
  * - `DELETE /nodefony/profiler/api/recent` → vide le ring buffer
14
19
  *
15
20
  * La debug bar (toute page, dev) lit `X-Request-Id` de SON appel AJAX puis
@@ -32,9 +37,11 @@ function createProfilerAdminApi(profiler) {
32
37
  const raw = query.limit;
33
38
  const n = typeof raw === "string" ? parseInt(raw, 10) : NaN;
34
39
  const limit = Number.isFinite(n) && n > 0 ? Math.min(n, 200) : 60;
40
+ const of = query.connection;
41
+ const connection = typeof of === "string" && of.length > 0 && of.length <= 200 ? of : void 0;
35
42
  return {
36
43
  count: profiler.size,
37
- entries: profiler.recent(limit)
44
+ entries: profiler.recent(limit, connection)
38
45
  };
39
46
  }
40
47
  },
@@ -47,6 +54,11 @@ function createProfilerAdminApi(profiler) {
47
54
  return { cleared: true };
48
55
  }
49
56
  },
57
+ {
58
+ path: "summary",
59
+ summary: "Profiles aggregated by route: percentiles, SQL, N+1, errors",
60
+ handler: () => aggregateProfiles(profiler.since(0).entries)
61
+ },
50
62
  {
51
63
  path: "{id}",
52
64
  summary: "Full profile (phase timeline) for a requestId",
@@ -57,7 +69,9 @@ function createProfilerAdminApi(profiler) {
57
69
  status: 404,
58
70
  body: {
59
71
  error: "Profile not found",
60
- requestId: params.id
72
+ requestId: params.id,
73
+ startedAt: profiler.startedAt,
74
+ clearedAt: profiler.clearedAt
61
75
  }
62
76
  };
63
77
  return entry;
@@ -1,5 +1,4 @@
1
1
  import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
2
- import { performance } from "node:perf_hooks";
3
2
  // nodefony/service/audit-logger.ts
4
3
  const statusOf = (context) => {
5
4
  return context.response?.statusCode ?? null;
@@ -8,19 +7,6 @@ const headersOf = (context) => {
8
7
  return context.request?.headers ?? {};
9
8
  };
10
9
  /**
11
- * Durée totale de la requête, de la première phase (`startMs`) à maintenant.
12
- * `null` si le chronométrage est coupé (aucune phase enregistrée). Partagée
13
- * par le journal d'audit et le journal lisible — une seule définition.
14
- *
15
- * @param phases - phases chronométrées du contexte
16
- * @returns durée en millisecondes, ou `null`
17
- */
18
- function computeDurationMs(phases) {
19
- const first = phases.at(0);
20
- if (first === void 0 || typeof first.startMs !== "number") return null;
21
- return performance.now() - first.startMs;
22
- }
23
- /**
24
10
  * Severity derived from HTTP status code — RFC 9110 categories.
25
11
  * 1xx/2xx/3xx → INFO ; 4xx → WARNING ; 5xx → ERROR.
26
12
  * Unknown/missing status → INFO.
@@ -96,7 +82,7 @@ var JsonAuditLogger = class {
96
82
  method: ctx.method,
97
83
  url: ctx.url,
98
84
  status,
99
- durationMs: computeDurationMs(ctx.phases),
85
+ durationMs: ctx.stopClock(),
100
86
  remoteAddress: ctx.remoteAddress ?? null,
101
87
  host: ctx.getHost() ?? null,
102
88
  userAgent: ctx.getUserAgent() ?? null,
@@ -128,7 +114,7 @@ var JsonAuditLogger = class {
128
114
  method: ctx.method,
129
115
  url: ctx.url,
130
116
  status,
131
- durationMs: computeDurationMs(ctx.phases),
117
+ durationMs: ctx.stopClock(),
132
118
  remoteAddress: ctx.remoteAddress ?? null,
133
119
  host: ctx.getHost() ?? null,
134
120
  userAgent: ctx.getUserAgent() ?? null,
@@ -165,4 +151,4 @@ var JsonAuditLogger = class {
165
151
  return entry;
166
152
  }
167
153
  };
168
- export { computeDurationMs, JsonAuditLogger as default, severityFromStatus };
154
+ export { JsonAuditLogger as default, severityFromStatus, statusOf };
@@ -208,10 +208,11 @@ const PG_INVALID_INPUT = /^invalid input syntax for type (\w+): "(.*)"$/su;
208
208
  /**
209
209
  * Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
210
210
  *
211
- * Deux moteurs le disent : MongoDB par une `CastError` de Mongoose (la clé est
212
- * un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier).
213
- * SQLite et MySQL rangent la clé en texte : ils ne lèvent rien, la lecture rend
214
- * simplement « rien », d'où leur 404 — c'est la cohérence avec eux qui est visée.
211
+ * Trois sources le disent : MongoDB par une `CastError` de Mongoose (la clé est
212
+ * un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier), et
213
+ * le kit de colonnes portable par une `ColumnValueError` de genre `uuid` ou
214
+ * `integer` — il refuse sur les trois moteurs la clé que PostgreSQL refuserait. Une clé rangée en
215
+ * texte NATIF ne lève rien, la lecture rend « rien » : le même 404.
215
216
  *
216
217
  * Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
217
218
  *
@@ -223,6 +224,7 @@ function malformedIdentifierOf(error) {
223
224
  for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
224
225
  const candidate = current;
225
226
  if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
227
+ if (candidate.name === "ColumnValueError" && (candidate.kind === "uuid" || candidate.kind === "integer") && typeof candidate.value === "string") return candidate.value;
226
228
  if (String(candidate.code) === "22P02") {
227
229
  const match = PG_INVALID_INPUT.exec(candidate.message);
228
230
  if (match) {