@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.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -25
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
- package/dist/nodefony/service/audit-logger.js +3 -17
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +191 -40
- package/dist/nodefony/service/pretty-request-logger.js +11 -10
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +56 -13
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +75 -25
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +28 -5
- package/dist/nodefony/src/context/requestTrace.js +104 -0
- package/dist/nodefony/src/context/timingMode.js +21 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/unsentResponse.js +30 -0
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +54 -13
- package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
- package/dist/nodefony/src/profiler/Profiler.js +25 -22
- package/dist/nodefony/src/profiler/aggregate.js +26 -6
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +8 -0
- package/dist/types/nodefony/config/config.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +25 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
- package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
- package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +303 -0
- package/docs/index.md +14 -1
- package/docs/observabilite.md +94 -16
- package/docs/reverse-proxy.md +33 -19
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- 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.
|
|
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);
|
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
|
|
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
|
-
*
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
*
|
|
212
|
-
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier)
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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) {
|