@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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { isLoopbackHostname } from "nodefony";
|
|
1
|
+
import { isLoopbackHostname, parseBareOrigin } from "nodefony";
|
|
2
2
|
// nodefony/src/proxy/rules.ts
|
|
3
|
+
/** Schémas d’amont d’un relais : un proxy inverse ne parle que HTTP. */
|
|
4
|
+
const PROXY_ORIGIN_PROTOCOLS = ["http:", "https:"];
|
|
3
5
|
/**
|
|
4
6
|
* Règles d'un montage du proxy inverse — UNIQUE source, lue par le schéma de
|
|
5
7
|
* configuration (`proxy.mounts`) ET par `ReverseProxy.mount()` : un montage
|
|
@@ -49,17 +51,7 @@ function normalizePrefix(prefix) {
|
|
|
49
51
|
* @returns l'URL analysée, ou `null` si ce n'est pas une origine http(s) nue
|
|
50
52
|
*/
|
|
51
53
|
function parseProxyOrigin(origin) {
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
url = new URL(origin);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
59
|
-
if (!/^https?:\/\/[^/?#]+\/?$/i.test(origin.trim())) return null;
|
|
60
|
-
if (url.search !== "" || url.hash !== "") return null;
|
|
61
|
-
if (url.username !== "" || url.password !== "") return null;
|
|
62
|
-
return url;
|
|
54
|
+
return parseBareOrigin(origin, PROXY_ORIGIN_PROTOCOLS);
|
|
63
55
|
}
|
|
64
56
|
/**
|
|
65
57
|
* Examine un montage. Rend TOUS les refus, pas le premier : une configuration
|
|
@@ -163,6 +155,29 @@ const UPGRADE_WEBSOCKET = /^\s*websocket\s*$/i;
|
|
|
163
155
|
/** Le jeton `upgrade` dans une liste `Connection` (RFC 9110 §7.6.1). */
|
|
164
156
|
const CONNECTION_UPGRADE = /(?:^|,)\s*upgrade\s*(?:,|$)/i;
|
|
165
157
|
/**
|
|
158
|
+
* Le corps reçu porte-t-il un codage de transfert que le serveur ne sait pas
|
|
159
|
+
* défaire ? Seul `chunked` est compris : Node le retire, mais laisse en place
|
|
160
|
+
* tout codage qui le précède (`gzip, chunked`) — le corps livré serait encore
|
|
161
|
+
* compressé. RFC 9112 §6.1 : un tel message reçoit 501. Un `chunked` qui n'est
|
|
162
|
+
* pas le DERNIER codage est déjà refusé par Node en 400 (§6.3).
|
|
163
|
+
*
|
|
164
|
+
* Seule règle, pour le serveur d'origine et pour le proxy inverse. Chemin de
|
|
165
|
+
* chaque requête : une lecture d'en-tête, rien d'autre sans `Transfer-Encoding`.
|
|
166
|
+
*
|
|
167
|
+
* @param coding - valeur de l'en-tête `Transfer-Encoding`, absente le plus souvent
|
|
168
|
+
* @returns `true` quand le corps ne peut pas être lu tel qu'annoncé
|
|
169
|
+
*/
|
|
170
|
+
function isUnsupportedTransferCoding(coding) {
|
|
171
|
+
if (coding === void 0 || coding === "chunked") return false;
|
|
172
|
+
return coding.trim().toLowerCase() !== "chunked";
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Raison rendue par {@link websocketHandshakeProblem} quand le client parle une
|
|
176
|
+
* autre version du protocole : la seule qui appelle une réponse distincte —
|
|
177
|
+
* le client doit apprendre vers quelle version se rabattre (RFC 6455 §4.4).
|
|
178
|
+
*/
|
|
179
|
+
const WS_UNSUPPORTED_VERSION = "Sec-WebSocket-Version ≠ 13";
|
|
180
|
+
/**
|
|
166
181
|
* Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
|
|
167
182
|
* un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
|
|
168
183
|
* octet pour octet ouvrirait un tunnel que rien n'inspecte.
|
|
@@ -180,9 +195,9 @@ function websocketHandshakeProblem(method, headers) {
|
|
|
180
195
|
if (typeof upgrade !== "string" || !UPGRADE_WEBSOCKET.test(upgrade)) return "Upgrade autre que websocket";
|
|
181
196
|
const connection = headers.connection;
|
|
182
197
|
if (!(typeof connection === "string" ? CONNECTION_UPGRADE.test(connection) : Array.isArray(connection) && connection.some((token) => CONNECTION_UPGRADE.test(token)))) return "Connection sans upgrade";
|
|
183
|
-
if (headers["sec-websocket-version"] !== "13") return
|
|
198
|
+
if (headers["sec-websocket-version"] !== "13") return WS_UNSUPPORTED_VERSION;
|
|
184
199
|
const key = headers["sec-websocket-key"];
|
|
185
200
|
if (typeof key !== "string" || !WS_KEY.test(key)) return "Sec-WebSocket-Key invalide";
|
|
186
201
|
return null;
|
|
187
202
|
}
|
|
188
|
-
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
203
|
+
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, WS_UNSUPPORTED_VERSION, isAmbiguousPath, isUnsupportedTransferCoding, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
@@ -1,7 +1,60 @@
|
|
|
1
|
+
import { refuseMalformedHandshake, refuseUpgrade } from "../proxy/forward.js";
|
|
1
2
|
import { websocketHandshakeProblem } from "../proxy/rules.js";
|
|
2
|
-
import { refuseUpgrade } from "../proxy/forward.js";
|
|
3
3
|
import { STATUS_CODES } from "node:http";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
4
5
|
// nodefony/src/servers/upgradeDispatch.ts
|
|
6
|
+
/**
|
|
7
|
+
* Sous-protocole choisi par handshake, entre {@link IUpgradeAdmission.selectSubprotocol}
|
|
8
|
+
* et le rappel `handleProtocols` de `ws` — qui ne reçoit que la requête. Une
|
|
9
|
+
* table faible plutôt qu'une variable « en cours » : un `verifyClient`
|
|
10
|
+
* asynchrone sépare les deux instants, et deux handshakes s'y croiseraient.
|
|
11
|
+
* Une entrée n'existe que pour un handshake qui a choisi.
|
|
12
|
+
*/
|
|
13
|
+
const chosenSubprotocols = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Fabrique l'option `handleProtocols` du serveur `ws` de Nodefony : le choix
|
|
16
|
+
* retenu par l'admission ({@link IUpgradeAdmission.selectSubprotocol}) s'il y
|
|
17
|
+
* en a un, sinon `fallback` (celle de la configuration), sinon le premier
|
|
18
|
+
* offert — le défaut de `ws` 8, conservé pour tout contrôleur qui ne choisit
|
|
19
|
+
* pas.
|
|
20
|
+
*
|
|
21
|
+
* @param fallback - l'option `handleProtocols` de la configuration, s'il y en a une
|
|
22
|
+
* @returns le rappel à passer à `new WebSocketServer({ handleProtocols })`
|
|
23
|
+
*/
|
|
24
|
+
function createSubprotocolHandler(fallback) {
|
|
25
|
+
return (protocols, request) => {
|
|
26
|
+
const chosen = chosenSubprotocols.get(request);
|
|
27
|
+
if (chosen !== void 0) {
|
|
28
|
+
chosenSubprotocols.delete(request);
|
|
29
|
+
return chosen !== false && protocols.has(chosen) ? chosen : false;
|
|
30
|
+
}
|
|
31
|
+
if (fallback !== void 0) return fallback(protocols, request);
|
|
32
|
+
const first = protocols.values().next();
|
|
33
|
+
return first.done === true ? false : first.value;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Construit le serveur `ws` de Nodefony à partir des options de la
|
|
38
|
+
* configuration, en forçant ce que Nodefony gère — seul endroit où ces
|
|
39
|
+
* options sont posées, pour le serveur clair comme pour le sécurisé :
|
|
40
|
+
*
|
|
41
|
+
* - `noServer` : l'upgrade passe d'abord par le proxy inverse, puis par
|
|
42
|
+
* {@link attachUpgradeDispatch} ;
|
|
43
|
+
* - `clientTracking` : requis par `broadcast()` et le heartbeat ;
|
|
44
|
+
* - `handleProtocols` : le sous-protocole CHOISI par le contrôleur
|
|
45
|
+
* ({@link createSubprotocolHandler}), celui de la configuration à défaut.
|
|
46
|
+
*
|
|
47
|
+
* @param options - options `ws` issues de la configuration du serveur WebSocket
|
|
48
|
+
* @returns le serveur, sans écoute propre
|
|
49
|
+
*/
|
|
50
|
+
function createNodefonyWebSocketServer(options) {
|
|
51
|
+
return new WebSocketServer({
|
|
52
|
+
...options,
|
|
53
|
+
noServer: true,
|
|
54
|
+
clientTracking: true,
|
|
55
|
+
handleProtocols: createSubprotocolHandler(options.handleProtocols)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
5
58
|
/** Défaut sans noyau : tout upgrade conforme est accepté puis émis en `connection`. */
|
|
6
59
|
function acceptAll(wss) {
|
|
7
60
|
return (req, admission) => {
|
|
@@ -38,6 +91,9 @@ var UpgradeAdmission = class {
|
|
|
38
91
|
this.#onPendingError = () => socket.destroy();
|
|
39
92
|
socket.on("error", this.#onPendingError);
|
|
40
93
|
}
|
|
94
|
+
selectSubprotocol(protocol) {
|
|
95
|
+
if (!this.#settled) chosenSubprotocols.set(this.req, protocol);
|
|
96
|
+
}
|
|
41
97
|
#settle() {
|
|
42
98
|
if (this.#settled) return false;
|
|
43
99
|
this.#settled = true;
|
|
@@ -71,6 +127,7 @@ var UpgradeAdmission = class {
|
|
|
71
127
|
}
|
|
72
128
|
refuse(status, headers) {
|
|
73
129
|
if (!this.#settle()) return;
|
|
130
|
+
chosenSubprotocols.delete(this.req);
|
|
74
131
|
refuseUpgrade(this.socket, status, STATUS_CODES[status] ?? "Refused", headers);
|
|
75
132
|
}
|
|
76
133
|
};
|
|
@@ -114,8 +171,9 @@ function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(ws
|
|
|
114
171
|
refuseUpgrade(socket, status, STATUS_CODES[status] ?? "Refused");
|
|
115
172
|
return;
|
|
116
173
|
}
|
|
117
|
-
|
|
118
|
-
|
|
174
|
+
const problem = websocketHandshakeProblem(req.method, req.headers);
|
|
175
|
+
if (problem !== null) {
|
|
176
|
+
refuseMalformedHandshake(socket, problem);
|
|
119
177
|
return;
|
|
120
178
|
}
|
|
121
179
|
admit(req, new UpgradeAdmission(wss, req, socket, head));
|
|
@@ -125,4 +183,4 @@ function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(ws
|
|
|
125
183
|
server.removeListener("upgrade", onUpgrade);
|
|
126
184
|
};
|
|
127
185
|
}
|
|
128
|
-
export { attachUpgradeDispatch };
|
|
186
|
+
export { attachUpgradeDispatch, createNodefonyWebSocketServer, createSubprotocolHandler };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -68,10 +68,16 @@ declare class Http extends Module<IHttpConfig> {
|
|
|
68
68
|
}
|
|
69
69
|
export default Http;
|
|
70
70
|
export { defineHttpConfig, httpConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
|
|
71
|
+
export { parseRequestTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
72
|
+
export { isContentlessStatus } from "./nodefony/src/context/http/Response.js";
|
|
73
|
+
export type { ISplitTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
71
74
|
export { httpConfigSchema } from "./nodefony/config/config.js";
|
|
72
75
|
export type { IHttpConfig, IHttpConfigInput, } from "./nodefony/interfaces/IHttpConfig.js";
|
|
73
76
|
export { decideSend, readBackpressureOptions, type WsBackpressurePolicy, type WsSendDecision, type IWsBackpressureOptions, type IBackpressureSocket, type IBackpressureTarget, } from "./nodefony/src/context/websocket/wsBackpressure.js";
|
|
77
|
+
export { admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter, FRAME_RATE_WINDOW_MS, type IFrameRateWindow, type IWsFrameRateOptions, } from "./nodefony/src/context/websocket/wsFrameRate.js";
|
|
74
78
|
export { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
|
|
79
|
+
export { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
|
|
80
|
+
export type { IRequestTrace, RequestTraceProtocol, } from "./nodefony/interfaces/IRequestTrace.js";
|
|
75
81
|
export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed, } from "./nodefony/src/context/domainMatcher.js";
|
|
76
82
|
export type { DomainPattern, ITrustedHostsConfig, } from "./nodefony/src/context/domainMatcher.js";
|
|
77
83
|
export { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
@@ -79,6 +85,8 @@ export type { IRateLimitStore, IRateLimitOptions, RateLimitVerdict, } from "./no
|
|
|
79
85
|
export { Context, Session, SessionsService, HttpError, HttpKernel, Response, Http2Response, Response as HttpResponse, wsResponse, wsResponse as WebsocketResponse, Cookie, HttpContext, WebsocketContext, HttpRequest, Http2Request, DefaultErrorRenderer, DefaultRequestLogger, JsonAuditLogger, PrettyRequestLogger, Profiler, };
|
|
80
86
|
export { SseStream, openSseStream, acceptsEventStream, SSE_HEARTBEAT_MS, } from "./nodefony/src/context/http/SseStream.js";
|
|
81
87
|
export type { ISseStreamOptions, ISseSendOptions, } from "./nodefony/src/context/http/SseStream.js";
|
|
88
|
+
export { openByteStream } from "./nodefony/src/context/http/ByteStream.js";
|
|
89
|
+
export type { ByteStreamEndReason, ByteStreamSource, IByteStreamOptions, IByteStreamResult, } from "./nodefony/src/context/http/ByteStream.js";
|
|
82
90
|
export { schemaMismatchOf, type SchemaMismatchConfidence, } from "./nodefony/service/error-renderer.js";
|
|
83
91
|
export { toValidationFields, type IValidationField, } from "./nodefony/service/error-renderer.js";
|
|
84
92
|
export type { ProfileEntry, ProfileSummary, ProfilePhase, ProfileQuery, ProfileSecurity, IProfileBatch, IProfilerReader, } from "./nodefony/src/profiler/Profiler.js";
|
|
@@ -115,6 +115,8 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
115
115
|
close: "close";
|
|
116
116
|
drop: "drop";
|
|
117
117
|
}>>;
|
|
118
|
+
maxFramesPerSecond: z.ZodDefault<z.ZodNumber>;
|
|
119
|
+
frameRateCloseAfterRejects: z.ZodDefault<z.ZodNumber>;
|
|
118
120
|
}, z.core.$loose>>;
|
|
119
121
|
websocketSecure: z.ZodDefault<z.ZodObject<{
|
|
120
122
|
keepaliveInterval: z.ZodDefault<z.ZodNumber>;
|
|
@@ -139,6 +141,8 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
139
141
|
close: "close";
|
|
140
142
|
drop: "drop";
|
|
141
143
|
}>>;
|
|
144
|
+
maxFramesPerSecond: z.ZodDefault<z.ZodNumber>;
|
|
145
|
+
frameRateCloseAfterRejects: z.ZodDefault<z.ZodNumber>;
|
|
142
146
|
}, z.core.$loose>>;
|
|
143
147
|
statics: z.ZodDefault<z.ZodObject<{
|
|
144
148
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -198,6 +202,10 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
198
202
|
stripHeaders: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
199
203
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
200
204
|
secure: z.ZodDefault<z.ZodBoolean>;
|
|
205
|
+
accessLog: z.ZodDefault<z.ZodEnum<{
|
|
206
|
+
all: "all";
|
|
207
|
+
errors: "errors";
|
|
208
|
+
}>>;
|
|
201
209
|
}, z.core.$strict>>>;
|
|
202
210
|
}, z.core.$strict>>;
|
|
203
211
|
wsMaxConnectionsPerIp: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
@@ -127,6 +127,15 @@ export interface IContext {
|
|
|
127
127
|
readonly phases: PhaseTiming[];
|
|
128
128
|
phaseStart(name: PhaseName): void;
|
|
129
129
|
phaseEnd(name: PhaseName): void;
|
|
130
|
+
/**
|
|
131
|
+
* Arrête l'horloge de la requête (au premier appel) et rend sa durée en ms —
|
|
132
|
+
* la SEULE durée de requête du framework, mesurée partout par défaut ; `null`
|
|
133
|
+
* seulement si `timing.enabled` vaut `false`.
|
|
134
|
+
*
|
|
135
|
+
* Membre REQUIS : le journal d'accès et le profileur le lisent sans repli.
|
|
136
|
+
* Une implémentation maison d'`IContext` qui ne mesure rien rend `null`.
|
|
137
|
+
*/
|
|
138
|
+
stopClock(): number | null;
|
|
130
139
|
profiling: boolean;
|
|
131
140
|
securityTrace: ISecurityTrace | null;
|
|
132
141
|
onAfterResponse(fn: AfterResponseHandler): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SchemeType } from "./IContext.js";
|
|
2
|
+
import type { HTTPMethodType } from "./IRequest.js";
|
|
3
|
+
/**
|
|
4
|
+
* Nature de l'opération tracée : une requête HTTP, ou le handshake d'une
|
|
5
|
+
* connexion WebSocket.
|
|
6
|
+
*/
|
|
7
|
+
export type RequestTraceProtocol = "http" | "websocket";
|
|
8
|
+
/**
|
|
9
|
+
* Message publié sur le `TracingChannel` `nodefony.http.request`, le même
|
|
10
|
+
* objet de `start` à `asyncEnd` pour une requête donnée.
|
|
11
|
+
*
|
|
12
|
+
* Les champs d'identité (`requestId`, `protocol`, `method`, `scheme`, `url`)
|
|
13
|
+
* sont posés dès `start`. Les champs de résultat (`route`, `routePath`,
|
|
14
|
+
* `module`, `statusCode`, `durationMs`, `error`) valent `null` jusqu'à la fin
|
|
15
|
+
* de la requête : un abonné les lit sur `asyncEnd`. Tout est en lecture seule
|
|
16
|
+
* pour l'abonné — c'est Nodefony qui les remplit.
|
|
17
|
+
*/
|
|
18
|
+
export interface IRequestTrace {
|
|
19
|
+
/** Identifiant de corrélation de la requête (`X-Request-Id` entrant, sinon généré). */
|
|
20
|
+
readonly requestId: string;
|
|
21
|
+
/** `"http"` pour une requête, `"websocket"` pour un handshake. */
|
|
22
|
+
readonly protocol: RequestTraceProtocol;
|
|
23
|
+
/** Méthode de la requête (`"WEBSOCKET"` pour un handshake). */
|
|
24
|
+
readonly method: HTTPMethodType | null;
|
|
25
|
+
/** Schéma vu par le client, proxy de confiance compris (`url.scheme` d'OpenTelemetry). */
|
|
26
|
+
readonly scheme: SchemeType;
|
|
27
|
+
/** URL complète de la requête, chaîne de requête comprise — l'abonné en tire `url.path` et `url.query`. */
|
|
28
|
+
readonly url: string;
|
|
29
|
+
/** Nom de la route qui a répondu, `null` sans route (statique, 404). */
|
|
30
|
+
readonly route: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Gabarit de la route, préfixe compris (`/users/{id}`) : faible cardinalité,
|
|
33
|
+
* c'est l'attribut `http.route` d'OpenTelemetry. La racine vaut `/`. `null`
|
|
34
|
+
* sans route.
|
|
35
|
+
*/
|
|
36
|
+
readonly routePath: string | null;
|
|
37
|
+
/** Module qui a signé la réponse — le même que celui de la ligne d'accès. */
|
|
38
|
+
readonly module: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Statut rendu au client : celui de l'amont pour une requête relayée, `101`
|
|
41
|
+
* pour un handshake accepté, le statut du refus sinon. `null` si le client
|
|
42
|
+
* est parti avant toute réponse.
|
|
43
|
+
*/
|
|
44
|
+
readonly statusCode: number | null;
|
|
45
|
+
/**
|
|
46
|
+
* Durée de la requête en millisecondes, construction du contexte → dernier
|
|
47
|
+
* octet remis (fin du handshake en WebSocket). `null` si `timing.enabled`
|
|
48
|
+
* vaut `false`.
|
|
49
|
+
*/
|
|
50
|
+
readonly durationMs: number | null;
|
|
51
|
+
/**
|
|
52
|
+
* Exception attrapée par le pipeline, `null` sinon. Un statut 4xx rendu
|
|
53
|
+
* sans exception n'en porte pas ; c'est à l'abonné de classer un statut.
|
|
54
|
+
*/
|
|
55
|
+
readonly error: unknown;
|
|
56
|
+
}
|
|
@@ -59,6 +59,14 @@ export interface IProxyMountOptions {
|
|
|
59
59
|
* ACCEPTÉ que vers la boucle locale (refus au montage sinon).
|
|
60
60
|
*/
|
|
61
61
|
secure?: boolean | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Ligne d'accès des requêtes relayées : `"all"` (défaut) les écrit toutes,
|
|
64
|
+
* `"errors"` seulement celles en échec (statut ≥ 400). Pour un amont qui sert
|
|
65
|
+
* des centaines de fichiers par page (le relais de Vite) : un fichier bien
|
|
66
|
+
* servi n'apprend rien, et ses lignes chassaient les autres de la mémoire
|
|
67
|
+
* du journal.
|
|
68
|
+
*/
|
|
69
|
+
accessLog?: "all" | "errors" | undefined;
|
|
62
70
|
}
|
|
63
71
|
/** Un montage tel que le proxy le garde : préfixe normalisé + réglages. */
|
|
64
72
|
export interface IProxyMount extends IProxyMountOptions {
|
|
@@ -11,6 +11,15 @@ import type { ContextType } from "../service/http-kernel.js";
|
|
|
11
11
|
export interface IResolvedRoute {
|
|
12
12
|
/** Nom de la route (journalisation, diagnostic). */
|
|
13
13
|
readonly name: string;
|
|
14
|
+
/**
|
|
15
|
+
* Gabarit de la route, préfixe compris (`/users/{id}`) — faible cardinalité,
|
|
16
|
+
* publié comme `routePath` sur le canal `nodefony.http.request`.
|
|
17
|
+
*/
|
|
18
|
+
readonly path?: string | undefined;
|
|
19
|
+
/** Module propriétaire — il signe la ligne d'accès de la requête. */
|
|
20
|
+
readonly module?: {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
} | undefined;
|
|
14
23
|
/** Corps laissé en flux (`@Body({ stream: true })`) : le parse est sauté. */
|
|
15
24
|
readonly bodyStream?: boolean | undefined;
|
|
16
25
|
}
|
|
@@ -35,6 +44,13 @@ export interface IRouteResolver {
|
|
|
35
44
|
} | null | undefined;
|
|
36
45
|
/** Nom de l'action résolue. */
|
|
37
46
|
actionName?: string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Ce que l'action a rendu sans que rien ne soit envoyé (`"null"`,
|
|
49
|
+
* `"undefined"`, `"une instance de Book"`…), `null` sinon. Posé seulement
|
|
50
|
+
* sur la branche « l'action répond elle-même » : lu au démontage pour dire,
|
|
51
|
+
* en développement, POURQUOI une requête a attendu son délai d'expiration.
|
|
52
|
+
*/
|
|
53
|
+
readonly unrendered?: string | null | undefined;
|
|
38
54
|
/**
|
|
39
55
|
* Rejoue le match d'une route sur un contexte (WS : handshake puis frames).
|
|
40
56
|
*
|
|
@@ -40,12 +40,37 @@ export interface IFirewallGate {
|
|
|
40
40
|
enforceCsrf(context: ContextType): void;
|
|
41
41
|
/** Répond au preflight CORS ; `204` quand la réponse est déjà envoyée. */
|
|
42
42
|
handleCors(context: ContextType): number | undefined;
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Pose les en-têtes de sécurité applicatifs (CSP, Permissions-Policy…) de la
|
|
45
|
+
* réponse, appelé par la réponse juste avant son `writeHead`.
|
|
46
|
+
*
|
|
47
|
+
* @param contentType - `Content-Type` de la réponse, qui choisit la politique ;
|
|
48
|
+
* `null` = réponse sans contenu ; omis = type inconnu (politique complète).
|
|
49
|
+
* @param servedAsFile - corps servi tel quel (fichier statique, relais) : aucun
|
|
50
|
+
* gabarit ne l'a écrit, il ne peut pas porter de nonce.
|
|
51
|
+
*/
|
|
52
|
+
applySecurityHeaders(context: ContextType, contentType?: string | null, servedAsFile?: boolean): void;
|
|
45
53
|
/**
|
|
46
54
|
* Le revalidateur de l'identité authentifiée de la requête courante, pour une
|
|
47
55
|
* connexion longue qui la gardera (flux SSE) : la même règle que les sockets
|
|
48
56
|
* WebSocket. `null` pour un anonyme — rien à révoquer.
|
|
49
57
|
*/
|
|
50
58
|
currentRevalidator?(): ((nowMs?: number) => Promise<boolean>) | null;
|
|
59
|
+
/**
|
|
60
|
+
* Prévenu quand le noyau HTTP a recalculé ses en-têtes de transport (édition à
|
|
61
|
+
* chaud de `securityHeaders`) : la politique de contenu suit `frameOptions`,
|
|
62
|
+
* qui pilote aussi `frame-ancestors`.
|
|
63
|
+
*/
|
|
64
|
+
onTransportHeadersChanged?(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Déclare les sources qu'un module ajoute à la politique de contenu
|
|
67
|
+
* (`{ "connect-src": ["wss://rt.example.com"] }`). Un second appel du même
|
|
68
|
+
* module REMPLACE sa contribution. Absent quand aucun pare-feu ne pose de CSP.
|
|
69
|
+
*
|
|
70
|
+
* @param moduleName - module qui contribue, clé de sa contribution
|
|
71
|
+
* @param fragment - sources par directive, origines exactes (sans joker)
|
|
72
|
+
*/
|
|
73
|
+
registerCspOrigins?(moduleName: string, fragment: Readonly<Record<string, readonly string[]>>): void;
|
|
74
|
+
/** Retire la contribution CSP de `moduleName`. */
|
|
75
|
+
unregisterCspOrigins?(moduleName: string): void;
|
|
51
76
|
}
|
|
@@ -7,4 +7,5 @@ export type { IContext, IHttpContext, IWebsocketContext, ServerType, ProtocolNam
|
|
|
7
7
|
export type { IHttpKernel } from "./IHttpKernel.js";
|
|
8
8
|
export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./IRouting.js";
|
|
9
9
|
export type { ISecurityZone, IFirewallGate } from "./ISecurity.js";
|
|
10
|
+
export type { IRequestTrace, RequestTraceProtocol } from "./IRequestTrace.js";
|
|
10
11
|
export type { IProxyMount, IProxyMountOptions, ProxyTarget, } from "./IReverseProxy.js";
|
|
@@ -10,7 +10,11 @@ import type { Profiler } from "../src/profiler/Profiler.js";
|
|
|
10
10
|
*
|
|
11
11
|
* Endpoints :
|
|
12
12
|
* - `GET /nodefony/profiler/api/recent` → derniers profils (résumés, récent → ancien)
|
|
13
|
-
*
|
|
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
|
|
14
18
|
* - `DELETE /nodefony/profiler/api/recent` → vide le ring buffer
|
|
15
19
|
*
|
|
16
20
|
* La debug bar (toute page, dev) lit `X-Request-Id` de SON appel AJAX puis
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Severity } from "nodefony";
|
|
2
2
|
import type { IRequestLogger, IRequestLogEntry } from "../interfaces/IRequestLogger.js";
|
|
3
|
-
import type { IHttpContext, IWebsocketContext
|
|
3
|
+
import type { IContext, IHttpContext, IWebsocketContext } from "../interfaces/IContext.js";
|
|
4
|
+
declare const statusOf: (context: IContext) => number | null;
|
|
4
5
|
/**
|
|
5
6
|
* Canonical audit log entry — 1 JSON PDU per request (P3.1).
|
|
6
7
|
*
|
|
@@ -52,15 +53,6 @@ export interface AuditErrorEntry {
|
|
|
52
53
|
/** Recursive — capped to depth 5 to avoid pathological cycles. */
|
|
53
54
|
cause?: AuditErrorEntry;
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Durée totale de la requête, de la première phase (`startMs`) à maintenant.
|
|
57
|
-
* `null` si le chronométrage est coupé (aucune phase enregistrée). Partagée
|
|
58
|
-
* par le journal d'audit et le journal lisible — une seule définition.
|
|
59
|
-
*
|
|
60
|
-
* @param phases - phases chronométrées du contexte
|
|
61
|
-
* @returns durée en millisecondes, ou `null`
|
|
62
|
-
*/
|
|
63
|
-
declare function computeDurationMs(phases: PhaseTiming[]): number | null;
|
|
64
56
|
/**
|
|
65
57
|
* Severity derived from HTTP status code — RFC 9110 categories.
|
|
66
58
|
* 1xx/2xx/3xx → INFO ; 4xx → WARNING ; 5xx → ERROR.
|
|
@@ -145,4 +137,4 @@ declare class JsonAuditLogger implements IRequestLogger {
|
|
|
145
137
|
private serializeError;
|
|
146
138
|
}
|
|
147
139
|
export default JsonAuditLogger;
|
|
148
|
-
export {
|
|
140
|
+
export { severityFromStatus, statusOf };
|
|
@@ -34,10 +34,11 @@ export declare function toValidationFields(error: Error): IValidationField[] | n
|
|
|
34
34
|
/**
|
|
35
35
|
* Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier)
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Trois sources le disent : MongoDB par une `CastError` de Mongoose (la clé est
|
|
38
|
+
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier), et
|
|
39
|
+
* le kit de colonnes portable par une `ColumnValueError` de genre `uuid` ou
|
|
40
|
+
* `integer` — il refuse sur les trois moteurs la clé que PostgreSQL refuserait. Une clé rangée en
|
|
41
|
+
* texte NATIF ne lève rien, la lecture rend « rien » : le même 404.
|
|
41
42
|
*
|
|
42
43
|
* Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
|
|
43
44
|
*
|
|
@@ -134,6 +134,8 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
134
134
|
private errorRenderer;
|
|
135
135
|
private requestLogger;
|
|
136
136
|
private profiler;
|
|
137
|
+
/** `timing.tooling` : profiler aussi l'outillage (lu une fois, à onReady). */
|
|
138
|
+
private profileTooling;
|
|
137
139
|
private _trustProxyChecker;
|
|
138
140
|
private _wsOriginPolicy;
|
|
139
141
|
private rateLimiter;
|
|
@@ -145,11 +147,20 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
145
147
|
* dans `onHttpRequest` (0 alloc/concat par requête).
|
|
146
148
|
*/
|
|
147
149
|
private computeSecurityHeaderCaches;
|
|
150
|
+
/**
|
|
151
|
+
* En-têtes du transport d'une réponse, figés au démarrage — ce que
|
|
152
|
+
* `HttpResponse` place en tête de sa réserve.
|
|
153
|
+
*
|
|
154
|
+
* @param type - famille du serveur qui a reçu la requête.
|
|
155
|
+
* @returns la liste plate `[nom, valeur, …]`, noms en minuscules.
|
|
156
|
+
*/
|
|
157
|
+
transportHeaders(type: ServerType): readonly string[];
|
|
148
158
|
/**
|
|
149
159
|
* Ce que ce kernel ÉMET réellement comme en-têtes de sécurité transport.
|
|
150
160
|
*
|
|
151
161
|
* Ces trois en-têtes (HSTS, X-Frame-Options, X-Content-Type-Options) sont posés
|
|
152
|
-
*
|
|
162
|
+
* par `@nodefony/http` sur TOUTE réponse (en tête de la réserve de `HttpResponse`),
|
|
163
|
+
* statiques et pages d'erreur comprises
|
|
153
164
|
* — pas par `@nodefony/security`, dont les clés homonymes sont inertes. Toute
|
|
154
165
|
* console d'administration doit donc lire CE point, sinon elle affiche une
|
|
155
166
|
* intention de configuration au lieu de l'état appliqué.
|
|
@@ -186,6 +197,32 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
186
197
|
* tel quel plutôt que d'inventer un état.
|
|
187
198
|
*/
|
|
188
199
|
get rateLimitStore(): IRateLimitStore | null;
|
|
200
|
+
/**
|
|
201
|
+
* Cette requête — ou cette frame du pont — se profile-t-elle ? Jamais sans
|
|
202
|
+
* profileur (production). L'outillage (console, profileur, journaux, Vite :
|
|
203
|
+
* `isProfiledUrl`, sur la règle `isConsolePath` du cœur) seulement sous
|
|
204
|
+
* `timing.tooling` — on ne
|
|
205
|
+
* mesure pas la façon dont on mesure : ni tampon SQL, ni instantané, ni place
|
|
206
|
+
* dans les anneaux du profileur.
|
|
207
|
+
*
|
|
208
|
+
* @param url - URL ou chemin de la requête
|
|
209
|
+
* @returns `true` quand le profil doit être pris
|
|
210
|
+
*/
|
|
211
|
+
shouldProfile(url: string): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* En développement, signale une action qui a rendu une valeur que le
|
|
214
|
+
* Resolver ne sait pas envoyer (`null`, `undefined`, instance de classe)
|
|
215
|
+
* et n'a rien envoyé elle-même : la requête attend son délai d'expiration.
|
|
216
|
+
* Le message nomme l'action, ce qu'elle a rendu et le geste qui convient
|
|
217
|
+
* (`unsentResponseWarning`).
|
|
218
|
+
*
|
|
219
|
+
* Appelé au timeout, AVANT que le 504/408 parte — après, `sended` est vrai et
|
|
220
|
+
* plus rien ne se signalerait — et au démontage, pour un client parti avant.
|
|
221
|
+
* Gratuit en production : rien n'est lu au-delà de `waitAsync`.
|
|
222
|
+
*
|
|
223
|
+
* @param context - le contexte HTTP de la requête
|
|
224
|
+
*/
|
|
225
|
+
warnUnsentResponse(context: HttpContext): void;
|
|
189
226
|
private configureRateLimit;
|
|
190
227
|
/**
|
|
191
228
|
* (Re)configure le backstop OPT-IN de connexions WS concurrentes par IP (F6c)
|
|
@@ -474,6 +511,18 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
474
511
|
* @returns le contexte servi, ou sa promesse.
|
|
475
512
|
*/
|
|
476
513
|
handleHttp(scope: Scope, request: httpRequest, response: httpResponse, type: ServerType): MaybePromise<HttpContext>;
|
|
514
|
+
/**
|
|
515
|
+
* Sert un contexte HTTP fraîchement créé : le crochet `onCreateContext`
|
|
516
|
+
* s'il a des écouteurs, puis le pipeline.
|
|
517
|
+
*
|
|
518
|
+
* Hot path : `fireAsync` est une fonction async → 1 Promise + 1 microtask
|
|
519
|
+
* MÊME à 0 listener. En prod sans @nodefony/security ces seams
|
|
520
|
+
* (onCreateContext/beforeResolve/afterAuth) n'ont aucun listener → garde
|
|
521
|
+
* `listenerCount` (O(1), 0 alloc) pour ne RIEN planifier.
|
|
522
|
+
*
|
|
523
|
+
* @returns le contexte servi, ou sa promesse
|
|
524
|
+
*/
|
|
525
|
+
private dispatchHttpContext;
|
|
477
526
|
/**
|
|
478
527
|
* Traceparent, tampon du profileur, puis le pipeline dans la bulle ALS de la
|
|
479
528
|
* requête (`RequestContext.run`).
|
|
@@ -482,6 +531,10 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
482
531
|
/**
|
|
483
532
|
* Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
|
|
484
533
|
* n'a pas pu se construire, puis rend l'erreur au client (`onError`).
|
|
534
|
+
*
|
|
535
|
+
* Sans contexte, `onError` n'a rien sur quoi écrire : la réponse BRUTE
|
|
536
|
+
* reçoit alors un 500 nu. Sinon HTTP/1.1 coupait la connexion sans un octet
|
|
537
|
+
* et HTTP/2 laissait le flux pendu.
|
|
485
538
|
*/
|
|
486
539
|
private failHttp;
|
|
487
540
|
/**
|
|
@@ -578,6 +631,16 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
578
631
|
* @returns ce que rend l'action de handshake
|
|
579
632
|
*/
|
|
580
633
|
handleWebsocket(scope: Scope, req: IncomingMessage, admission: IUpgradeAdmission, type: ServerType): Promise<unknown>;
|
|
634
|
+
/**
|
|
635
|
+
* Fin du canal `nodefony.http.request` pour un handshake interrompu par une
|
|
636
|
+
* exception — sans effet si un refus l'a déjà publiée (`refuseHandshake`).
|
|
637
|
+
* Le statut est un fait, pas une déduction : une socket rattachée dit que
|
|
638
|
+
* le `101` est parti ; sinon c'est le `500` du filet qui a été rendu.
|
|
639
|
+
*
|
|
640
|
+
* @param context - le contexte du handshake, `null` s'il n'a pu être construit
|
|
641
|
+
* @param error - l'exception qui a interrompu le pipeline
|
|
642
|
+
*/
|
|
643
|
+
private closeFailedWebsocketTrace;
|
|
581
644
|
private releaseOrphanWsScope;
|
|
582
645
|
/**
|
|
583
646
|
* Juge un handshake WebSocket AVANT son acceptation : hôte, origine,
|
|
@@ -665,7 +728,7 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
665
728
|
once(event: "close", listener: () => void): unknown;
|
|
666
729
|
}): boolean;
|
|
667
730
|
/**
|
|
668
|
-
* Re-valide tous les flux
|
|
731
|
+
* Re-valide tous les flux en cours et ferme ceux dont l'identité est morte.
|
|
669
732
|
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
670
733
|
*/
|
|
671
734
|
revalidateStreams(): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Duplex } from "node:stream";
|
|
2
2
|
/**
|
|
3
|
-
* Gère l'event `clientError` d'un `http.Server` / `https.Server
|
|
3
|
+
* Gère l'event `clientError` d'un `http.Server` / `https.Server`, et des connexions
|
|
4
|
+
* HTTP/1.1 d'un serveur HTTP/2 à `allowHTTP1`.
|
|
4
5
|
*
|
|
5
6
|
* ⚠️ Doc Node (`http.Server` event `'clientError'`) : dès qu'un listener
|
|
6
7
|
* `clientError` est attaché, Node **cesse de fermer le socket automatiquement**
|
|
@@ -11,7 +12,7 @@ import type { Duplex } from "node:stream";
|
|
|
11
12
|
* @param error - erreur émise (codes `llhttp` : `HPE_*`, ou `ECONNRESET`…).
|
|
12
13
|
* @param socket - flux client brut (l'event `clientError` fournit un `Duplex`).
|
|
13
14
|
*/
|
|
14
|
-
export declare function handleClientError(error:
|
|
15
|
+
export declare function handleClientError(error: Error, socket: Duplex): void;
|
|
15
16
|
/**
|
|
16
17
|
* Gère l'event `sessionError` d'un serveur HTTP/2 (h2c ou TLS) : un pair PARTI
|
|
17
18
|
* se tait, toute autre erreur est rapportée.
|
|
@@ -16,9 +16,11 @@ export interface IDrainTerminator {
|
|
|
16
16
|
* Fabrique l'arrêt gracieux d'un serveur HTTP/1.1, HTTPS, HTTP/2 sur TLS ou h2c.
|
|
17
17
|
*
|
|
18
18
|
* Au `terminate()` (SIGTERM, `docker stop`), le serveur cesse d'accepter, puis :
|
|
19
|
-
* - **HTTP/2** : chaque session reçoit un GOAWAY
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* - **HTTP/2** : chaque session reçoit un GOAWAY en deux temps — d'abord à
|
|
20
|
+
* `2^31-1` ({@link announceShutdown}), puis, un aller-retour plus tard, le
|
|
21
|
+
* définitif (`session.close()`). Les flux en vol se terminent, les nouveaux
|
|
22
|
+
* sont refusés, et le client sait qu'il doit rouvrir ailleurs au lieu de
|
|
23
|
+
* constater une coupure ;
|
|
22
24
|
* - **HTTP/1.1** : la réponse en vol part avec `Connection: close`, la socket
|
|
23
25
|
* inactive est fermée tout de suite, une requête qui arrive encore sur une
|
|
24
26
|
* socket gardée ouverte reçoit aussi `Connection: close`.
|
|
@@ -67,8 +67,8 @@ export interface IPrebuiltUiOptions {
|
|
|
67
67
|
* requête. Le SPA fallback reste au controller du module (routes littérales,
|
|
68
68
|
* cf. StudioController).
|
|
69
69
|
*
|
|
70
|
-
* Perf : l'index est lu UNE fois (lazy, caché)
|
|
71
|
-
*
|
|
70
|
+
* Perf : l'index est lu UNE fois (lazy, caché) et découpé UNE fois autour des
|
|
71
|
+
* emplacements du nonce ; un rendu ne coûte qu'un `join`.
|
|
72
72
|
*/
|
|
73
73
|
export declare class PrebuiltUi {
|
|
74
74
|
readonly publicPath: string;
|
|
@@ -76,6 +76,8 @@ export declare class PrebuiltUi {
|
|
|
76
76
|
private readonly indexPath;
|
|
77
77
|
/** Template HTML caché — `null` tant que non lu (lazy). */
|
|
78
78
|
private template;
|
|
79
|
+
/** Index découpé autour des emplacements du nonce — `null` tant que non calculé. */
|
|
80
|
+
private nonceParts;
|
|
79
81
|
constructor(opts: IPrebuiltUiOptions);
|
|
80
82
|
/**
|
|
81
83
|
* Monte `distDir` sous `publicPath` auprès de `server-static` (résolu par
|
|
@@ -90,8 +92,11 @@ export declare class PrebuiltUi {
|
|
|
90
92
|
* Document HTML d'entrée de l'UI (index pré-buildé par Vite).
|
|
91
93
|
*
|
|
92
94
|
* @param nonce nonce CSP de la requête (`Context.cspNonce`) — injecté sur
|
|
93
|
-
* chaque `<script
|
|
94
|
-
*
|
|
95
|
+
* chaque `<script>`, `src` compris : sous `'strict-dynamic'`
|
|
96
|
+
* (ADR-0024 §C1), `'self'` est ignoré et seul le nonce autorise
|
|
97
|
+
* une balise ; ce qu'elle importe ensuite hérite de la confiance.
|
|
98
|
+
* Annoncé aussi par `<meta property="csp-nonce">` en tête de
|
|
99
|
+
* `<head>` (ADR-0024 §C2) : les `<style>` créés à l'exécution.
|
|
95
100
|
* @returns le HTML, ou un commentaire HTML fail-loud si l'index est absent
|
|
96
101
|
*/
|
|
97
102
|
renderIndex(nonce?: string): string;
|