@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +12 -10
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/service/HttpAdminApi.js +2 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +20 -13
- package/dist/nodefony/service/certificates.js +97 -13
- package/dist/nodefony/service/error-renderer.js +29 -12
- package/dist/nodefony/service/http-kernel.js +118 -43
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +3 -3
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
- package/dist/nodefony/service/servers/server-websocket.js +10 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
- package/dist/nodefony/service/sessions/sessions-service.js +7 -14
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +23 -31
- package/dist/nodefony/src/context/domainMatcher.js +1 -2
- package/dist/nodefony/src/context/forwarded.js +1 -2
- package/dist/nodefony/src/context/http/HttpContext.js +7 -6
- package/dist/nodefony/src/context/http/Request.js +33 -29
- package/dist/nodefony/src/context/http/Response.js +23 -6
- package/dist/nodefony/src/context/http/SseStream.js +1 -2
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +1 -2
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +33 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +102 -20
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +29 -5
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +12 -6
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
- package/dist/nodefony/src/session/session.js +2 -3
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +7 -3
- package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -2
- package/dist/types/nodefony/service/certificates.d.ts +55 -4
- package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/src/context/Context.d.ts +13 -4
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
- package/docs/cookies.md +85 -78
- package/docs/observabilite.md +80 -80
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +54 -54
- package/docs/servers.md +338 -224
- package/docs/session.md +179 -179
- package/docs/sse.md +24 -20
- package/docs/upload.md +120 -117
- package/package.json +8 -9
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -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
|
-
/**
|
|
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:
|
|
11
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
125
|
-
(fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
235
|
-
|
|
236
|
-
Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts
|
|
237
|
-
constructeur de `Cookie` (`cookie.ts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
252
|
-
Comparaison **timing-safe** (`cookie.ts
|
|
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
|
|
259
|
-
| Lire tous les cookies entrants | `context.cookies` → `Record<string, Cookie>` | `cookies` (`Context.ts
|
|
260
|
-
| Écrire un cookie sortant | `context.setCookie(new Cookie(…))` | `setCookie()` (`Context.ts
|
|
261
|
-
| Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts
|
|
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
|
|
264
|
-
(`http/Response.ts
|
|
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
|
|
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
|
|
271
|
-
`parser()` `cookie.ts
|
|
272
|
-
(`Context.ts
|
|
273
|
-
l'initialisation du contexte HTTP (`HttpContext.ts
|
|
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
|
|
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
|
|
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
|
|
287
|
-
de la **session** — bloc Zod `sessionCookieSchema` (`config.ts
|
|
288
|
-
(`config.ts
|
|
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
|
|
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
|
|
299
|
-
| `SameSite=Lax` (défaut) | **CSRF** inter-site | Fallback toujours `Lax`, jamais `None` (`setSameSite()` `cookie.ts
|
|
300
|
-
| `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts
|
|
301
|
-
| Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts
|
|
302
|
-
| Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts
|
|
303
|
-
| Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts
|
|
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
|
|
310
|
-
| `SameSite` — 3 valeurs canoniques | RFC 6265bis §5.4.7 | `SameSiteType` (`ICookie.ts
|
|
311
|
-
| Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts
|
|
312
|
-
| `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts
|
|
313
|
-
| Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts
|
|
314
|
-
| Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts
|
|
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
|
|
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
|
|
334
|
-
`nodefony-tail-error-logs` ou le Suivi
|
|
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
|
|