@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.
Files changed (124) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +12 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +1 -2
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  11. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  12. package/dist/nodefony/service/audit-logger.js +20 -13
  13. package/dist/nodefony/service/certificates.js +97 -13
  14. package/dist/nodefony/service/error-renderer.js +29 -12
  15. package/dist/nodefony/service/http-kernel.js +118 -43
  16. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  17. package/dist/nodefony/service/request-logger.js +24 -16
  18. package/dist/nodefony/service/reverse-proxy.js +3 -3
  19. package/dist/nodefony/service/servers/clientError.js +22 -3
  20. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  21. package/dist/nodefony/service/servers/server-http.js +5 -8
  22. package/dist/nodefony/service/servers/server-https.js +12 -24
  23. package/dist/nodefony/service/servers/server-static.js +3 -3
  24. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  25. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  26. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  27. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  28. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  29. package/dist/nodefony/service/trace.js +1 -2
  30. package/dist/nodefony/service/upload/upload-service.js +5 -8
  31. package/dist/nodefony/service/x509.js +1 -2
  32. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  33. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  34. package/dist/nodefony/src/context/Context.js +23 -31
  35. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  36. package/dist/nodefony/src/context/forwarded.js +1 -2
  37. package/dist/nodefony/src/context/http/HttpContext.js +7 -6
  38. package/dist/nodefony/src/context/http/Request.js +33 -29
  39. package/dist/nodefony/src/context/http/Response.js +23 -6
  40. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  41. package/dist/nodefony/src/context/http/parser.js +36 -10
  42. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  43. package/dist/nodefony/src/context/http2/Request.js +4 -8
  44. package/dist/nodefony/src/context/http2/Response.js +22 -9
  45. package/dist/nodefony/src/context/metaData.js +1 -2
  46. package/dist/nodefony/src/context/requestId.js +1 -2
  47. package/dist/nodefony/src/context/responseEnded.js +1 -2
  48. package/dist/nodefony/src/context/transport.js +45 -0
  49. package/dist/nodefony/src/context/trustProxy.js +1 -2
  50. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  51. package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
  52. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  53. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  54. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  55. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  57. package/dist/nodefony/src/cookies/cookie.js +3 -7
  58. package/dist/nodefony/src/errors/httpError.js +1 -2
  59. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  60. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  61. package/dist/nodefony/src/profiler/Profiler.js +102 -20
  62. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  63. package/dist/nodefony/src/proxy/forward.js +29 -5
  64. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  65. package/dist/nodefony/src/proxy/rules.js +12 -6
  66. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  67. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  68. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  69. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  70. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  71. package/dist/nodefony/src/servers/portBinder.js +41 -10
  72. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  73. package/dist/nodefony/src/session/session.js +2 -3
  74. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  75. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  76. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  79. package/dist/types/index.d.ts +7 -3
  80. package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
  81. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  82. package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
  83. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  84. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  85. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  86. package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
  87. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  88. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  89. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  90. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  91. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  92. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  93. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  94. package/dist/types/nodefony/src/context/Context.d.ts +13 -4
  95. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
  96. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  97. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  98. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  99. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  100. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  101. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  102. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  103. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  104. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  105. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  106. package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
  107. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  108. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  109. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  110. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  111. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  112. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  113. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  114. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  115. package/docs/cookies.md +85 -78
  116. package/docs/observabilite.md +80 -80
  117. package/docs/rate-limit.md +83 -83
  118. package/docs/reverse-proxy.md +54 -54
  119. package/docs/servers.md +338 -224
  120. package/docs/session.md +179 -179
  121. package/docs/sse.md +24 -20
  122. package/docs/upload.md +120 -117
  123. package/package.json +8 -9
  124. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -3,12 +3,19 @@ import { resolvePortPolicy } from "nodefony";
3
3
  import type { PortPolicy } from "nodefony";
4
4
  export { resolvePortPolicy };
5
5
  export type { PortPolicy };
