@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +12 -10
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/service/HttpAdminApi.js +2 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +20 -13
- package/dist/nodefony/service/certificates.js +97 -13
- package/dist/nodefony/service/error-renderer.js +29 -12
- package/dist/nodefony/service/http-kernel.js +118 -43
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +3 -3
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
- package/dist/nodefony/service/servers/server-websocket.js +10 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
- package/dist/nodefony/service/sessions/sessions-service.js +7 -14
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +23 -31
- package/dist/nodefony/src/context/domainMatcher.js +1 -2
- package/dist/nodefony/src/context/forwarded.js +1 -2
- package/dist/nodefony/src/context/http/HttpContext.js +7 -6
- package/dist/nodefony/src/context/http/Request.js +33 -29
- package/dist/nodefony/src/context/http/Response.js +23 -6
- package/dist/nodefony/src/context/http/SseStream.js +1 -2
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +1 -2
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +33 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +102 -20
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +29 -5
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +12 -6
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
- package/dist/nodefony/src/session/session.js +2 -3
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +7 -3
- package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -2
- package/dist/types/nodefony/service/certificates.d.ts +55 -4
- package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/src/context/Context.d.ts +13 -4
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
- package/docs/cookies.md +85 -78
- package/docs/observabilite.md +80 -80
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +54 -54
- package/docs/servers.md +338 -224
- package/docs/session.md +179 -179
- package/docs/sse.md +24 -20
- package/docs/upload.md +120 -117
- package/package.json +8 -9
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -4,7 +4,7 @@ import { toWsCloseReason } from "./wsCloseReason.js";
|
|
|
4
4
|
import { RequestContext } from "nodefony";
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import Ws from "ws";
|
|
7
|
-
|
|
7
|
+
// nodefony/src/context/websocket/Response.ts
|
|
8
8
|
/**
|
|
9
9
|
* Codes d'erreur d'écriture « le client est PARTI » : reload de page, onglet
|
|
10
10
|
* fermé, HMR — la socket TCP est morte côté client pendant qu'une frame était
|
|
@@ -71,7 +71,11 @@ var WebsocketResponse = class {
|
|
|
71
71
|
const payload = data ?? this.body;
|
|
72
72
|
if (payload == null) throw new Error("no data");
|
|
73
73
|
return new Promise((resolve, reject) => {
|
|
74
|
-
if (!this.connection || this.connection.readyState
|
|
74
|
+
if (!this.connection || this.connection.readyState === Ws.CONNECTING) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
|
|
75
|
+
if (this.connection.readyState !== Ws.OPEN) {
|
|
76
|
+
this.log("WS send: client déjà déconnecté — frame perdue", "DEBUG");
|
|
77
|
+
return resolve(this);
|
|
78
|
+
}
|
|
75
79
|
const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context.server);
|
|
76
80
|
const decision = decideSend(this.connection, max, policy, closeAfterDrops);
|
|
77
81
|
if (decision !== "send") {
|
|
@@ -122,7 +126,7 @@ var WebsocketResponse = class {
|
|
|
122
126
|
setBody(ele, encoding) {
|
|
123
127
|
if (typeof ele === "string") this.body = Buffer.from(ele, encoding ?? this.encoding);
|
|
124
128
|
else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
|
|
125
|
-
else if (
|
|
129
|
+
else if (ArrayBuffer.isView(ele)) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
|
|
126
130
|
return this.body;
|
|
127
131
|
}
|
|
128
132
|
drop(reasonCode, description) {
|
|
@@ -176,14 +180,38 @@ var WebsocketResponse = class {
|
|
|
176
180
|
clean() {
|
|
177
181
|
this.connection = null;
|
|
178
182
|
this.body = null;
|
|
183
|
+
this.handshakeHeaders = null;
|
|
179
184
|
}
|
|
180
185
|
setEncoding(encoding) {
|
|
181
186
|
return this.encoding = encoding;
|
|
182
187
|
}
|
|
183
|
-
|
|
188
|
+
/**
|
|
189
|
+
* En-têtes de la réponse au handshake, retenus tant que la socket n'est pas
|
|
190
|
+
* acceptée : ils ne partent que dans un REFUS (`refuseHandshake`) — le `101`
|
|
191
|
+
* de `ws` ne les porte pas. `null` tant que personne n'en pose (le cas
|
|
192
|
+
* nominal : aucune allocation).
|
|
193
|
+
*/
|
|
194
|
+
handshakeHeaders = null;
|
|
195
|
+
/**
|
|
196
|
+
* Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
|
|
197
|
+
* une fois la socket ouverte, où il n'y a plus de réponse HTTP.
|
|
198
|
+
*
|
|
199
|
+
* @param name - nom de l'en-tête
|
|
200
|
+
* @param value - valeur
|
|
201
|
+
* @returns toujours `true`
|
|
202
|
+
*/
|
|
203
|
+
setHeader(name, value) {
|
|
204
|
+
if (this.connection !== null) return true;
|
|
205
|
+
this.handshakeHeaders ??= {};
|
|
206
|
+
this.handshakeHeaders[name] = typeof value === "object" ? value.join(", ") : value;
|
|
184
207
|
return true;
|
|
185
208
|
}
|
|
186
|
-
setHeaders() {
|
|
209
|
+
setHeaders(obj) {
|
|
210
|
+
if (this.connection !== null) return true;
|
|
211
|
+
for (const name of Object.keys(obj)) {
|
|
212
|
+
const value = obj[name];
|
|
213
|
+
if (value !== void 0) this.setHeader(name, value);
|
|
214
|
+
}
|
|
187
215
|
return true;
|
|
188
216
|
}
|
|
189
217
|
addCookie(cookie) {
|
|
@@ -193,5 +221,4 @@ var WebsocketResponse = class {
|
|
|
193
221
|
setCookies() {}
|
|
194
222
|
setCookie(_cookie) {}
|
|
195
223
|
};
|
|
196
|
-
//#endregion
|
|
197
224
|
export { WebsocketResponse as default, isPeerGoneError };
|
|
@@ -7,10 +7,10 @@ import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
|
7
7
|
import { formatWsLogContent } from "./wsLogContent.js";
|
|
8
8
|
import { parseWebsocketUrl } from "./websocketUrl.js";
|
|
9
9
|
import { RequestContext, logColor } from "nodefony";
|
|
10
|
-
import Ws from "ws";
|
|
11
10
|
import { AsyncResource } from "node:async_hooks";
|
|
12
11
|
import url, { URL } from "node:url";
|
|
13
|
-
|
|
12
|
+
import Ws from "ws";
|
|
13
|
+
// nodefony/src/context/websocket/WebsocketContext.ts
|
|
14
14
|
/**
|
|
15
15
|
* Appelle l'action résolue et rend TOUJOURS une promesse.
|
|
16
16
|
*
|
|
@@ -54,6 +54,51 @@ function toWsCloseCode(code) {
|
|
|
54
54
|
return 1011;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
+
* Statut HTTP d'un handshake REFUSÉ avant le `101` (RFC 6455 §4.2.2) — la
|
|
58
|
+
* réciproque de {@link toWsCloseCode} : une erreur levée pendant la décision
|
|
59
|
+
* porte un code HTTP (pare-feu, garde) ou, par héritage, un code de fermeture
|
|
60
|
+
* WS (origine `1008`, protocole `1002`, introuvable `4004`) ; le client qui
|
|
61
|
+
* lit le statut doit y reconnaître le même sens.
|
|
62
|
+
*
|
|
63
|
+
* @param code - code porté par l'erreur
|
|
64
|
+
* @returns un statut HTTP 4xx/5xx
|
|
65
|
+
*/
|
|
66
|
+
function toHandshakeStatus(code) {
|
|
67
|
+
if (typeof code !== "number" || !Number.isInteger(code)) return 500;
|
|
68
|
+
if (code >= 400 && code <= 599) return code;
|
|
69
|
+
switch (code) {
|
|
70
|
+
case 1002:
|
|
71
|
+
case 1003:
|
|
72
|
+
case 1007: return 400;
|
|
73
|
+
case 1008:
|
|
74
|
+
case 4003: return 403;
|
|
75
|
+
case 4001: return 401;
|
|
76
|
+
case 1009: return 413;
|
|
77
|
+
case 1013: return 503;
|
|
78
|
+
case 4004: return 404;
|
|
79
|
+
default: return 500;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Statut HTTP que reçoit le client d'un handshake refusé — ou `null` si la
|
|
84
|
+
* connexion était déjà acceptée.
|
|
85
|
+
*
|
|
86
|
+
* La ligne de journal d'un refus s'écrit AVANT `refuseHandshake` : sans ce
|
|
87
|
+
* calcul, elle affichait le code de fermeture par défaut (`1000`) en ERROR,
|
|
88
|
+
* pour un `400` que le client reçoit. Même conversion que le refus
|
|
89
|
+
* ({@link toHandshakeStatus}), donc même chiffre. Chemin d'ERREUR seulement :
|
|
90
|
+
* 1 comparaison et 1 `typeof`, aucune allocation.
|
|
91
|
+
*
|
|
92
|
+
* @param context - le contexte WS de la ligne
|
|
93
|
+
* @param error - l'erreur qui a interrompu le handshake
|
|
94
|
+
* @returns le statut HTTP du refus, ou `null` après l'acceptation
|
|
95
|
+
*/
|
|
96
|
+
function handshakeRefusalStatus(context, error) {
|
|
97
|
+
if (context.webSocketState !== "handshake") return null;
|
|
98
|
+
const code = error.code;
|
|
99
|
+
return toHandshakeStatus(typeof code === "number" ? code : void 0);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
57
102
|
* Drapeau « logger le CONTENU des messages WS » — résolu **une seule fois** au
|
|
58
103
|
* premier message (kernel présent). `true` hors production : le Suivi de requête
|
|
59
104
|
* (Studio) affiche alors les frames RECEIVE/SEND/BROADCAST. En prod → `false` →
|
|
@@ -68,16 +113,21 @@ var WebsocketContext = class extends Context {
|
|
|
68
113
|
rejected = false;
|
|
69
114
|
teardownWired = false;
|
|
70
115
|
connection = null;
|
|
116
|
+
/**
|
|
117
|
+
* Le verdict du handshake encore à rendre : posé par le noyau tant que le
|
|
118
|
+
* pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
|
|
119
|
+
* est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
|
|
120
|
+
*/
|
|
121
|
+
admission = null;
|
|
71
122
|
origin;
|
|
72
123
|
proxy = null;
|
|
73
124
|
forwarded = null;
|
|
74
125
|
wsUrl = null;
|
|
75
|
-
|
|
76
|
-
queryRequest = {};
|
|
126
|
+
query = {};
|
|
77
127
|
wsPath = "";
|
|
78
128
|
#frameSeq = 0;
|
|
79
|
-
constructor(scope, req, ws,
|
|
80
|
-
super(scope,
|
|
129
|
+
constructor(scope, req, ws, serverType) {
|
|
130
|
+
super(scope, serverType);
|
|
81
131
|
this.webSocketState = "handshake";
|
|
82
132
|
this.request = req;
|
|
83
133
|
this.connection = ws;
|
|
@@ -89,18 +139,16 @@ var WebsocketContext = class extends Context {
|
|
|
89
139
|
if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
|
|
90
140
|
this.remoteAddress = this.getRemoteAddress();
|
|
91
141
|
this.acceptedProtocol = req.headers["sec-websocket-protocol"];
|
|
92
|
-
this.scheme =
|
|
142
|
+
this.scheme = serverType === "wss" ? "wss" : "ws";
|
|
93
143
|
const incomingId = sanitizeRequestId(req.headers["x-request-id"]);
|
|
94
144
|
if (incomingId) this.requestId = incomingId;
|
|
95
145
|
this.wsUrl = parseWebsocketUrl(this.scheme, req.headers.host, req.url);
|
|
96
|
-
this.
|
|
97
|
-
this.queryRequest = { ...this.queryGet };
|
|
146
|
+
this.query = Object.fromEntries(this.wsUrl.searchParams.entries());
|
|
98
147
|
this.wsPath = this.wsUrl.pathname + this.wsUrl.search;
|
|
99
148
|
this.url = url.format(this.wsUrl);
|
|
100
|
-
this.port = parseInt(this.wsUrl.port, 10) || (
|
|
149
|
+
this.port = parseInt(this.wsUrl.port, 10) || (serverType === "wss" ? 443 : 80);
|
|
101
150
|
this.request.url = this.wsUrl;
|
|
102
|
-
this.request.
|
|
103
|
-
this.request.query = this.queryRequest;
|
|
151
|
+
this.request.query = this.query;
|
|
104
152
|
this.request.path = this.wsPath;
|
|
105
153
|
try {
|
|
106
154
|
this.originUrl = new URL(this.origin);
|
|
@@ -141,6 +189,34 @@ var WebsocketContext = class extends Context {
|
|
|
141
189
|
return;
|
|
142
190
|
}
|
|
143
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Rattache la socket que le handshake vient d'ouvrir (`101` envoyé) : le
|
|
194
|
+
* contexte a été construit et jugé AVANT, sans elle.
|
|
195
|
+
*
|
|
196
|
+
* @param ws - la socket acceptée
|
|
197
|
+
*/
|
|
198
|
+
acceptConnection(ws) {
|
|
199
|
+
this.admission = null;
|
|
200
|
+
this.connection = ws;
|
|
201
|
+
this.response?.setConnection(ws);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Refuse le handshake par un statut HTTP, avant tout `101` (RFC 6455
|
|
205
|
+
* §4.2.2) — avec les en-têtes posés pendant la décision (`WWW-Authenticate`
|
|
206
|
+
* du pare-feu, `Retry-After`).
|
|
207
|
+
*
|
|
208
|
+
* @param status - statut HTTP du refus
|
|
209
|
+
* @returns `false` si le verdict était déjà rendu (rien n'est écrit)
|
|
210
|
+
*/
|
|
211
|
+
refuseHandshake(status) {
|
|
212
|
+
const admission = this.admission;
|
|
213
|
+
if (admission === null) return false;
|
|
214
|
+
this.admission = null;
|
|
215
|
+
this.rejected = true;
|
|
216
|
+
this.webSocketState = "error";
|
|
217
|
+
admission.refuse(status, this.response?.handshakeHeaders ?? null);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
144
220
|
async connect() {
|
|
145
221
|
if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
|
|
146
222
|
if (this.rejected) throw new Error("Nodefony Websocket rejected");
|
|
@@ -337,12 +413,12 @@ var WebsocketContext = class extends Context {
|
|
|
337
413
|
}
|
|
338
414
|
onClose(code, reason) {
|
|
339
415
|
const description = reason.toString();
|
|
340
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.
|
|
416
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.serverType} ${logColor.magenta(code)} CLOSE ${this.method}`);
|
|
341
417
|
if (this.connection?.readyState !== Ws.CLOSED) {
|
|
342
418
|
try {
|
|
343
419
|
this.response?.drop(code, description);
|
|
344
420
|
} catch (e) {
|
|
345
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.
|
|
421
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.serverType} CLOSE ${logColor.red(this.method ?? "")}`);
|
|
346
422
|
}
|
|
347
423
|
this.fire("onClose", code, description, this.connection);
|
|
348
424
|
} else this.fire("onClose", code, description, this.connection);
|
|
@@ -359,7 +435,7 @@ var WebsocketContext = class extends Context {
|
|
|
359
435
|
*/
|
|
360
436
|
onConnectionError(error) {
|
|
361
437
|
this.webSocketState = "error";
|
|
362
|
-
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.
|
|
438
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error.message}`, "ERROR", `${this.serverType} ${logColor.red("SOCKET ERROR")} ${this.method}`);
|
|
363
439
|
this.fire("onError", error, this);
|
|
364
440
|
}
|
|
365
441
|
setScheme() {
|
|
@@ -417,5 +493,4 @@ var WebsocketContext = class extends Context {
|
|
|
417
493
|
this.rejected = true;
|
|
418
494
|
}
|
|
419
495
|
};
|
|
420
|
-
|
|
421
|
-
export { WebsocketContext as default, toWsCloseCode };
|
|
496
|
+
export { WebsocketContext as default, handshakeRefusalStatus, toHandshakeStatus, toWsCloseCode };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// nodefony/src/context/websocket/isWebsocketContext.ts
|
|
2
|
+
/**
|
|
3
|
+
* Dit si un contexte sert une connexion WebSocket — la garde qui rétrécit
|
|
4
|
+
* `ContextType` à `WebsocketContext` sans conversion.
|
|
5
|
+
*
|
|
6
|
+
* Elle lit la famille (`type`) posée à la construction du contexte, jamais
|
|
7
|
+
* `instanceof` : deux copies installées de `@nodefony/http` font échouer
|
|
8
|
+
* `instanceof` en silence, quand ce champ dit vrai pour les deux. Deux
|
|
9
|
+
* Une comparaison de chaînes, aucune allocation.
|
|
10
|
+
*
|
|
11
|
+
* @param context - le contexte à éprouver.
|
|
12
|
+
* @returns `true` si le contexte traite une connexion WebSocket (`ws` ou `wss`).
|
|
13
|
+
*/
|
|
14
|
+
function isWebsocketContext(context) {
|
|
15
|
+
return context.type === "websocket";
|
|
16
|
+
}
|
|
17
|
+
export { isWebsocketContext };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/websocketUrl.ts
|
|
2
2
|
/**
|
|
3
3
|
* URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
|
|
4
4
|
*
|
|
@@ -17,5 +17,4 @@
|
|
|
17
17
|
function parseWebsocketUrl(scheme, host, rawUrl) {
|
|
18
18
|
return new URL(`${scheme}://${host ?? "localhost"}${rawUrl ?? "/"}`);
|
|
19
19
|
}
|
|
20
|
-
//#endregion
|
|
21
20
|
export { parseWebsocketUrl };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/wsBackpressure.ts
|
|
2
2
|
/**
|
|
3
3
|
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
4
4
|
*
|
|
@@ -52,5 +52,4 @@ const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
|
|
|
52
52
|
}
|
|
53
53
|
return "drop";
|
|
54
54
|
};
|
|
55
|
-
//#endregion
|
|
56
55
|
export { decideSend, readBackpressureOptions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/wsCloseReason.ts
|
|
2
2
|
/**
|
|
3
3
|
* Taille maximale, en octets UTF-8, de la raison d'une trame Close (RFC 6455
|
|
4
4
|
* §5.5 : charge d'une trame de contrôle ≤ 125 octets, dont 2 pour le code).
|
|
@@ -37,5 +37,4 @@ function toWsCloseReason(reason) {
|
|
|
37
37
|
}
|
|
38
38
|
return reason.slice(0, end) + ELLIPSIS;
|
|
39
39
|
}
|
|
40
|
-
//#endregion
|
|
41
40
|
export { MAX_WS_CLOSE_REASON_BYTES, toWsCloseReason };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/websocket/wsLogContent.ts
|
|
2
2
|
/**
|
|
3
3
|
* Formatage **pur** (0 état, 0 dépendance) du CONTENU d'un message WebSocket pour
|
|
4
4
|
* le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
|
|
@@ -64,5 +64,4 @@ function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
|
|
|
64
64
|
if (typeof s !== "string") return String(data);
|
|
65
65
|
return s.length > cap ? `${s.slice(0, cap)}…` : s;
|
|
66
66
|
}
|
|
67
|
-
//#endregion
|
|
68
67
|
export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };
|
|
@@ -2,7 +2,7 @@ import { extend } from "nodefony";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { parseCookie } from "cookie";
|
|
4
4
|
import MS from "ms";
|
|
5
|
-
|
|
5
|
+
// nodefony/src/cookies/cookie.ts
|
|
6
6
|
const encode = encodeURIComponent;
|
|
7
7
|
const decode = decodeURIComponent;
|
|
8
8
|
const cookieDefaultSettings = {
|
|
@@ -20,11 +20,9 @@ function parser(strToParse) {
|
|
|
20
20
|
}
|
|
21
21
|
function cookiesParser(context) {
|
|
22
22
|
let cookies = null;
|
|
23
|
-
let co
|
|
23
|
+
let co;
|
|
24
24
|
switch (context.type) {
|
|
25
25
|
case "http":
|
|
26
|
-
case "https":
|
|
27
|
-
case "http2":
|
|
28
26
|
if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
|
|
29
27
|
if (cookies) {
|
|
30
28
|
const obj = parser(cookies);
|
|
@@ -35,8 +33,7 @@ function cookiesParser(context) {
|
|
|
35
33
|
if (context.response) context.request.request.cookies = context.response.cookies;
|
|
36
34
|
}
|
|
37
35
|
break;
|
|
38
|
-
case "websocket":
|
|
39
|
-
case "websocket-secure": {
|
|
36
|
+
case "websocket": {
|
|
40
37
|
const cookieHeader = context.request?.headers.cookie;
|
|
41
38
|
if (cookieHeader) {
|
|
42
39
|
const parsed = parser(cookieHeader);
|
|
@@ -254,5 +251,4 @@ var Cookie = class Cookie {
|
|
|
254
251
|
return obj;
|
|
255
252
|
}
|
|
256
253
|
};
|
|
257
|
-
//#endregion
|
|
258
254
|
export { cookiesParser, Cookie as default, parser };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clc, nodefonyError } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/errors/httpError.ts
|
|
3
3
|
const exclude = {
|
|
4
4
|
context: true,
|
|
5
5
|
resolver: true,
|
|
@@ -65,5 +65,4 @@ var HttpError = class extends nodefonyError {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
|
|
68
|
-
//#endregion
|
|
69
68
|
export { HttpError as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/errors/thrownMessage.ts
|
|
2
2
|
/**
|
|
3
3
|
* Message lisible d'une valeur LEVÉE (`catch`, rejet) dont on ignore le type.
|
|
4
4
|
*
|
|
@@ -23,5 +23,4 @@ function thrownMessage(value) {
|
|
|
23
23
|
case "function": return value === null ? "null" : Object.prototype.toString.call(value);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
//#endregion
|
|
27
26
|
export { thrownMessage };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { thrownMessage } from "../errors/thrownMessage.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/profiler/FrameProfile.ts
|
|
3
3
|
/**
|
|
4
4
|
* Profil d'**une invocation** du pont RPC (une frame `api.request`), et non de
|
|
5
5
|
* la connexion qui la porte.
|
|
@@ -92,5 +92,4 @@ var FrameProfile = class {
|
|
|
92
92
|
if (error !== void 0 && error !== null) this.error = { message: thrownMessage(error) };
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
|
-
//#endregion
|
|
96
95
|
export { FrameProfile, FrameProfile as default };
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { profileLane } from "./aggregate.js";
|
|
2
|
+
// nodefony/src/profiler/Profiler.ts
|
|
3
|
+
/**
|
|
4
|
+
* Durée totale = (fin de la dernière phase) − (début de la 1ère).
|
|
5
|
+
*
|
|
6
|
+
* Une phase encore OUVERTE à la collecte (action interrompue par le départ du
|
|
7
|
+
* client : flux SSE, outil MCP annulé) court jusqu'à la collecte — la compter
|
|
8
|
+
* pour zéro publiait 0,3 ms pour un appel coupé à 3 s.
|
|
9
|
+
*/
|
|
3
10
|
function totalDuration(phases) {
|
|
4
11
|
if (phases.length === 0) return null;
|
|
5
12
|
const first = phases.at(0);
|
|
6
13
|
if (first === void 0 || typeof first.startMs !== "number") return null;
|
|
7
14
|
let end = first.startMs;
|
|
15
|
+
let now = null;
|
|
8
16
|
for (const p of phases) {
|
|
9
|
-
const e = p.startMs +
|
|
17
|
+
const e = p.durationMs === void 0 ? now ??= performance.now() : p.startMs + p.durationMs;
|
|
10
18
|
if (e > end) end = e;
|
|
11
19
|
}
|
|
12
20
|
const d = end - first.startMs;
|
|
@@ -14,11 +22,41 @@ function totalDuration(phases) {
|
|
|
14
22
|
}
|
|
15
23
|
/** Capacité par défaut du ring buffer (assez pour une session de debug). */
|
|
16
24
|
const DEFAULT_CAP = 500;
|
|
25
|
+
/**
|
|
26
|
+
* Capacité par défaut des deux anneaux annexes : l'OUTILLAGE (console, API
|
|
27
|
+
* d'administration, porte MCP) et les ASSETS relayés de Vite. Un chargement
|
|
28
|
+
* de la console relaie à lui seul quelques centaines de modules : dans un
|
|
29
|
+
* tampon commun, il chassait en quelques secondes les profils de
|
|
30
|
+
* l'application, puis ceux des appels de la console elle-même.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_SIDE_CAP = 100;
|
|
33
|
+
/**
|
|
34
|
+
* Collecteur des profils de requête, en un anneau par provenance
|
|
35
|
+
* ({@link profileLane}) : l'application, l'outillage, les assets relayés.
|
|
36
|
+
* Chacun évince ses propres profils — ouvrir la console ne chasse plus celui
|
|
37
|
+
* qu'on y consulte.
|
|
38
|
+
*/
|
|
17
39
|
var Profiler = class {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
_lanes = {
|
|
41
|
+
app: /* @__PURE__ */ new Map(),
|
|
42
|
+
tooling: /* @__PURE__ */ new Map(),
|
|
43
|
+
asset: /* @__PURE__ */ new Map()
|
|
44
|
+
};
|
|
45
|
+
_caps;
|
|
46
|
+
/** Dernier numéro d'ordre attribué (0 = aucun profil collecté). */
|
|
47
|
+
_seq = 0;
|
|
48
|
+
/**
|
|
49
|
+
* @param cap - capacité de l'anneau de l'application.
|
|
50
|
+
* @param sideCap - capacité de chacun des anneaux de l'outillage et des
|
|
51
|
+
* assets relayés.
|
|
52
|
+
*/
|
|
53
|
+
constructor(cap = DEFAULT_CAP, sideCap = DEFAULT_SIDE_CAP) {
|
|
54
|
+
const side = sideCap > 0 ? sideCap : DEFAULT_SIDE_CAP;
|
|
55
|
+
this._caps = {
|
|
56
|
+
app: cap > 0 ? cap : DEFAULT_CAP,
|
|
57
|
+
tooling: side,
|
|
58
|
+
asset: side
|
|
59
|
+
};
|
|
22
60
|
}
|
|
23
61
|
/**
|
|
24
62
|
* Snapshot un `Context` en fin de requête. Tolérant aux champs absents
|
|
@@ -32,8 +70,9 @@ var Profiler = class {
|
|
|
32
70
|
const phases = ctx.phases ?? [];
|
|
33
71
|
const entry = {
|
|
34
72
|
requestId,
|
|
73
|
+
seq: ++this._seq,
|
|
35
74
|
ts: Date.now(),
|
|
36
|
-
kind: ctx.type === "websocket"
|
|
75
|
+
kind: ctx.type === "websocket" ? "ws" : "http",
|
|
37
76
|
method: ctx.method ?? null,
|
|
38
77
|
url: ctx.url ?? "",
|
|
39
78
|
scheme: ctx.scheme ?? "",
|
|
@@ -54,24 +93,30 @@ var Profiler = class {
|
|
|
54
93
|
queries: ctx.profilerQueries && ctx.profilerQueries.length > 0 ? ctx.profilerQueries : void 0,
|
|
55
94
|
security: readSecurity(ctx)
|
|
56
95
|
};
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
96
|
+
const { app, tooling, asset } = this._lanes;
|
|
97
|
+
app.delete(requestId);
|
|
98
|
+
tooling.delete(requestId);
|
|
99
|
+
asset.delete(requestId);
|
|
100
|
+
const lane = profileLane(entry.url);
|
|
101
|
+
const buf = this._lanes[lane];
|
|
102
|
+
buf.set(requestId, entry);
|
|
103
|
+
if (buf.size > this._caps[lane]) {
|
|
104
|
+
const oldest = buf.keys().next().value;
|
|
105
|
+
if (oldest !== void 0) buf.delete(oldest);
|
|
62
106
|
}
|
|
63
107
|
}
|
|
64
108
|
/** Profil complet d'une requête, ou `undefined` si évincé/inconnu. */
|
|
65
109
|
get(requestId) {
|
|
66
|
-
|
|
110
|
+
const { app, tooling, asset } = this._lanes;
|
|
111
|
+
return app.get(requestId) ?? tooling.get(requestId) ?? asset.get(requestId);
|
|
67
112
|
}
|
|
68
113
|
/**
|
|
69
114
|
* Résumés des requêtes les plus récentes (récent → ancien), capés à `limit`.
|
|
70
115
|
*/
|
|
71
116
|
recent(limit = 60) {
|
|
72
117
|
const out = [];
|
|
73
|
-
const entries =
|
|
74
|
-
for (const e of entries
|
|
118
|
+
const entries = this.#collectAfter(0).reverse();
|
|
119
|
+
for (const e of entries) {
|
|
75
120
|
if (out.length >= limit) break;
|
|
76
121
|
out.push({
|
|
77
122
|
requestId: e.requestId,
|
|
@@ -87,13 +132,51 @@ var Profiler = class {
|
|
|
87
132
|
}
|
|
88
133
|
return out;
|
|
89
134
|
}
|
|
135
|
+
/** Dernier numéro d'ordre attribué : le point de départ d'une lecture suivie. */
|
|
136
|
+
get cursor() {
|
|
137
|
+
return this._seq;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Profils collectés après `cursor`, tous anneaux confondus, dans l'ordre
|
|
141
|
+
* des numéros.
|
|
142
|
+
*
|
|
143
|
+
* Les pertes se comptent sans estimation : chaque numéro attribué est un
|
|
144
|
+
* profil collecté, donc ce qui manque entre le curseur et le dernier numéro
|
|
145
|
+
* a disparu avant d'être lu. Un curseur venu d'ailleurs (supérieur au
|
|
146
|
+
* dernier numéro) repart de zéro.
|
|
147
|
+
*
|
|
148
|
+
* @param cursor - le `cursor` rendu par l'appel précédent, ou {@link cursor}.
|
|
149
|
+
*/
|
|
150
|
+
since(cursor) {
|
|
151
|
+
const from = cursor >= 0 && cursor <= this._seq ? cursor : 0;
|
|
152
|
+
const entries = this.#collectAfter(from);
|
|
153
|
+
return {
|
|
154
|
+
entries,
|
|
155
|
+
cursor: this._seq,
|
|
156
|
+
lost: this._seq - from - entries.length
|
|
157
|
+
};
|
|
158
|
+
}
|
|
90
159
|
/** Nombre de profils en mémoire. */
|
|
91
160
|
get size() {
|
|
92
|
-
|
|
161
|
+
const { app, tooling, asset } = this._lanes;
|
|
162
|
+
return app.size + tooling.size + asset.size;
|
|
93
163
|
}
|
|
94
|
-
/** Vide
|
|
164
|
+
/** Vide les anneaux. */
|
|
95
165
|
clear() {
|
|
96
|
-
this.
|
|
166
|
+
const { app, tooling, asset } = this._lanes;
|
|
167
|
+
app.clear();
|
|
168
|
+
tooling.clear();
|
|
169
|
+
asset.clear();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Profils de numéro supérieur à `from`, tous anneaux confondus, du plus
|
|
173
|
+
* ancien au plus récent. Lecture à la demande (console, outil de l'agent) :
|
|
174
|
+
* jamais sur le chemin d'une requête.
|
|
175
|
+
*/
|
|
176
|
+
#collectAfter(from) {
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const lane of Object.values(this._lanes)) for (const e of lane.values()) if (e.seq > from) out.push(e);
|
|
179
|
+
return out.sort((a, b) => a.seq - b.seq);
|
|
97
180
|
}
|
|
98
181
|
};
|
|
99
182
|
/**
|
|
@@ -131,5 +214,4 @@ function readSecurity(ctx) {
|
|
|
131
214
|
function readUser(ctx) {
|
|
132
215
|
return ctx.userIdentifier ?? ctx.securityTrace?.user ?? null;
|
|
133
216
|
}
|
|
134
|
-
//#endregion
|
|
135
217
|
export { Profiler, Profiler as default };
|