@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.
Files changed (130) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +30 -26
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +29 -4
  7. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  8. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  9. package/dist/nodefony/grpc/index.js +4 -0
  10. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  11. package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
  12. package/dist/nodefony/service/audit-logger.js +52 -6
  13. package/dist/nodefony/service/error-renderer.js +47 -13
  14. package/dist/nodefony/service/http-kernel.js +306 -49
  15. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  16. package/dist/nodefony/service/request-logger.js +21 -6
  17. package/dist/nodefony/service/reverse-proxy.js +8 -3
  18. package/dist/nodefony/service/servers/clientError.js +53 -4
  19. package/dist/nodefony/service/servers/server-h2c.js +6 -6
  20. package/dist/nodefony/service/servers/server-http.js +6 -4
  21. package/dist/nodefony/service/servers/server-https.js +14 -7
  22. package/dist/nodefony/service/servers/server-static.js +1 -6
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
  24. package/dist/nodefony/service/servers/server-websocket.js +7 -11
  25. package/dist/nodefony/service/servers/serverShutdown.js +43 -6
  26. package/dist/nodefony/service/sessions/sessions-service.js +7 -7
  27. package/dist/nodefony/service/upload/upload-service.js +2 -2
  28. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  29. package/dist/nodefony/src/context/Context.js +12 -1
  30. package/dist/nodefony/src/context/http/ByteStream.js +364 -0
  31. package/dist/nodefony/src/context/http/HttpContext.js +139 -32
  32. package/dist/nodefony/src/context/http/Request.js +111 -22
  33. package/dist/nodefony/src/context/http/Response.js +459 -37
  34. package/dist/nodefony/src/context/http/SseStream.js +83 -148
  35. package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
  36. package/dist/nodefony/src/context/http/parser.js +29 -11
  37. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  38. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  39. package/dist/nodefony/src/context/http2/Request.js +1 -1
  40. package/dist/nodefony/src/context/http2/Response.js +55 -12
  41. package/dist/nodefony/src/context/requestTrace.js +6 -2
  42. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  43. package/dist/nodefony/src/context/transport.js +20 -1
  44. package/dist/nodefony/src/context/websocket/Response.js +15 -3
  45. package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
  46. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  47. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  48. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  49. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  50. package/dist/nodefony/src/cookies/cookie.js +57 -14
  51. package/dist/nodefony/src/errors/httpError.js +58 -25
  52. package/dist/nodefony/src/grpc/cors.js +59 -0
  53. package/dist/nodefony/src/grpc/limits.js +16 -0
  54. package/dist/nodefony/src/grpc/refusal.js +271 -0
  55. package/dist/nodefony/src/profiler/Profiler.js +14 -1
  56. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  57. package/dist/nodefony/src/proxy/forward.js +109 -9
  58. package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
  59. package/dist/nodefony/src/proxy/rules.js +29 -14
  60. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  61. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  62. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  63. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  64. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  65. package/dist/types/index.d.ts +11 -2
  66. package/dist/types/nodefony/config/config.d.ts +14 -0
  67. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  68. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  69. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  70. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  71. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  72. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  73. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  74. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  75. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  76. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  77. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  78. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  79. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
  80. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  81. package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
  82. package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
  83. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  84. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  85. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
  86. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  87. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  88. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  89. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
  90. package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
  91. package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
  92. package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
  93. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
  94. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  95. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  96. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  97. package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
  98. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  99. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  100. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  101. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  102. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
  103. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  104. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  105. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  106. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  107. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  108. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  109. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  110. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  111. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  112. package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
  113. package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
  114. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  115. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  116. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  117. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  118. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  119. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  120. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  121. package/docs/byte-stream.md +406 -0
  122. package/docs/cookies.md +13 -3
  123. package/docs/diagnostics-channel.md +45 -1
  124. package/docs/index.md +8 -0
  125. package/docs/reverse-proxy.md +15 -3
  126. package/docs/servers.md +67 -43
  127. package/docs/session.md +3 -3
  128. package/docs/sse.md +27 -20
  129. package/docs/upload.md +43 -17
  130. 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, 0 dépendance) du CONTENU d'un message WebSocket pour
4
- * le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
5
- * être testable aux limites sans serveur.
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 sérialisée
11
- * (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme et
12
- * faux). Seules string et objets « JSON » sont rendus en texte (borné).
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 dump) ;
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.length > cap ? `${data.slice(0, cap)}…` : 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) return `[binary ${bytes} B]`;
58
- let s;
59
- try {
60
- s = JSON.stringify(data);
61
- } catch {
62
- return String(data);
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
- if (typeof s !== "string") return String(data);
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 crypto from "node:crypto";
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
- const decode = decodeURIComponent;
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 = parser(cookies);
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 = parser(cookieHeader);
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
- if (this.expires && this.expires instanceof Date) {
170
- const s = (this.expires.getTime() - (/* @__PURE__ */ new Date()).getTime()) / 1e3;
171
- if (s > 0) this.maxAge = s;
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, parser };
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
- const exclude = {
4
- context: true,
5
- resolver: true,
6
- container: true,
7
- secure: true,
8
- request: true,
9
- response: true
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
- const alt = {};
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.response = res ?? void 0;
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.context) return `${clc.red(this.message)}
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 };