6
- /** Serveur écoutable (surface minimale commune `http`/`https`/`http2`). */
6
+ /**
7
+ * Serveur écoutable (surface minimale commune `http`/`https`/`http2`).
8
+ *
9
+ * Les écouteurs sont typés événement par événement : c'est la forme que les
10
+ * surcharges de Node acceptent telle quelle, sans conversion chez l'appelant.
11
+ */
7
12
  export interface Listenable {
8
13
  listen(port: number, host?: string): unknown;
9
14
  address(): AddressInfo | string | null;
10
- once(event: string, listener: (...args: never[]) => void): unknown;
11
- removeListener(event: string, listener: (...args: never[]) => void): unknown;
15
+ once(event: "error", listener: (error: NodeJS.ErrnoException) => void): unknown;
16
+ once(event: "listening", listener: () => void): unknown;
17
+ removeListener(event: "error", listener: (error: NodeJS.ErrnoException) => void): unknown;
18
+ removeListener(event: "listening", listener: () => void): unknown;
12
19
  }
13
20
  /** Nombre de ports essayés après le désiré, en `auto`. */
14
21
  export declare const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
@@ -34,18 +41,35 @@ export interface ServersPortConfig {
34
41
  https?: {
35
42
  port?: number | undefined;
36
43
  } | false;
44
+ h2c?: {
45
+ enabled?: boolean | undefined;
46
+ port?: number | undefined;
47
+ };
37
48
  portPolicy?: PortPolicy;
38
49
  portRetryAttempts?: number;
39
50
  }
51
+ /** Serveur dont `buildBindPlan` compose le plan. */
52
+ export type BoundServer = "http" | "https" | "h2c";
53
+ /**
54
+ * Serveur actif qui convoite le MÊME port que h2c, ou `null`.
55
+ *
56
+ * Un port partagé avec h2c n'est pas un conflit à contourner : sur ce port, le
57
+ * client HTTP/1.1 et le proxy h2c se marchent dessus, et le repli `auto` ferait
58
+ * glisser l'un des deux là où personne ne l'attend. Le boot le refuse.
59
+ *
60
+ * @param servers - `kernel.options.servers`.
61
+ * @returns `"http"` ou `"https"` en conflit, sinon `null` (h2c inactif, port 0).
62
+ */
63
+ export declare function h2cPortConflict(servers: ServersPortConfig | undefined): "http" | "https" | null;
40
64
  /**
41
65
  * Compose le plan de bind d'un serveur depuis la config du kernel — source UNIQUE
42
- * (les deux serveurs l'appellent ; la politique n'est décidée qu'ici).
66
+ * (les serveurs l'appellent ; la politique n'est décidée qu'ici).
43
67
  *
44
68
  * @param which - le serveur qu'on borne.
45
69
  * @param servers - `kernel.options.servers`.
46
70
  * @param environment - `kernel.environment` (arbitre le défaut de la politique).
47
71
  */
48
- export declare function buildBindPlan(which: "http" | "https", servers: ServersPortConfig | undefined, environment: string | undefined): BindPlan;
72
+ export declare function buildBindPlan(which: BoundServer, servers: ServersPortConfig | undefined, environment: string | undefined): BindPlan;
49
73
  /**
50
74
  * Sonde « un serveur répond-il déjà ici ? ».
51
75
  *
@@ -1,6 +1,6 @@
1
1
  import { type IncomingMessage } from "node:http";
2
2
  import type { Duplex } from "node:stream";
3
- import type { WebSocketServer } from "ws";
3
+ import type { WebSocket, WebSocketServer } from "ws";
4
4
  /** Serveur qui émet `upgrade` : HTTP/1.1, HTTPS ou HTTP/2 sécurisé (`allowHTTP1`). */
5
5
  interface IUpgradeEmitter {
6
6
  on(event: "upgrade", listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): unknown;
@@ -11,6 +11,31 @@ interface IUpgradeRelay {
11
11
  readonly mounts: readonly unknown[] | null;
12
12
  handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): boolean;
13
13
  }
