@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7
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.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- 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 +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- 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 +57 -14
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
- 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/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { decideBackpressure, nextDropBalance } from "nodefony";
|
|
2
|
+
// nodefony/src/context/websocket/wsFrameRate.ts
|
|
3
|
+
/** Durée d'une fenêtre de comptage (ms) : la borne s'exprime en trames PAR SECONDE. */
|
|
4
|
+
const FRAME_RATE_WINDOW_MS = 1e3;
|
|
5
|
+
let rejectedTotal = 0;
|
|
6
|
+
/**
|
|
7
|
+
* Nombre cumulé de trames entrantes écartées par la borne de débit dans ce processus (unité : une trame reçue).
|
|
8
|
+
*
|
|
9
|
+
* Monotone, `0` tant que la borne est désactivée. Relayé par la sonde du hub
|
|
10
|
+
* temps réel sous le nom figé `frameRateRejectedTotal` (ADR-0021 §S9).
|
|
11
|
+
*
|
|
12
|
+
* @returns le cumul depuis le démarrage du processus
|
|
13
|
+
*/
|
|
14
|
+
const frameRateRejectedTotal = () => rejectedTotal;
|
|
15
|
+
/**
|
|
16
|
+
* Lit une borne numérique dans les options du serveur WebSocket d'un contexte.
|
|
17
|
+
*
|
|
18
|
+
* Sans allocation ni conversion de type : un serveur sans `options` (HTTP) ou
|
|
19
|
+
* une clé absente rendent `0`.
|
|
20
|
+
*/
|
|
21
|
+
const readOption = (server, key) => {
|
|
22
|
+
if (typeof server !== "object" || server === null) return 0;
|
|
23
|
+
if (!("options" in server)) return 0;
|
|
24
|
+
const opts = server.options;
|
|
25
|
+
if (typeof opts !== "object" || opts === null || !(key in opts)) return 0;
|
|
26
|
+
const value = Reflect.get(opts, key);
|
|
27
|
+
return typeof value === "number" ? value : 0;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Borne de débit entrante du serveur WebSocket (`maxFramesPerSecond`) ; `0` = désactivée.
|
|
31
|
+
*
|
|
32
|
+
* Appelée à chaque trame : une lecture de propriété, aucune allocation.
|
|
33
|
+
*
|
|
34
|
+
* @param server - le serveur du contexte (`context.server`)
|
|
35
|
+
* @returns trames admises par seconde, `0` si la borne est désactivée
|
|
36
|
+
*/
|
|
37
|
+
const readFrameRateBound = (server) => readOption(server, "maxFramesPerSecond");
|
|
38
|
+
/**
|
|
39
|
+
* Solde de refus qui ferme la connexion (`frameRateCloseAfterRejects`) ; `0` = jamais.
|
|
40
|
+
*
|
|
41
|
+
* @param server - le serveur du contexte (`context.server`)
|
|
42
|
+
* @returns le solde de fermeture
|
|
43
|
+
*/
|
|
44
|
+
const readFrameRateCloseAfter = (server) => readOption(server, "frameRateCloseAfterRejects");
|
|
45
|
+
/**
|
|
46
|
+
* Décide du sort d'une trame reçue, AVANT tout décodage : l'admettre, l'écarter, ou fermer la connexion.
|
|
47
|
+
*
|
|
48
|
+
* Compte des TRAMES, pas des octets (`maxPayload` borne déjà la taille). La
|
|
49
|
+
* décision est la règle unique du cœur (`decideBackpressure`), appliquée au
|
|
50
|
+
* nombre de trames de la fenêtre : écarter au-delà de la borne, fermer quand le
|
|
51
|
+
* solde de refus atteint `closeAfter` — jamais au premier dépassement. Une
|
|
52
|
+
* trame écartée est comptée dans {@link frameRateRejectedTotal}.
|
|
53
|
+
*
|
|
54
|
+
* @param win - la fenêtre de la connexion (mutée)
|
|
55
|
+
* @param now - l'instant de réception (ms)
|
|
56
|
+
* @param bound - trames admises par seconde (`> 0`)
|
|
57
|
+
* @param closeAfter - solde de refus qui ferme (`0` = jamais)
|
|
58
|
+
* @returns `"send"` (admettre), `"drop"` (écarter) ou `"close"` (fermer en `1008`)
|
|
59
|
+
*/
|
|
60
|
+
const admitFrame = (win, now, bound, closeAfter) => {
|
|
61
|
+
if (now - win.start >= 1e3) {
|
|
62
|
+
win.start = now;
|
|
63
|
+
win.count = 0;
|
|
64
|
+
}
|
|
65
|
+
win.count += 1;
|
|
66
|
+
const verdict = decideBackpressure(win.count, bound, "drop", closeAfter, win.balance);
|
|
67
|
+
win.balance = nextDropBalance(verdict, win.balance);
|
|
68
|
+
if (verdict !== "send") rejectedTotal += 1;
|
|
69
|
+
return verdict;
|
|
70
|
+
};
|
|
71
|
+
export { FRAME_RATE_WINDOW_MS, admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter };
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { binaryPayloadLabel } from "nodefony";
|
|
1
2
|
// nodefony/src/context/websocket/wsLogContent.ts
|
|
2
3
|
/**
|
|
3
|
-
* Formatage **pur** (0 état
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Formatage **pur** (0 état) du CONTENU d'un message WebSocket pour le log de trace
|
|
5
|
+
* (Suivi de requête Studio). Séparé de `WebsocketContext` pour être testable aux
|
|
6
|
+
* limites sans serveur.
|
|
6
7
|
*
|
|
7
8
|
* 🔒 Robustesse binaire (cf doc `ws` — `socket.send(data)` accepte
|
|
8
9
|
* `String | Number | Object | Buffer | ArrayBuffer | TypedArray | DataView |
|
|
9
10
|
* Buffer[] | Blob`, et l'event `message` livre `Buffer | ArrayBuffer | Buffer[]`).
|
|
10
|
-
* **Toute** charge binaire est résumée `[binary N B]` — JAMAIS
|
|
11
|
-
* (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme
|
|
12
|
-
* faux).
|
|
11
|
+
* **Toute** charge binaire est résumée `[binary N B]` — JAMAIS vidée octet par
|
|
12
|
+
* octet (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme
|
|
13
|
+
* et faux). Quand la connexion a négocié un codec de frame (CBOR, ADR-0022 §2), la
|
|
14
|
+
* trame est en plus DÉCODÉE pour être lisible : le débogage se fait à l'observation,
|
|
15
|
+
* jamais en changeant le fil.
|
|
13
16
|
*/
|
|
14
17
|
/** Cap de troncature du contenu loggé (octets/chars). Borne ring + JSONL. */
|
|
15
18
|
const WS_LOG_CONTENT_CAP = 4096;
|
|
@@ -40,28 +43,63 @@ function binaryByteLength(data) {
|
|
|
40
43
|
return -1;
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
46
|
+
* Octets imbriqués dans une valeur JSON : leur étiquette, jamais leur vidage.
|
|
47
|
+
*
|
|
48
|
+
* Lit la valeur BRUTE chez son parent (`this[key]`) : `JSON.stringify` appelle
|
|
49
|
+
* `toJSON` avant le replacer, et un `Buffer` — ce que rend un décodage depuis la
|
|
50
|
+
* trame de `ws` — y deviendrait `{"type":"Buffer","data":[…]}`.
|
|
51
|
+
*/
|
|
52
|
+
function binaryAsLabel(key, value) {
|
|
53
|
+
const raw = Reflect.get(this, key);
|
|
54
|
+
if (ArrayBuffer.isView(raw)) return binaryPayloadLabel(raw.byteLength);
|
|
55
|
+
if (raw instanceof ArrayBuffer) return binaryPayloadLabel(raw.byteLength);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/** Sérialisation d'une valeur pour une ligne de journal — jamais de levée. */
|
|
59
|
+
function stringifyForLog(value) {
|
|
60
|
+
let s;
|
|
61
|
+
try {
|
|
62
|
+
s = JSON.stringify(value, binaryAsLabel);
|
|
63
|
+
} catch {
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
return typeof s === "string" ? s : String(value);
|
|
67
|
+
}
|
|
68
|
+
/** Tronque à `cap` caractères, ellipse comprise au-delà. */
|
|
69
|
+
function bound(s, cap) {
|
|
70
|
+
return s.length > cap ? `${s.slice(0, cap)}…` : s;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
43
73
|
* Formate une charge utile WS en chaîne **bornée** et **sûre** pour le log :
|
|
44
74
|
* - `string` → tronquée à `cap` (+ ellipse) ;
|
|
45
|
-
* - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de
|
|
75
|
+
* - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de vidage) ;
|
|
76
|
+
* avec un `codec`, une trame d'un seul tenant est aussi décodée :
|
|
77
|
+
* `[binary N B] <protocole> <frame en JSON>`, octets imbriqués étiquetés ;
|
|
78
|
+
* illisible → `[binary N B] <protocole> illisible` ;
|
|
46
79
|
* - `null`/`undefined` → `""` ;
|
|
47
80
|
* - objet « JSON » → `JSON.stringify` compact tronqué (cycle, `bigint`, valeur
|
|
48
81
|
* non sérialisable → repli `String(...)`).
|
|
49
82
|
*
|
|
50
83
|
* @param data - charge utile (RECEIVE/SEND/BROADCAST).
|
|
51
84
|
* @param cap - longueur max avant troncature (défaut {@link WS_LOG_CONTENT_CAP}).
|
|
85
|
+
* @param codec - codec des trames binaires de la connexion, `null` = octets bruts.
|
|
86
|
+
* @returns la ligne à journaliser, jamais plus longue que `cap` + 1.
|
|
52
87
|
*/
|
|
53
|
-
function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
|
|
54
|
-
if (typeof data === "string") return data
|
|
88
|
+
function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP, codec = null) {
|
|
89
|
+
if (typeof data === "string") return bound(data, cap);
|
|
55
90
|
if (data === null || data === void 0) return "";
|
|
56
91
|
const bytes = binaryByteLength(data);
|
|
57
|
-
if (bytes >= 0)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
92
|
+
if (bytes >= 0) {
|
|
93
|
+
const label = binaryPayloadLabel(bytes);
|
|
94
|
+
if (codec === null || !(data instanceof Uint8Array)) return label;
|
|
95
|
+
let frame;
|
|
96
|
+
try {
|
|
97
|
+
frame = codec.decode(data);
|
|
98
|
+
} catch {
|
|
99
|
+
return `${label} ${codec.protocol} illisible`;
|
|
100
|
+
}
|
|
101
|
+
return bound(`${label} ${codec.protocol} ${stringifyForLog(frame)}`, cap);
|
|
63
102
|
}
|
|
64
|
-
|
|
65
|
-
return s.length > cap ? `${s.slice(0, cap)}…` : s;
|
|
103
|
+
return bound(stringifyForLog(data), cap);
|
|
66
104
|
}
|
|
67
105
|
export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { extend } from "nodefony";
|
|
2
|
-
import
|
|
3
|
-
import { parseCookie } from "cookie";
|
|
2
|
+
import { parseCookie, stringifySetCookie } from "cookie";
|
|
4
3
|
import MS from "ms";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
5
|
// nodefony/src/cookies/cookie.ts
|
|
6
6
|
const encode = encodeURIComponent;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Décode une valeur de cookie sans jamais lever : une séquence `%` invalide
|
|
9
|
+
* rend la valeur brute (règle du paquet `cookie`). Un `decodeURIComponent`
|
|
10
|
+
* nu levait dans le constructeur du contexte, et la requête restait sans
|
|
11
|
+
* réponse (`Cookie: a=50%25`).
|
|
12
|
+
*/
|
|
13
|
+
function decode(value) {
|
|
14
|
+
if (!value.includes("%")) return value;
|
|
15
|
+
try {
|
|
16
|
+
return decodeURIComponent(value);
|
|
17
|
+
} catch {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lecture de l'en-tête `Cookie` SANS décodage : la valeur est décodée une
|
|
23
|
+
* seule fois, par {@link Cookie.setValue}. Décoder aux deux endroits
|
|
24
|
+
* transformait `a%2541` en `aA` au lieu de `a%41`.
|
|
25
|
+
*/
|
|
26
|
+
const RAW_VALUES = { decode: (value) => value };
|
|
8
27
|
const cookieDefaultSettings = {
|
|
9
28
|
maxAge: 0,
|
|
10
29
|
path: "/",
|
|
@@ -15,9 +34,6 @@ const cookieDefaultSettings = {
|
|
|
15
34
|
signed: false,
|
|
16
35
|
secret: "!nodefony.secret!"
|
|
17
36
|
};
|
|
18
|
-
function parser(strToParse) {
|
|
19
|
-
return parseCookie(strToParse);
|
|
20
|
-
}
|
|
21
37
|
function cookiesParser(context) {
|
|
22
38
|
let cookies = null;
|
|
23
39
|
let co;
|
|
@@ -25,8 +41,9 @@ function cookiesParser(context) {
|
|
|
25
41
|
case "http":
|
|
26
42
|
if (context.request.request.headers.cookie) cookies = context.request.request.headers.cookie;
|
|
27
43
|
if (cookies) {
|
|
28
|
-
const obj =
|
|
44
|
+
const obj = parseCookie(cookies, RAW_VALUES);
|
|
29
45
|
for (const cookie in obj) {
|
|
46
|
+
if (cookie === "") continue;
|
|
30
47
|
co = new Cookie(cookie, obj[cookie]);
|
|
31
48
|
context.addRequestCookie(co);
|
|
32
49
|
}
|
|
@@ -36,8 +53,9 @@ function cookiesParser(context) {
|
|
|
36
53
|
case "websocket": {
|
|
37
54
|
const cookieHeader = context.request?.headers.cookie;
|
|
38
55
|
if (cookieHeader) {
|
|
39
|
-
const parsed =
|
|
56
|
+
const parsed = parseCookie(cookieHeader, RAW_VALUES);
|
|
40
57
|
for (const name in parsed) {
|
|
58
|
+
if (name === "") continue;
|
|
41
59
|
co = new Cookie(name, parsed[name]);
|
|
42
60
|
context.addRequestCookie(co);
|
|
43
61
|
}
|
|
@@ -165,14 +183,39 @@ var Cookie = class Cookie {
|
|
|
165
183
|
setPriority(val) {
|
|
166
184
|
return val;
|
|
167
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Durée de vie restante en SECONDES ENTIÈRES, `undefined` sous une seconde.
|
|
188
|
+
*
|
|
189
|
+
* RFC 6265 §4.1.1 : `max-age-av = "Max-Age=" non-zero-digit *DIGIT`. Un
|
|
190
|
+
* `Max-Age=3599.874` est ignoré par le navigateur, et le cookie devient un
|
|
191
|
+
* cookie de session. Arrondi INFÉRIEUR : le cookie ne survit jamais à son
|
|
192
|
+
* `Expires`.
|
|
193
|
+
*/
|
|
168
194
|
getMaxAge() {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
else this.maxAge = void 0;
|
|
173
|
-
} else this.maxAge = this.originalMaxAge;
|
|
195
|
+
const seconds = this.expires instanceof Date ? (this.expires.getTime() - Date.now()) / 1e3 : this.originalMaxAge;
|
|
196
|
+
const whole = seconds === void 0 ? 0 : Math.floor(seconds);
|
|
197
|
+
this.maxAge = whole >= 1 ? whole : void 0;
|
|
174
198
|
return this.maxAge;
|
|
175
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Vérifie la grammaire RFC 6265 §4.1.1 du nom, du `Path` et du `Domain`,
|
|
202
|
+
* au moment où le cookie est POSÉ sur la réponse.
|
|
203
|
+
*
|
|
204
|
+
* Sans elle, `path: "/; Domain=evil.example"` injectait un attribut, et un
|
|
205
|
+
* nom porteur de `;` partait tel quel. La grammaire est celle du paquet
|
|
206
|
+
* `cookie` : une seule implémentation.
|
|
207
|
+
*
|
|
208
|
+
* @throws {TypeError} nommant l'attribut hors grammaire.
|
|
209
|
+
*/
|
|
210
|
+
assertGrammar() {
|
|
211
|
+
const attributes = {
|
|
212
|
+
name: this.name,
|
|
213
|
+
value: ""
|
|
214
|
+
};
|
|
215
|
+
if (this.path !== void 0) attributes.path = this.path;
|
|
216
|
+
if (this.domain !== void 0) attributes.domain = this.domain;
|
|
217
|
+
stringifySetCookie(attributes);
|
|
218
|
+
}
|
|
176
219
|
toString() {
|
|
177
220
|
return `${this.name}=${encode(String(this.value))}`;
|
|
178
221
|
}
|
|
@@ -251,4 +294,4 @@ var Cookie = class Cookie {
|
|
|
251
294
|
return obj;
|
|
252
295
|
}
|
|
253
296
|
};
|
|
254
|
-
export { cookiesParser, Cookie as default
|
|
297
|
+
export { cookiesParser, Cookie as default };
|
|
@@ -1,41 +1,51 @@
|
|
|
1
1
|
import { clc, nodefonyError } from "nodefony";
|
|
2
2
|
// nodefony/src/errors/httpError.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
/** Références vivantes (circulaires) jamais sérialisées, quel que soit l'environnement. */
|
|
4
|
+
const EXCLUDED_KEYS = /* @__PURE__ */ new Set([
|
|
5
|
+
"context",
|
|
6
|
+
"resolver",
|
|
7
|
+
"container",
|
|
8
|
+
"secure",
|
|
9
|
+
"request",
|
|
10
|
+
"response"
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Sérialise les propriétés propres d'une erreur HTTP, sans jamais LIRE celles
|
|
14
|
+
* qu'on écarte.
|
|
15
|
+
*
|
|
16
|
+
* Ne pas lire compte : `stack` est un accesseur paresseux de V8, et sa première
|
|
17
|
+
* lecture FORMATE la pile (`prepareStackTrace`, plusieurs µs). Lire puis
|
|
18
|
+
* supprimer, c'est payer ce formatage pour le jeter — ce que faisait le rendu
|
|
19
|
+
* d'erreur en production, sur chaque 404.
|
|
20
|
+
*
|
|
21
|
+
* @param error - l'erreur à sérialiser (en pratique un `HttpError`).
|
|
22
|
+
* @param omit - clés à écarter EN PLUS des références vivantes, ou `null`.
|
|
23
|
+
* @returns un objet plat, prêt pour `JSON.stringify`.
|
|
24
|
+
*/
|
|
25
|
+
function serializeHttpError(error, omit = null) {
|
|
26
|
+
const alt = {};
|
|
27
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
28
|
+
if (EXCLUDED_KEYS.has(key) || omit?.has(key)) continue;
|
|
29
|
+
alt[key] = Reflect.get(error, key);
|
|
30
|
+
}
|
|
31
|
+
return alt;
|
|
32
|
+
}
|
|
11
33
|
const jsonHttpError = {
|
|
12
34
|
configurable: true,
|
|
13
35
|
writable: true,
|
|
14
36
|
value() {
|
|
15
|
-
|
|
16
|
-
const storeKey = function(key) {
|
|
17
|
-
if (key in exclude) return;
|
|
18
|
-
alt[key] = this[key];
|
|
19
|
-
};
|
|
20
|
-
Object.getOwnPropertyNames(this).forEach(storeKey, this);
|
|
21
|
-
return alt;
|
|
37
|
+
return serializeHttpError(this);
|
|
22
38
|
}
|
|
23
39
|
};
|
|
24
|
-
var HttpError = class extends nodefonyError {
|
|
25
|
-
context;
|
|
26
|
-
response;
|
|
27
|
-
request;
|
|
40
|
+
var HttpError = class HttpError extends nodefonyError {
|
|
28
41
|
url;
|
|
29
42
|
controller;
|
|
30
43
|
action;
|
|
31
44
|
jsonResponse;
|
|
32
45
|
constructor(message, code, context) {
|
|
33
46
|
super(message, code);
|
|
34
|
-
this.context = context;
|
|
35
47
|
const res = context?.response;
|
|
36
|
-
this.
|
|
37
|
-
this.request = context?.request ?? void 0;
|
|
38
|
-
this.url = this.context?.url;
|
|
48
|
+
this.url = context?.url;
|
|
39
49
|
if (res && code) res.statusCode = code;
|
|
40
50
|
if (!this.message && res) this.message = res.body?.toString() || res.statusMessage || res.getStatusMessage();
|
|
41
51
|
const resolver = context?.resolver;
|
|
@@ -43,8 +53,31 @@ var HttpError = class extends nodefonyError {
|
|
|
43
53
|
this.action = resolver?.actionName ?? void 0;
|
|
44
54
|
if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage}`.trim();
|
|
45
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Construit un VERDICT de protocole (404, 405, 426…) sans capturer de pile.
|
|
58
|
+
*
|
|
59
|
+
* La pile d'un verdict pointe toujours au même endroit du noyau ou du routeur
|
|
60
|
+
* et ne dit rien de la requête ; la capturer coûte ~3,6 µs à profondeur
|
|
61
|
+
* réelle (`micro-error-path.mjs`), payés par chaque requête d'un scanner.
|
|
62
|
+
* Réservé à ce que le framework DÉCIDE — une panne garde sa pile. `stack` ne
|
|
63
|
+
* porte que l'en-tête (`HttpError: Not Found`).
|
|
64
|
+
*
|
|
65
|
+
* @param message - message du verdict.
|
|
66
|
+
* @param code - statut HTTP (ou code de fermeture WebSocket).
|
|
67
|
+
* @param context - contexte de la requête, comme pour le constructeur.
|
|
68
|
+
* @returns l'erreur, sans pile.
|
|
69
|
+
*/
|
|
70
|
+
static withoutStack(message, code, context) {
|
|
71
|
+
const limit = Error.stackTraceLimit;
|
|
72
|
+
Error.stackTraceLimit = 0;
|
|
73
|
+
try {
|
|
74
|
+
return new HttpError(message, code, context);
|
|
75
|
+
} finally {
|
|
76
|
+
Error.stackTraceLimit = limit;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
46
79
|
toString() {
|
|
47
|
-
if (this.
|
|
80
|
+
if (this.url !== void 0) return `${clc.red(this.message)}
|
|
48
81
|
${clc.blue("Name :")} ${this.name}
|
|
49
82
|
${clc.blue("Type :")} ${this.errorType}
|
|
50
83
|
${clc.green("Controller :")} ${this.controller}
|
|
@@ -65,4 +98,4 @@ var HttpError = class extends nodefonyError {
|
|
|
65
98
|
}
|
|
66
99
|
};
|
|
67
100
|
Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
|
|
68
|
-
export { HttpError as default };
|
|
101
|
+
export { HttpError as default, serializeHttpError };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { hasGrpcRoutes } from "./refusal.js";
|
|
2
|
+
// nodefony/src/grpc/cors.ts
|
|
3
|
+
/**
|
|
4
|
+
* Les listes de Connect (`cors` de `@connectrpc/connect`, `connect-cors.md`),
|
|
5
|
+
* recopiées : `@nodefony/http` principal ne charge jamais Connect, pair
|
|
6
|
+
* facultatif (ADR-0026 D12). La copie est COMPARÉE à l'original par un test
|
|
7
|
+
* (`grpcCors.test.ts`) : une version de Connect qui change ses listes le fait
|
|
8
|
+
* tomber.
|
|
9
|
+
*/
|
|
10
|
+
const GRPC_CORS = Object.freeze({
|
|
11
|
+
allowedMethods: Object.freeze(["POST", "GET"]),
|
|
12
|
+
allowedHeaders: Object.freeze([
|
|
13
|
+
"Content-Type",
|
|
14
|
+
"Connect-Protocol-Version",
|
|
15
|
+
"Connect-Timeout-Ms",
|
|
16
|
+
"Connect-Content-Encoding",
|
|
17
|
+
"Connect-Accept-Encoding",
|
|
18
|
+
"Content-Encoding",
|
|
19
|
+
"Accept-Encoding",
|
|
20
|
+
"Grpc-Message-Type",
|
|
21
|
+
"X-Grpc-Web",
|
|
22
|
+
"X-User-Agent",
|
|
23
|
+
"Grpc-Timeout"
|
|
24
|
+
]),
|
|
25
|
+
exposedHeaders: Object.freeze([
|
|
26
|
+
"Grpc-Status",
|
|
27
|
+
"Grpc-Message",
|
|
28
|
+
"Grpc-Status-Details-Bin",
|
|
29
|
+
"Content-Encoding",
|
|
30
|
+
"Connect-Content-Encoding"
|
|
31
|
+
])
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* `Access-Control-Expose-Headers` d'une réponse RPC, composé une fois au
|
|
35
|
+
* chargement.
|
|
36
|
+
*/
|
|
37
|
+
const RPC_EXPOSED_HEADERS = GRPC_CORS.exposedHeaders.join(", ");
|
|
38
|
+
/**
|
|
39
|
+
* Les listes CORS que demandent les protocoles RPC — dès qu'un service gRPC
|
|
40
|
+
* est déclaré dans le processus, `null` sinon. Le pare-feu les ajoute à la
|
|
41
|
+
* politique de l'application au démarrage (`onReady`) : le préflight ne
|
|
42
|
+
* connaît pas la route, l'union est la seule voie (ADR-0026 D9 point 2).
|
|
43
|
+
*
|
|
44
|
+
* @returns les listes, ou `null` sans service gRPC déclaré.
|
|
45
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
46
|
+
*/
|
|
47
|
+
function rpcCorsLists() {
|
|
48
|
+
return hasGrpcRoutes() ? GRPC_CORS : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Les listes de Connect telles que recopiées, sans condition — réservé au test
|
|
52
|
+
* qui les compare à l'original.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
function grpcCorsCopy() {
|
|
57
|
+
return GRPC_CORS;
|
|
58
|
+
}
|
|
59
|
+
export { RPC_EXPOSED_HEADERS, grpcCorsCopy, rpcCorsLists };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// nodefony/src/grpc/limits.ts
|
|
2
|
+
/**
|
|
3
|
+
* Borne par défaut d'un message gRPC, reçu ou émis : 4 Mio (ADR-0026 D4,
|
|
4
|
+
* points 1 et 2). C'est la borne de réception des implémentations de
|
|
5
|
+
* référence (`grpc-js`, `grpc-go`) ; l'adaptateur Connect, lui, n'en pose
|
|
6
|
+
* aucune (`limit-io.js` : `0xffffffff`). Seule source de cette valeur : le
|
|
7
|
+
* schéma de la section `grpc` et le point d'entrée gRPC la lisent ici.
|
|
8
|
+
*/
|
|
9
|
+
const GRPC_DEFAULT_MAX_MESSAGE_BYTES = 4194304;
|
|
10
|
+
/**
|
|
11
|
+
* Plus grande borne qu'accepte l'adaptateur Connect pour `readMaxBytes` et
|
|
12
|
+
* `writeMaxBytes` (`limit-io.js#validateReadWriteMaxBytes`) : la longueur
|
|
13
|
+
* d'une enveloppe tient sur 32 bits.
|
|
14
|
+
*/
|
|
15
|
+
const GRPC_MAX_MESSAGE_BYTES = 4294967295;
|
|
16
|
+
export { GRPC_DEFAULT_MAX_MESSAGE_BYTES, GRPC_MAX_MESSAGE_BYTES };
|