14
+ /**
15
+ * Le verdict d'un upgrade, rendu par le noyau APRÈS sa décision — qui peut
16
+ * attendre (session, pare-feu, garde de l'action). Un seul verdict compte : le
17
+ * premier appel l'emporte, les suivants sont ignorés.
18
+ */
19
+ export interface IUpgradeAdmission {
20
+ /**
21
+ * Termine le handshake (`101`) et rend la socket ouverte.
22
+ *
23
+ * @returns la socket DIRECTEMENT quand `ws` la rend tout de suite (le cas
24
+ * nominal : aucune promesse, aucune micro-tâche), sinon sa promesse ;
25
+ * `null` si le client est parti pendant la décision (ou si `ws` a rejeté
26
+ * le handshake) — il n'y a alors rien à servir
27
+ */
28
+ accept(): WebSocket | null | Promise<WebSocket | null>;
29
+ /**
30
+ * Refuse par un statut HTTP, AVANT le `101` (RFC 6455 §4.2.2).
31
+ *
32
+ * @param status - statut du refus (`401`, `403`, `429`…)
33
+ * @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`)
34
+ */
35
+ refuse(status: number, headers?: Readonly<Record<string, string | number>> | null): void;
36
+ }
37
+ /** Ce qui décide d'un upgrade : reçoit la requête et rend son verdict, plus tard s'il le faut. */
38
+ export type UpgradeAdmit = (req: IncomingMessage, admission: IUpgradeAdmission) => void;
14
39
  /**
15
40
  * Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
16
41
  * puis le serveur WebSocket de Nodefony.
@@ -24,7 +49,10 @@ interface IUpgradeRelay {
24
49
  *
25
50
  * Entre les deux, `refusal` tranche ce qui doit être refusé AVANT le `101`
26
51
  * (RFC 6455 §4.2.2) : un chemin sans route WebSocket reçoit un statut HTTP,
27
- * jamais une ouverture suivie d'une fermeture.
52
+ * jamais une ouverture suivie d'une fermeture. Puis la FORME du handshake est
53
+ * contrôlée (`websocketHandshakeProblem`, la règle du proxy inverse) : un
54
+ * handshake malformé reçoit `400` sans jamais atteindre le noyau. Enfin
55
+ * `admit` décide — pare-feu, garde de l'action — et seul son verdict ouvre.
28
56
  *
29
57
  * Sans montage, le coût par connexion est une lecture de champ.
30
58
  *
@@ -33,7 +61,9 @@ interface IUpgradeRelay {
33
61
  * @param relay - proxy inverse, lu à chaque upgrade (il peut naître après)
34
62
  * @param refusal - statut HTTP qui refuse l'upgrade, `null` pour l'admettre
35
63
  * (cf `HttpKernel.websocketUpgradeRefusal`)
64
+ * @param admit - décide d'un upgrade admis (cf `HttpKernel.onWebsocketUpgrade`) ;
65
+ * par défaut, tout upgrade conforme est accepté et émis en `connection`
36
66
  * @returns la fonction qui retire l'écouteur — à appeler à l'arrêt
37
67
  */
38
- export declare function attachUpgradeDispatch(server: IUpgradeEmitter, wss: WebSocketServer, relay: () => IUpgradeRelay | null, refusal: (req: IncomingMessage) => number | null): () => void;
68
+ export declare function attachUpgradeDispatch(server: IUpgradeEmitter, wss: WebSocketServer, relay: () => IUpgradeRelay | null, refusal: (req: IncomingMessage) => number | null, admit?: UpgradeAdmit): () => void;
39
69
  export {};
package/docs/cookies.md CHANGED
@@ -112,20 +112,20 @@ Nodefony ne se contente pas de proposer ces attributs : il **choisit des défaut
112
112
  les invariants** que le navigateur exigerait de toute façon — pour que l'erreur ne parte pas sur le fil.
113
113
 
114
114
  **Le défaut est fermé.** Un cookie créé sans options est `Secure` + `HttpOnly` + `SameSite=Lax`
115
- (`cookieDefaultSettings`, `cookie.ts:39`). Il faut **choisir** d'ouvrir (ex. `httpOnly: false` pour un
115
+ (`cookieDefaultSettings`, `cookie.ts#cookieDefaultSettings`). Il faut **choisir** d'ouvrir (ex. `httpOnly: false` pour un
116
116
  cookie lu en JS), jamais choisir de fermer.
117
117
 
118
118
  **Les préfixes sont forcés, pas espérés.** Nommer un cookie `__Host-…` ne suffit pas : `serialize()`
119
- (`cookie.ts:393`) **réécrit** la sortie — `Secure` ajouté, `Path=/` imposé, `Domain` retiré — pour que le
119
+ (`cookie.ts#Cookie.serialize`) **réécrit** la sortie — `Secure` ajouté, `Path=/` imposé, `Domain` retiré — pour que le
120
120
  navigateur ne rejette jamais le cookie en silence. Idem `__Secure-` (Secure imposé) et `SameSite=None` (qui
121
- impose `Secure`, `cookie.ts:393`).
121
+ impose `Secure`, `cookie.ts#Cookie.secure`).
122
122
 
123
123
  **La signature refuse le secret prévisible.** Un cookie `signed: true` sans secret configuré **jette**
124
- (`setValue()`, `cookie.ts:205`) : signer avec le secret public par défaut ne protégerait rien
125
- (fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts:380`).
124
+ (`setValue()`, `cookie.ts#Cookie.setValue`) : signer avec le secret public par défaut ne protégerait rien
125
+ (fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts#Cookie.unsign`).
126
126
 
127
127
  **`SameSite` retombe toujours sur `Lax`, jamais sur `None`.** Toute valeur inconnue est normalisée vers
128
- `Lax` (`setSameSite()`, `cookie.ts:250`) : `None` désactive la protection CSRF, ce n'est jamais un défaut.
128
+ `Lax` (`setSameSite()`, `cookie.ts#Cookie.setSameSite`) : `None` désactive la protection CSRF, ce n'est jamais un défaut.
129
129
 
130
130
  **HTTP et WebSocket lisent les mêmes cookies.** Le parseur tourne pour les deux transports ; en revanche la
131
131
  poignée de main WebSocket **ne peut pas** écrire de cookie (limite de la bibliothèque `ws`) — voir plus bas.
@@ -201,10 +201,13 @@ Pour qu'une valeur ne puisse pas être **falsifiée** côté client (sans la chi
201
201
  secret **configuré** (le secret par défaut est refusé) :
202
202
 
203
203
  ```typescript ignore
204
- // fragment — un secret prévisible est refusé (fail-closed)
204
+ // fragment — un secret prévisible est refusé (fail-closed).
205
+ // `NF_COOKIE_SECRET` se déclare dans `env.ts` (`envString`), seul lecteur de l'environnement.
206
+ import { env } from "../../env";
207
+
205
208
  const c = new Cookie("pref", "v1", {
206
209
  signed: true,
207
- secret: process.env.COOKIE_SECRET!,
210
+ secret: env.NF_COOKIE_SECRET,
208
211
  });
209
212
  // c.value === "s:v1.<hmac base64url>" ; c.unsign() renvoie "v1" ou false si altéré
210
213
  ```
@@ -218,120 +221,124 @@ suit montre l'**usage réel**. La classe s'importe `import { Cookie } from "@nod
218
221
  ### Construire un cookie et ses attributs
219
222
 
220
223
  Le constructeur accepte `(nom, valeur, options?)` ou **un cookie à copier** (surcharge,
221
- `cookie.ts:149`). Les options fusionnent avec les défauts sûrs.
222
-
223
- | Option | Type | Défaut | Effet |
224
- | ---------- | -------------------------- | ------------ | ---------------------------------------------------------------------- |
225
- | `maxAge` | `number` (secondes) | `0` | Durée de vie. `0` = cookie de session (ni `Max-Age` ni `Expires`). |
226
- | `expires` | `Date \| string \| number` | — | Date d'expiration absolue (`setExpires()`, `cookie.ts:272`). |
227
- | `path` | `string` | `/` | Préfixe d'URL de portée. |
228
- | `domain` | `string` | `undefined` | Domaine de portée (retiré si nom `__Host-`). |
229
- | `secure` | `boolean` | `true` | HTTPS only. Forcé si `SameSite=None` ou préfixe `__Host-`/`__Secure-`. |
230
- | `httpOnly` | `boolean` | `true` | Invisible à `document.cookie` (anti-XSS). |
231
- | `sameSite` | `SameSiteType` | `Lax` | `Strict`/`Lax`/`None`. Toute autre valeur retombe sur `Lax`. |
232
- | `signed` | `boolean` | `false` | Signe la valeur (HMAC). Exige `secret` configuré, sinon **jette**. |
233
- | `secret` | `string` | (par défaut) | Clé HMAC. Le secret par défaut est **refusé** pour signer. |
234
- | `priority` | `High \| Medium \| Low` | `undefined` | Attribut `Priority` (`setPriority()`, `cookie.ts:305`). |
235
-
236
- Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts:39`) ; la fusion se fait dans le
237
- constructeur de `Cookie` (`cookie.ts:130`).
224
+ `cookie.ts#Cookie.constructor`). Les options fusionnent avec les défauts sûrs.
225
+
226
+ | Option | Type | Défaut | Effet |
227
+ | ---------- | -------------------------- | ------------ | -------------------------------------------------------------------------- |
228
+ | `maxAge` | `number` (secondes) | `0` | Durée de vie. `0` = cookie de session (ni `Max-Age` ni `Expires`). |
229
+ | `expires` | `Date \| string \| number` | — | Date d'expiration absolue (`setExpires()`, `cookie.ts#Cookie.setExpires`). |
230
+ | `path` | `string` | `/` | Préfixe d'URL de portée. |
231
+ | `domain` | `string` | `undefined` | Domaine de portée (retiré si nom `__Host-`). |
232
+ | `secure` | `boolean` | `true` | HTTPS only. Forcé si `SameSite=None` ou préfixe `__Host-`/`__Secure-`. |
233
+ | `httpOnly` | `boolean` | `true` | Invisible à `document.cookie` (anti-XSS). |
234
+ | `sameSite` | `SameSiteType` | `Lax` | `Strict`/`Lax`/`None`. Toute autre valeur retombe sur `Lax`. |
235
+ | `signed` | `boolean` | `false` | Signe la valeur (HMAC). Exige `secret` configuré, sinon **jette**. |
236
+ | `secret` | `string` | (par défaut) | Clé HMAC. Le secret par défaut est **refusé** pour signer. |
237
+ | `priority` | `High \| Medium \| Low` | `undefined` | Attribut `Priority` (`setPriority()`, `cookie.ts#Cookie.setPriority`). |
238
+
239
+ Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts#cookieDefaultSettings`) ; la fusion se fait dans le
240
+ constructeur de `Cookie` (`cookie.ts#Cookie.options`).
238
241
 
239
242
  ### Sérialiser : `serialize()` et `serializeWebSocket()`
240
243
 
241
- - `serialize()` (`cookie.ts:393`) produit la **ligne `Set-Cookie`** complète (attributs dans l'ordre,
244
+ - `serialize()` (`cookie.ts#Cookie.serialize`) produit la **ligne `Set-Cookie`** complète (attributs dans l'ordre,
242
245
  préfixes forcés, `Max-Age` seulement s'il est positif).
243
- - `serializeWebSocket()` (`cookie.ts:435`) produit un **objet** `IWsCookie` (mêmes invariants de sécurité)
246
+ - `serializeWebSocket()` (`cookie.ts#Cookie.serializeWebSocket`) produit un **objet** `IWsCookie` (mêmes invariants de sécurité)
244
247
  — utilisé quand un cookie doit être décrit hors en-tête HTTP.
245
- - `toString()` (`cookie.ts:325`) ne rend que `nom=valeurEncodée` (sans attributs).
248
+ - `toString()` (`cookie.ts#Cookie.toString`) ne rend que `nom=valeurEncodée` (sans attributs).
246
249
 
247
250
  ### Signer / vérifier : `sign()` et `unsign()`
248
251
 
249
- - `sign(val, secret)` (`cookie.ts:331`) → `val.base64url(HMAC-SHA256)` — la valeur d'origine est
252
+ - `sign(val, secret)` (`cookie.ts#Cookie.sign`) → `val.base64url(HMAC-SHA256)` — la valeur d'origine est
250
253
  **préservée** (récupérable), la signature garantit l'intégrité.
251
- - `unsign(val?, secret?)` (`cookie.ts:355`) → la valeur en clair si la signature est valide, sinon `false`.
252
- Comparaison **timing-safe** (`cookie.ts:380`) ; tolère le préfixe marqueur `s:`.
254
+ - `unsign(val?, secret?)` (`cookie.ts#Cookie.unsign`) → la valeur en clair si la signature est valide, sinon `false`.
255
+ Comparaison **timing-safe** (`cookie.ts#Cookie.unsign`) ; tolère le préfixe marqueur `s:`.
253
256
 
254
257
  ### Lire et écrire depuis le contexte
255
258
 
256
- | Besoin | Appel | Où |
257
- | ------------------------------ | ----------------------------------------------------- | ---------------------------------------- |
258
- | Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts:706`) |
259
- | Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts:193`) |
260
- | Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts:717`) |
261
- | Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts:233` |
259
+ | Besoin | Appel | Où |
260
+ | ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------- |
261
+ | Lire un cookie entrant | `context.getRequestCookies("nom")` → `Cookie \| null` | `getRequestCookies()` (`Context.ts#Context.getRequestCookies`) |
262
+ | Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts#Context.cookies`) |
263
+ | Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts#Context.setCookie`) |
264
+ | Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts#HttpResponse.deleteCookieByName` |
262
265
 
263
- Côté réponse HTTP, `addCookie()` (`http/Response.ts:215`) enregistre le cookie, et `setCookies()`
264
- (`http/Response.ts:241`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
266
+ Côté réponse HTTP, `addCookie()` (`http/Response.ts#HttpResponse.addCookie`) enregistre le cookie, et `setCookies()`
267
+ (`http/Response.ts#HttpResponse.setCookies`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
265
268
  boucle de `setHeader` (qui écraserait tout sauf le dernier). Pour expirer un cookie chez le client :
266
- `clearCookie()` (`cookie.ts:200`) recule `Expires` à l'époque.
269
+ `clearCookie()` (`cookie.ts#Cookie.clearCookie`) recule `Expires` à l'époque.
267
270
 
268
271
  ### Parsing des cookies entrants
269
272
 
270
- `cookiesParser(context)` (`cookie.ts:87`) lit l'en-tête `Cookie:` (via la bibliothèque `cookie`,
271
- `parser()` `cookie.ts:50`), crée un `Cookie` par entrée et l'ajoute au contexte avec `addRequestCookie()`
272
- (`Context.ts:696`). Il est déclenché automatiquement par le pipeline : `parseCookies()` est appelé à
273
- l'initialisation du contexte HTTP (`HttpContext.ts:255`) **et** WebSocket (`WebsocketContext.ts:170`).
273
+ `cookiesParser(context)` (`cookie.ts#cookiesParser`) lit l'en-tête `Cookie:` (via la bibliothèque `cookie`,
274
+ `parser()` `cookie.ts#parser`), crée un `Cookie` par entrée et l'ajoute au contexte avec `addRequestCookie()`
275
+ (`Context.ts#Context.addRequestCookie`). Il est déclenché automatiquement par le pipeline : `parseCookies()` est appelé à
276
+ l'initialisation du contexte HTTP (`HttpContext.ts#HttpContext.constructor`) **et** WebSocket (`WebsocketContext.ts#WebsocketContext.constructor`).
274
277
 
275
278
  ### Côté WebSocket — lecture oui, écriture non
276
279
 
277
280
  Les cookies **entrants** sont lus au handshake, exactement comme en HTTP (même parseur). Mais la **poignée
278
281
  de main WebSocket ne peut pas poser de cookie** : `setCookie()` et `setCookies()` de la réponse WS sont des
279
- **no-op** (`websocket/Response.ts:315`), une limite de la bibliothèque `ws`. Le cookie de session, lui, est
282
+ **no-op** (`websocket/Response.ts#WebsocketResponse.setCookies`), une limite de la bibliothèque `ws`. Le cookie de session, lui, est
280
283
  posé pendant la **phase HTTP** qui précède l'upgrade. La forme d'un cookie décrit pour le WS est
281
- `IWsCookie` (`ICookie.ts:19`), produite par `serializeWebSocket()`.
284
+ `IWsCookie` (`ICookie.ts#IWsCookie`), produite par `serializeWebSocket()`.
282
285
 
283
286
  ## ⚙️ Configuration
284
287
 
285
288
  Les cookies **applicatifs** ne se configurent pas par schéma : on les construit dans le code, avec les
286
- défauts sûrs de `cookieDefaultSettings` (`cookie.ts:39`). Le seul cookie **piloté par la config** est celui
287
- de la **session** — bloc Zod `sessionCookieSchema` (`config.ts:769`), avec notamment `hostPrefix`
288
- (`config.ts:791`) qui décide du préfixe `__Host-`. Tout cela est documenté dans [Sessions](session.md) :
289
+ défauts sûrs de `cookieDefaultSettings` (`cookie.ts#cookieDefaultSettings`). Le seul cookie **piloté par la config** est celui
290
+ de la **session** — bloc Zod `sessionCookieSchema` (`config.ts#sessionCookieSchema`), avec notamment `hostPrefix`
291
+ (`config.ts#sessionCookieSchema`) qui décide du préfixe `__Host-`. Tout cela est documenté dans [Sessions](session.md) :
289
292
  cette page ne le duplique pas.
290
293
 
291
294
  Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport) est calculé par
292
- `getSessionCookieName()` (`Context.ts:757`) — encore un détail qui appartient à la page Sessions.
295
+ `getSessionCookieName()` (`Context.ts#Context.getSessionCookieName`) — encore un détail qui appartient à la page Sessions.
293
296
 
294
297
  ## 🛡️ Défenses par attribut
295
298
 
296
- | Attribut / mécanisme | Faille bloquée | Comment Nodefony l'applique |
297
- | -------------------------- | --------------------------------------- | ------------------------------------------------------------------------ |
298
- | `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts:39`) |
299
- | `SameSite=Lax` (défaut) | **CSRF** inter-site | Fallback toujours `Lax`, jamais `None` (`setSameSite()` `cookie.ts:250`) |
300
- | `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts:393`) |
301
- | Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts:393`) |
302
- | Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts:355`, `cookie.ts:355`) |
303
- | Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts:205`) |
299
+ | Attribut / mécanisme | Faille bloquée | Comment Nodefony l'applique |
300
+ | -------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------- |
301
+ | `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts#cookieDefaultSettings`) |
302
+ | `SameSite=Lax` (défaut) | **CSRF** inter-site | Fallback toujours `Lax`, jamais `None` (`setSameSite()` `cookie.ts#Cookie.setSameSite`) |
303
+ | `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts#Cookie.serialize`) |
304
+ | Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts#Cookie.serialize`) |
305
+ | Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts#Cookie.unsign`) |
306
+ | Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts#Cookie.setValue`) |
304
307
 
305
308
  ## 📜 Normes appliquées
306
309
 
307
- | Domaine | Norme | Ancrage |
308
- | ---------------------------------- | -------------------- | ------------------------------------------------------------------ |
309
- | Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts:393`) |
310
- | `SameSite` — 3 valeurs canoniques | RFC 6265bis §5.4.7 | `SameSiteType` (`ICookie.ts:3`), `setSameSite()` (`cookie.ts:250`) |
311
- | Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts:393`) |
312
- | `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts:393`) |
313
- | Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts:331`) |
314
- | Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts:380`) |
310
+ | Domaine | Norme | Ancrage |
311
+ | ---------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
312
+ | Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts#Cookie.serialize`) |
313
+ | `SameSite` — 3 valeurs canoniques | RFC 6265bis §5.4.7 | `SameSiteType` (`ICookie.ts#SameSiteType`), `setSameSite()` (`cookie.ts#Cookie.setSameSite`) |
314
+ | Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts#Cookie.serialize`) |
315
+ | `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts#Cookie.serialize`) |
316
+ | Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts#Cookie.sign`) |
317
+ | Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts#Cookie.unsign`) |
315
318
 
316
319
  ## ⚠️ Pièges (symptôme → cause → correction)
317
320
 
318
- | Symptôme | Cause | Correction |
319
- | --------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
320
- | Le cookie posé en HTTP clair n'est pas stocké | Défaut `Secure: true` → le navigateur exige HTTPS (sauf `localhost`) | Servir en HTTPS, ou `secure: false` en dev hors localhost |
321
- | Un cookie `__Host-` ignore mon `Domain`/`Path` | `serialize()` **force** les contraintes du préfixe | Attendu (RFC 6265bis) — renommer sans préfixe si tu veux un `Domain` |
322
- | `new Cookie(..., { signed: true })` **jette** | Aucun `secret` configuré → refus du secret prévisible (fail-closed) | Passer un `secret` réel (`{ signed: true, secret: … }`) |
323
- | Modifier un cookie entrant ne change rien côté client | Lecture (`context.cookies`) et écriture (réponse) ne sont pas symétriques | (Re)poser le cookie sur la réponse : `context.setCookie(new Cookie(…))` |
324
- | Le cookie WS posé au handshake n'arrive jamais | `setCookie`/`setCookies` de la réponse WS sont des no-op (`ws`) | Poser le cookie pendant la phase HTTP avant l'upgrade (cf session) |
325
- | Deux `Set-Cookie` s'écrasent, un seul survit | Un `setHeader('Set-Cookie', str)` remplace le précédent | Déjà géré : `setCookies()` passe un **tableau** (`http/Response.ts:241`) |
326
- | `SameSite` mal orthographié devient `Lax` silencieusement | Fallback fail-safe sur `Lax` | Attendu — vérifier la casse ; `Strict`/`Lax`/`None` seulement |
327
- | `maxAge` interprété en millisecondes | `maxAge` est en **secondes** (comme `Set-Cookie` Max-Age) | Passer des secondes (`30*24*60*60`), pas des ms |
321
+ | Symptôme | Cause | Correction |
322
+ | --------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
323
+ | Le cookie posé en HTTP clair n'est pas stocké | Défaut `Secure: true` → le navigateur exige HTTPS (sauf `localhost`) | Servir en HTTPS, ou `secure: false` en dev hors localhost |
324
+ | Un cookie `__Host-` ignore mon `Domain`/`Path` | `serialize()` **force** les contraintes du préfixe | Attendu (RFC 6265bis) — renommer sans préfixe si tu veux un `Domain` |
325
+ | `new Cookie(..., { signed: true })` **jette** | Aucun `secret` configuré → refus du secret prévisible (fail-closed) | Passer un `secret` réel (`{ signed: true, secret: … }`) |
326
+ | Modifier un cookie entrant ne change rien côté client | Lecture (`context.cookies`) et écriture (réponse) ne sont pas symétriques | (Re)poser le cookie sur la réponse : `context.setCookie(new Cookie(…))` |
327
+ | Le cookie WS posé au handshake n'arrive jamais | `setCookie`/`setCookies` de la réponse WS sont des no-op (`ws`) | Poser le cookie pendant la phase HTTP avant l'upgrade (cf session) |
328
+ | Deux `Set-Cookie` s'écrasent, un seul survit | Un `setHeader('Set-Cookie', str)` remplace le précédent | Déjà géré : `setCookies()` passe un **tableau** (`http/Response.ts#HttpResponse.setCookies`) |
329
+ | `SameSite` mal orthographié devient `Lax` silencieusement | Fallback fail-safe sur `Lax` | Attendu — vérifier la casse ; `Strict`/`Lax`/`None` seulement |
330
+ | `maxAge` interprété en millisecondes | `maxAge` est en **secondes** (comme `Set-Cookie` Max-Age) | Passer des secondes (`30*24*60*60`), pas des ms |
328
331
 
329
332
  ## 📡 Observabilité
330
333
 
331
334
  Il n'y a pas d'écran Studio dédié aux cookies applicatifs (le cookie **de session** est surfacé dans
332
335
  l'écran **Sessions**). En développement, chaque écriture de cookie est journalisée en `DEBUG` par la
333
- réponse HTTP (`ADD COOKIE ==> …`, `setCookie()` `http/Response.ts:261`) — visible via le skill
334
- `nodefony-tail-error-logs` ou le Suivi de requête. Sur le fil, un `curl -i` montre les lignes `Set-Cookie`.
336
+ réponse HTTP — nom et attributs, **valeur masquée** (`ADD COOKIE ==> nodefony=<masqué>; Path=/; …`,
337
+ `http/Response.ts#HttpResponse.setCookie`) — visible via le skill `nodefony-tail-error-logs` ou le Suivi
338
+ de requête. La valeur n'y figure jamais : ce journal se lit par le plan d'administration, et un
339
+ identifiant de session y passerait à quiconque le consulte, agent de code compris. La ligne n'est
340
+ construite que si `DEBUG` passe le seuil du journal (debug ciblé compris) : en production, rien n'est
341
+ alloué. La valeur se lit sur le fil : un `curl -i` montre les lignes `Set-Cookie`.
335
342
 
336
343
  ## 🧪 Tests & couverture
337
344