@nodefony/http 10.0.0-alpha.8 → 10.0.0-alpha.9

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.
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
2
2
  function __decorateMetadata(k, v) {
3
3
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
4
  }
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
package/dist/index.js CHANGED
@@ -21,8 +21,8 @@ import DefaultErrorRenderer, { schemaMismatchOf } from "./nodefony/service/error
21
21
  import DefaultRequestLogger from "./nodefony/service/request-logger.js";
22
22
  import JsonAuditLogger from "./nodefony/service/audit-logger.js";
23
23
  import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
24
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
25
- import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
24
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
25
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
26
26
  import http_kernel_default from "./nodefony/service/http-kernel.js";
27
27
  import server_http_default from "./nodefony/service/servers/server-http.js";
28
28
  import server_https_default from "./nodefony/service/servers/server-https.js";
@@ -145,6 +145,103 @@ function isUniqueViolation(error) {
145
145
  return false;
146
146
  }
147
147
  /**
148
+ * Reconnaît une suppression refusée parce que l'entité est encore RÉFÉRENCÉE
149
+ * (`ReferencedEntityError` de `@nodefony/orm-core`) et rend le nom de l'entité
150
+ * qui la désigne.
151
+ *
152
+ * Reconnaissance **structurelle** (`name` + `referencedBy`), sans importer
153
+ * orm-core : `@nodefony/http` n'en dépend pas, et l'erreur reste une erreur de
154
+ * DONNÉES que chaque surface projette elle-même — ici en 409, un conflit d'état
155
+ * (RFC 9110 §15.5.10) : la requête est valide, c'est l'existence d'un enfant qui
156
+ * la refuse.
157
+ *
158
+ * @param error - erreur remontée par le pipeline.
159
+ * @returns le nom de l'entité référençante, ou `null`.
160
+ */
161
+ function referencedByOf(error) {
162
+ let current = error;
163
+ for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
164
+ const candidate = current;
165
+ if (candidate.name === "ReferencedEntityError" && typeof candidate.referencedBy === "string") return candidate.referencedBy;
166
+ current = candidate.cause;
167
+ }
168
+ return null;
169
+ }
170
+ /**
171
+ * Types de colonne dont une valeur mal formée est un IDENTIFIANT mal formé,
172
+ * tel que PostgreSQL le nomme dans son message `22P02`.
173
+ *
174
+ * Restreint aux types qui portent des clés : `22P02` couvre aussi une date ou
175
+ * un JSON illisibles, que le contrat d'entrée rejette avant la base — les
176
+ * retenir ici déguiserait en faute du client une valeur fabriquée par le code.
177
+ */
178
+ const IDENTIFIER_COLUMN_TYPES = /* @__PURE__ */ new Set([
179
+ "uuid",
180
+ "integer",
181
+ "bigint",
182
+ "smallint"
183
+ ]);
184
+ /**
185
+ * Signature figée du message `22P02` de PostgreSQL : le type visé, puis la
186
+ * valeur refusée entre guillemets. Même statut que {@link SQLITE_SCHEMA_PREFIXES} :
187
+ * un début de message stable dans le source du moteur, lu seulement APRÈS le code.
188
+ */
189
+ const PG_INVALID_INPUT = /^invalid input syntax for type (\w+): "(.*)"$/su;
190
+ /**
191
+ * Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
192
+ *
193
+ * Deux moteurs le disent : MongoDB par une `CastError` de Mongoose (la clé est
194
+ * un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier).
195
+ * SQLite et MySQL rangent la clé en texte : ils ne lèvent rien, la lecture rend
196
+ * simplement « rien », d'où leur 404 — c'est la cohérence avec eux qui est visée.
197
+ *
198
+ * Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
199
+ *
200
+ * @param error - erreur remontée par le pipeline.
201
+ * @returns la valeur refusée, ou `null` si ce n'est pas ce cas.
202
+ */
203
+ function malformedIdentifierOf(error) {
204
+ let current = error;
205
+ for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
206
+ const candidate = current;
207
+ if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
208
+ if (String(candidate.code) === "22P02") {
209
+ const match = PG_INVALID_INPUT.exec(candidate.message ?? "");
210
+ if (match && IDENTIFIER_COLUMN_TYPES.has(match[1])) return match[2];
211
+ }
212
+ current = candidate.cause;
213
+ }
214
+ return null;
215
+ }
216
+ /**
217
+ * Statut d'un identifiant mal formé : **404** s'il vient du CHEMIN, **400** sinon.
218
+ *
219
+ * Le chemin DÉSIGNE une ressource : `/api/posts/abc` demande une ressource qui ne
220
+ * peut pas exister, et SQLite comme MySQL rendent déjà 404 — le client reçoit le
221
+ * même statut quel que soit le moteur. Une valeur venue d'ailleurs (filtre,
222
+ * corps) est un PARAMÈTRE invalide : un 404 y dirait « la collection n'existe
223
+ * pas », ce qui est faux. On compare aux segments DÉCODÉS du chemin — l'URL est
224
+ * reçue encodée, la valeur est ce que la base a vu.
225
+ *
226
+ * @param value - la valeur refusée par la base.
227
+ * @param url - l'URL de la requête (`context.url`).
228
+ */
229
+ function malformedIdentifierStatus(value, url) {
230
+ if (!url) return 400;
231
+ let pathname;
232
+ try {
233
+ pathname = new URL(url, "http://nodefony.invalid").pathname;
234
+ } catch {
235
+ return 400;
236
+ }
237
+ for (const segment of pathname.split("/")) try {
238
+ if (decodeURIComponent(segment) === value) return 404;
239
+ } catch {
240
+ if (segment === value) return 404;
241
+ }
242
+ return 400;
243
+ }
244
+ /**
148
245
  * Codes rendus par les pilotes quand la requête porte sur une TABLE ou une
149
246
  * COLONNE que la base n'a pas — la signature d'un schéma en retard sur le code.
150
247
  *
@@ -298,6 +395,19 @@ var DefaultErrorRenderer = class {
298
395
  conflict.stack = error.stack;
299
396
  return new HttpError(conflict, CONFLICT_STATUS, context);
300
397
  }
398
+ const referencedBy = referencedByOf(error);
399
+ if (referencedBy !== null) {
400
+ const conflict = new nodefonyError(`Conflict — this resource is still referenced by ${referencedBy}`, CONFLICT_STATUS);
401
+ conflict.stack = error.stack;
402
+ return new HttpError(conflict, CONFLICT_STATUS, context);
403
+ }
404
+ const malformed = malformedIdentifierOf(error);
405
+ if (malformed !== null) {
406
+ const status = malformedIdentifierStatus(malformed, context?.url);
407
+ const refused = new nodefonyError(status === 404 ? "Not Found — this identifier cannot designate any resource" : "Bad Request — malformed identifier", status);
408
+ refused.stack = error.stack;
409
+ return new HttpError(refused, status, context);
410
+ }
301
411
  const code = error.code;
302
412
  return new HttpError(error, typeof code === "number" ? code : void 0, context);
303
413
  }
@@ -317,4 +427,4 @@ var DefaultErrorRenderer = class {
317
427
  }
318
428
  };
319
429
  //#endregion
320
- export { DefaultErrorRenderer as default, schemaMismatchOf };
430
+ export { DefaultErrorRenderer as default, malformedIdentifierOf, schemaMismatchOf };
@@ -12,8 +12,8 @@ import DefaultRequestLogger from "./request-logger.js";
12
12
  import JsonAuditLogger from "./audit-logger.js";
13
13
  import PrettyRequestLogger from "./pretty-request-logger.js";
14
14
  import { resolveTraceparent } from "./trace.js";
15
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
16
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
15
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
16
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
17
17
  import { GcScheduler, Module, RequestContext, Service, injectable, logColor, nodefonyError, servedUrl, writeRuntimeState } from "nodefony";
18
18
  import cluster from "node:cluster";
19
19
  import Ws from "ws";
@@ -1,10 +1,10 @@
1
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
3
  import http_kernel_default from "../http-kernel.js";
4
4
  import { handleClientError } from "./clientError.js";
5
5
  import { createDrainTerminator } from "./serverShutdown.js";
6
6
  import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
7
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
7
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
8
8
  import { Module, Service, extend, inject, nodefonyError } from "nodefony";
9
9
  import http from "node:http";
10
10
  //#region nodefony/service/servers/server-http.ts
@@ -1,10 +1,10 @@
1
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
3
  import http_kernel_default from "../http-kernel.js";
4
4
  import { handleClientError } from "./clientError.js";
5
5
  import { createDrainTerminator } from "./serverShutdown.js";
6
6
  import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
7
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
7
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
8
8
  import { Module, Service, extend, inject, nodefonyError } from "nodefony";
9
9
  import http2 from "node:http2";
10
10
  import https from "node:https";
@@ -1,7 +1,7 @@
1
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
3
  import http_kernel_default from "../http-kernel.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
5
5
  import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
6
6
  import { Module, Service, inject } from "nodefony";
7
7
  import Ws, { WebSocketServer } from "ws";
@@ -1,7 +1,7 @@
1
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
3
  import http_kernel_default from "../http-kernel.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
5
5
  import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
6
6
  import { Module, Service, inject } from "nodefony";
7
7
  import Ws, { WebSocketServer } from "ws";
@@ -1,12 +1,12 @@
1
1
  import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
2
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
3
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
2
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
3
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
4
4
  import http_kernel_default from "../http-kernel.js";
5
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
5
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
6
6
  import Session from "../../src/session/session.js";
7
7
  import MemorySessionStorage from "../../src/session/storage/MemorySessionStorage.js";
8
8
  import RevocationGuardStorage from "../../src/session/storage/RevocationGuardStorage.js";
9
- import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Module, Nodefony, RequestContext, Service, countFacets, extend, inject, injectable, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
9
+ import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Module, Nodefony, RequestContext, Service, countFacets, extend, inject, injectable, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices } from "nodefony";
10
10
  import { createHash, createHmac } from "node:crypto";
11
11
  //#region nodefony/service/sessions/sessions-service.ts
12
12
  var _SessionsService;
@@ -173,7 +173,8 @@ let SessionsService = class SessionsService extends Service {
173
173
  available: _SessionsService.storageHandlers(),
174
174
  reason,
175
175
  configPath: "http.session.store",
176
- location: readStoreLocation(innerStorage)
176
+ location: readStoreLocation(innerStorage),
177
+ connector: readStoreConnector(innerStorage)
177
178
  });
178
179
  await storage.open();
179
180
  this.gcScheduler = new GcScheduler({
@@ -1,7 +1,7 @@
1
- import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
3
  import http_kernel_default from "../http-kernel.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
5
5
  import { Cli, FileClass, Module, Service, inject } from "nodefony";
6
6
  import fs from "node:fs";
7
7
  import fsp from "node:fs/promises";
@@ -1,5 +1,19 @@
1
1
  import type { IErrorRenderer, IErrorHttpResult, IErrorWebsocketResult } from "../interfaces/IErrorRenderer.js";
2
2
  import type { IHttpContext, IWebsocketContext } from "../interfaces/IContext.js";
3
+ /**
4
+ * Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
5
+ *
6
+ * Deux moteurs le disent : MongoDB par une `CastError` de Mongoose (la clé est
7
+ * un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier).
8
+ * SQLite et MySQL rangent la clé en texte : ils ne lèvent rien, la lecture rend
9
+ * simplement « rien », d'où leur 404 — c'est la cohérence avec eux qui est visée.
10
+ *
11
+ * Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
12
+ *
13
+ * @param error - erreur remontée par le pipeline.
14
+ * @returns la valeur refusée, ou `null` si ce n'est pas ce cas.
15
+ */
16
+ export declare function malformedIdentifierOf(error: unknown): string | null;
3
17
  /**
4
18
  * Force du signal « la base ne porte pas ce que le code demande ».
5
19
  *
package/docs/cookies.md CHANGED
@@ -112,7 +112,7 @@ 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:43`). Il faut **choisir** d'ouvrir (ex. `httpOnly: false` pour un
115
+ (`cookieDefaultSettings`, `cookie.ts:39`). 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()`
@@ -121,7 +121,7 @@ navigateur ne rejette jamais le cookie en silence. Idem `__Secure-` (Secure impo
121
121
  impose `Secure`, `cookie.ts:393`).
122
122
 
123
123
  **La signature refuse le secret prévisible.** Un cookie `signed: true` sans secret configuré **jette**
124
- (`setValue()`, `cookie.ts:211`) : signer avec le secret public par défaut ne protégerait rien
124
+ (`setValue()`, `cookie.ts:199`) : signer avec le secret public par défaut ne protégerait rien
125
125
  (fail-closed). La vérification est **timing-safe** (`unsign()` → `crypto.timingSafeEqual`, `cookie.ts:380`).
126
126
 
127
127
  **`SameSite` retombe toujours sur `Lax`, jamais sur `None`.** Toute valeur inconnue est normalisée vers
@@ -233,7 +233,7 @@ Le constructeur accepte `(nom, valeur, options?)` ou **un cookie à copier** (su
233
233
  | `secret` | `string` | (par défaut) | Clé HMAC. Le secret par défaut est **refusé** pour signer. |
234
234
  | `priority` | `High \| Medium \| Low` | `undefined` | Attribut `Priority` (`setPriority()`, `cookie.ts:297`). |
235
235
 
236
- Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts:43`) ; la fusion se fait dans le
236
+ Les défauts sont matérialisés dans `cookieDefaultSettings` (`cookie.ts:39`) ; la fusion se fait dans le
237
237
  constructeur de `Cookie` (`cookie.ts:130`).
238
238
 
239
239
  ### Sérialiser : `serialize()` et `serializeWebSocket()`
@@ -261,14 +261,14 @@ constructeur de `Cookie` (`cookie.ts:130`).
261
261
  | Supprimer un cookie sortant | `response.deleteCookieByName("nom")` | `http/Response.ts:119` |
262
262
 
263
263
  Côté réponse HTTP, `addCookie()` (`http/Response.ts:101`) enregistre le cookie, et `setCookies()`
264
- (`http/Response.ts:107`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
264
+ (`http/Response.ts:127`) émet **une ligne `Set-Cookie` par cookie** — un tableau passé à Node, jamais une
265
265
  boucle de `setHeader` (qui écraserait tout sauf le dernier). Pour expirer un cookie chez le client :
266
- `clearCookie()` (`cookie.ts:198`) recule `Expires` à l'époque.
266
+ `clearCookie()` (`cookie.ts:194`) recule `Expires` à l'époque.
267
267
 
268
268
  ### Parsing des cookies entrants
269
269
 
270
- `cookiesParser(context)` (`cookie.ts:91`) lit l'en-tête `Cookie:` (via la bibliothèque `cookie`,
271
- `parser()` `cookie.ts:54`), crée un `Cookie` par entrée et l'ajoute au contexte avec `addRequestCookie()`
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
272
  (`Context.ts:650`). Il est déclenché automatiquement par le pipeline : `parseCookies()` est appelé à
273
273
  l'initialisation du contexte HTTP (`HttpContext.ts:190`) **et** WebSocket (`WebsocketContext.ts:170`).
274
274
 
@@ -283,7 +283,7 @@ posé pendant la **phase HTTP** qui précède l'upgrade. La forme d'un cookie d
283
283
  ## ⚙️ Configuration
284
284
 
285
285
  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:43`). Le seul cookie **piloté par la config** est celui
286
+ défauts sûrs de `cookieDefaultSettings` (`cookie.ts:39`). Le seul cookie **piloté par la config** est celui
287
287
  de la **session** — bloc Zod `sessionCookieSchema` (`config.ts:748`), avec notamment `hostPrefix`
288
288
  (`config.ts:770`) qui décide du préfixe `__Host-`. Tout cela est documenté dans [Sessions](session.md) :
289
289
  cette page ne le duplique pas.
@@ -295,11 +295,11 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
295
295
 
296
296
  | Attribut / mécanisme | Faille bloquée | Comment Nodefony l'applique |
297
297
  | -------------------------- | --------------------------------------- | ------------------------------------------------------------------------ |
298
- | `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts:43`) |
298
+ | `HttpOnly` (défaut `true`) | Vol de cookie par **XSS** | Défaut fermé (`cookieDefaultSettings`, `cookie.ts:39`) |
299
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:399`) |
302
- | Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts:331`, `cookie.ts:380`) |
300
+ | `Secure` (défaut `true`) | Interception en clair | Forcé aussi par `None`/préfixes (`serialize()` `cookie.ts:383`) |
301
+ | Préfixe `__Host-` | **Session fixation** cross-sous-domaine | `Domain` retiré + `Path=/` imposés (`serialize()` `cookie.ts:383`) |
302
+ | Cookie signé (HMAC) | Altération de la valeur côté client | `sign()`/`unsign()` timing-safe (`cookie.ts:355`, `cookie.ts:355`) |
303
303
  | Refus du secret par défaut | Signature « fantôme » sans protection | Fail-closed à la signature (`setValue()` `cookie.ts:199`) |
304
304
 
305
305
  ## 📜 Normes appliquées
@@ -308,8 +308,8 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
308
308
  | ---------------------------------- | -------------------- | ------------------------------------------------------------------ |
309
309
  | Cookies — syntaxe `Set-Cookie` | RFC 6265bis | `serialize()` (`cookie.ts:383`) |
310
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:390`) |
312
- | `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts:393`) |
311
+ | Préfixes `__Host-` / `__Secure-` | RFC 6265bis §4.1.3 | `serialize()` force les contraintes (`cookie.ts:383`) |
312
+ | `SameSite=None` impose `Secure` | RFC 6265bis | `serialize()` (`cookie.ts:383`) |
313
313
  | Intégrité — HMAC-SHA256, base64url | RFC 2104 / RFC 4648 | `sign()` (`cookie.ts:331`) |
314
314
  | Vérification à temps constant | bonne pratique OWASP | `unsign()` → `timingSafeEqual` (`cookie.ts:380`) |
315
315
 
@@ -322,7 +322,7 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
322
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
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
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:117`) |
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:127`) |
326
326
  | `SameSite` mal orthographié devient `Lax` silencieusement | Fallback fail-safe sur `Lax` | Attendu — vérifier la casse ; `Strict`/`Lax`/`None` seulement |
327
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 |
328
328
 
@@ -330,7 +330,7 @@ Le nom effectif du cookie de session (avec ou sans `__Host-` selon le transport)
330
330
 
331
331
  Il n'y a pas d'écran Studio dédié aux cookies applicatifs (le cookie **de session** est surfacé dans
332
332
  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:126`) — visible via le skill
333
+ réponse HTTP (`ADD COOKIE ==> …`, `setCookie()` `http/Response.ts:146`) — visible via le skill
334
334
  `nodefony-tail-error-logs` ou le Suivi de requête. Sur le fil, un `curl -i` montre les lignes `Set-Cookie`.
335
335
 
336
336
  ## 🧪 Tests & couverture
@@ -229,9 +229,9 @@ GET 200 /trace/whoami 3.1ms 127.0.0.1 [demo-abc]
229
229
  | Adoption WS | `sanitizeRequestId(...)` au handshake (`WebsocketContext.ts:139`) | Même validation, stable sur toute la durée de la socket (handshake → close). |
230
230
  | Réflexion HTTP/1.1 | `Response.setHeader("x-request-id", …)` (`Response.ts:153`) | Écrit dans `writeHead()`, sur **chaque** réponse. |
231
231
  | Réflexion HTTP/2 | `this.headers["x-request-id"] = requestId` (`http2/Response.ts:71`) | Sinon les réponses du port 5152 sortiraient sans corrélation. |
232
- | ALS (HTTP) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:431`) | Ouvre la bulle → tout `Pdu` créé dedans est tagué. |
233
- | ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:431`) | Handshake **et** messages (via `AsyncResource.bind`, BUG-001). |
234
- | Capture dans le log | `Pdu.requestId = Pdu.requestIdProvider?.()` (`Pdu.ts:221`) | Provider injectable branché sur l'ALS côté Node — 0 lecture côté navigateur. |
232
+ | ALS (HTTP) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:436`) | Ouvre la bulle → tout `Pdu` créé dedans est tagué. |
233
+ | ALS (WS) | `RequestContext.run({ requestId, … })` (`http-kernel.ts:436`) | Handshake **et** messages (via `AsyncResource.bind`, BUG-001). |
234
+ | Capture dans le log | `Pdu.requestId = Pdu.requestIdProvider?.()` (`Pdu.ts:262`) | Provider injectable branché sur l'ALS côté Node — 0 lecture côté navigateur. |
235
235
 
236
236
  > [!IMPORTANT]
237
237
  > Les logs de **fin** de requête (bilan `req`, `onClose`) sont émis **hors** de la bulle ALS (déjà
@@ -269,7 +269,7 @@ de bilan, au teardown, via `Context.logRequest()` (`Context.ts:595`) côté HTTP
269
269
 
270
270
  Chaque frame WS (RECEIVE / SEND / BROADCAST) peut être tracée pour le Suivi de requête (Studio). Le
271
271
  formatage du contenu est **pur** et **borné** — `formatWsLogContent()` (`wsLogContent.ts:55`), appelé par
272
- `WebsocketContext.logMessageContent()` (`WebsocketContext.ts:397`) :
272
+ `WebsocketContext.logMessageContent()` (`WebsocketContext.ts:393`) :
273
273
 
274
274
  - `string` → tronquée à `WS_LOG_CONTENT_CAP` (4096, `wsLogContent.ts:15`) + ellipse.
275
275
  - **binaire** (Buffer, ArrayBuffer, TypedArray, Blob, `Buffer[]`) → résumé `[binary N B]`, **jamais**
@@ -316,7 +316,7 @@ Choisir en cinq secondes :
316
316
  Le plus grand gain en dev : `méthode statut url durée remote [id]`, avec couleur du statut (vert 2xx,
317
317
  jaune 4xx, rouge 5xx — `colorizeStatus()`, `pretty-request-logger.ts:100`) et `requestId` tronqué aux 8
318
318
  premiers caractères (`shortId()`, `pretty-request-logger.ts:116`). La durée est dérivée des phases de
319
- timing (`pretty-request-logger.ts:121`).
319
+ timing (`pretty-request-logger.ts:116`).
320
320
 
321
321
  ### `JsonAuditLogger` — un PDU par requête, pour la machine
322
322
 
@@ -372,7 +372,7 @@ Les choix visibles dans le code :
372
372
  - **Audit nominal coupable** — l'option `nominal` coupe le log des 2xx/3xx si le sink texte est `null`
373
373
  (l'entrée n'atteindrait aucune destination) — ~5,9 % du profil CPU récupérés (`audit-logger.ts:104`).
374
374
  - **Trace des frames WS gatée en prod** — `logMessageContent()` court-circuite avant toute construction
375
- de chaîne hors dev (`WebsocketContext.ts:401`) ; les events lifecycle ne créent aucun `Pdu` en production (`Context.ts:67`).
375
+ de chaîne hors dev (`WebsocketContext.ts:393`) ; les events lifecycle ne créent aucun `Pdu` en production (`Context.ts:67`).
376
376
 
377
377
  Gate mémoire avant tout commit touchant le pipeline : `npm run test:memory` (skill
378
378
  `nodefony-check-memory-health`). Rejouer les chiffres de charge : skill `nodefony-load-test`.
@@ -404,9 +404,9 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
404
404
  | Domaine | Norme | Ancrage |
405
405
  | --- | --- | --- |
406
406
  | W3C Trace Context (`traceparent`) | W3C Trace Context | `resolveTraceparent()` (`trace.ts:83`), `parseTraceparent()` (`trace.ts:38`) |
407
- | Sûreté des valeurs d'en-tête (field-value) | RFC 9110 §5.5 | `sanitizeRequestId()` allowlist (`requestId.ts:26`) |
407
+ | Sûreté des valeurs d'en-tête (field-value) | RFC 9110 §5.5 | `sanitizeRequestId()` allowlist (`requestId.ts:38`) |
408
408
  | En-têtes trop volumineux / borne | anti-abus (log flooding) | `MAX_REQUEST_ID_LENGTH` (`requestId.ts:18`) |
409
- | Log structuré (PDU, sévérités) | RFC 5424 | `Pdu` + `requestId`/`pid` (`Pdu.ts:157`) |
409
+ | Log structuré (PDU, sévérités) | RFC 5424 | `Pdu` + `requestId`/`pid` (`Pdu.ts:184`) |
410
410
  | Sévérité dérivée du statut HTTP | RFC 9110 (catégories) | `severityFromStatus()` (`audit-logger.ts:71`) |
411
411
  | Non-journalisation des secrets | OWASP (logging) | redaction présence-only (`audit-logger.ts:211`) |
412
412
 
@@ -420,7 +420,7 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
420
420
  | Le `X-Request-Id` que j'envoie n'est pas réfléchi | Valeur non conforme (espace, CR/LF, non-ASCII, > 128) → **rejetée** | Utiliser `[A-Za-z0-9._-]{1,128}` (UUID/nanoid/traceparent OK) — sinon UUID serveur |
421
421
  | Les logs de fin de requête n'ont pas de `requestId` | Ils sont émis hors bulle ALS | Déjà géré : l'override `log()` rouvre une micro-bulle (`Context.ts:459`) |
422
422
  | Réponse HTTP/2 sans `x-request-id` | Chemin de réponse h2 distinct du 1.1 | Déjà géré (`http2/Response.ts:71`) — le port 5152 réfléchit aussi |
423
- | Pas de `traceparent` renvoyé sur un WebSocket | `ws` n'expose pas l'écriture d'en-tête au handshake | Attendu — la trace WS reste propagée en ALS (`http-kernel.ts:1505`) |
423
+ | Pas de `traceparent` renvoyé sur un WebSocket | `ws` n'expose pas l'écriture d'en-tête au handshake | Attendu — la trace WS reste propagée en ALS (`http-kernel.ts:1348`) |
424
424
  | Frame WS binaire loggée en `{"0":..,"1":..}` | Sérialisation naïve d'un Buffer | Déjà géré : résumé `[binary N B]` (`wsLogContent.ts:63`) |
425
425
  | Le format de log ne change pas malgré la config | Un `setRequestLogger(...)` programmatique gagne sur la config | L'override est volontaire (last setter wins) — retirer l'appel, ou le régler |
426
426
  | Logs d'audit trop volumineux en prod | `stack` sérialisée, ou 100 % des 2xx audités | `includeStack:false` (défaut prod) + `sampleRate` via `setRequestLogger` |
@@ -203,8 +203,8 @@ flowchart TD
203
203
 
204
204
  Autour de ce cœur, le kernel orchestre le cycle de vie :
205
205
 
206
- - **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:412`) instancie le store
207
- depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:412`) et arme un `GcScheduler`
206
+ - **Construction / reconfiguration** : `configureRateLimit()` (`http-kernel.ts:417`) instancie le store
207
+ depuis la config (`windowMs = windowS × 1000`, `http-kernel.ts:417`) et arme un `GcScheduler`
208
208
  (`http-kernel.ts:422`) qui **purge les fenêtres expirées** hors du chemin chaud.
209
209
  - **Émission HTTP** : sous le quota, les en-têtes `X-RateLimit-*` sont posés (`http-kernel.ts:1000`) et
210
210
  la requête continue ; au-delà, `Retry-After` (`http-kernel.ts:1007`) puis `writeHead(429)`
@@ -219,10 +219,10 @@ schéma, écrits ici pour les montrer.
219
219
 
220
220
  | Option | Type | Défaut | Effet | Chaud |
221
221
  | ------------- | ------------ | --------- | -------------------------------------------------------------------------------- | ----- |
222
- | `enabled` | bool | `false` | Arme le rate-limit (HTTP **et** handshakes WS, même compteur) (`config.ts:849`). | oui |
223
- | `windowS` | int (s) | `60` | Largeur de la fenêtre fixe ; le compteur par IP repart à zéro (`config.ts:860`). | oui |
222
+ | `enabled` | bool | `false` | Arme le rate-limit (HTTP **et** handshakes WS, même compteur) (`config.ts:870`). | oui |
223
+ | `windowS` | int (s) | `60` | Largeur de la fenêtre fixe ; le compteur par IP repart à zéro (`config.ts:881`). | oui |
224
224
  | `max` | int | `300` | Requêtes/IP/fenêtre ; au-delà `429` + `Retry-After` (`config.ts:892`). | oui |
225
- | `maxTracked` | int (≥ 1000) | `100 000` | Borne mémoire : IP suivies ; au cap, purge puis éviction FIFO (`config.ts:883`). | non |
225
+ | `maxTracked` | int (≥ 1000) | `100 000` | Borne mémoire : IP suivies ; au cap, purge puis éviction FIFO (`config.ts:904`). | non |
226
226
  | `gcIntervalS` | int (s) | `300` | Intervalle du balayage de purge des fenêtres expirées, hors hot-path. | non |
227
227
  | `gcJitter` | bool | `true` | Étale le tick GC d'un jitter aléatoire (anti-thundering-herd multi-pod). | non |
228
228
 
@@ -230,7 +230,7 @@ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
230
230
 
231
231
  | Option | Type | Défaut | Effet | Chaud |
232
232
  | ----------------------- | ------------- | ------ | --------------------------------------------------------------------------------------------------- | ----- |
233
- | `wsMaxConnectionsPerIp` | int \| `null` | `null` | Cap de connexions WS **concurrentes** par IP ; au-delà, upgrade fermé en `1013` (`config.ts:1046`). | oui |
233
+ | `wsMaxConnectionsPerIp` | int \| `null` | `null` | Cap de connexions WS **concurrentes** par IP ; au-delà, upgrade fermé en `1013` (`config.ts:1067`). | oui |
234
234
 
235
235
  > [!TIP]
236
236
  > `max: 300` sur `windowS: 60` = **5 req/s soutenu** par IP, avec des rafales tolérées jusqu'à 300 d'un
@@ -242,14 +242,14 @@ Et un réglage **séparé**, propre au WebSocket, à la racine du module :
242
242
  Un WebSocket ne peut **pas** recevoir un `429` : au moment où le rate-limit décide, le `101 Switching
243
243
  Protocols` est déjà parti sur le fil (émis par la bibliothèque `ws`). Le refoulement se fait donc par
244
244
  une **fermeture RFC 6455 `1013 Try Again Later`**, décidée dans `onWebsocketRequest()`
245
- (`http-kernel.ts:1540`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
245
+ (`http-kernel.ts:1549`) — **avant** `enterScope`, l'ALS et le pipeline, comme le `429` HTTP.
246
246
 
247
247
  Deux plafonds distincts, tous deux par IP forwarded-aware :
248
248
 
249
249
  | Plafond | Ce qu'il borne | Source de config | Refus |
250
250
  | ---------------------- | ---------------------------------------------- | ----------------------- | ---------------------------------------------------------- |
251
- | Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:1521`) |
252
- | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:1531`) |
251
+ | Débit de handshakes | Ouvertures/seconde (le **même** compteur HTTP) | `rateLimit` | close `1013` (`http-kernel.ts:1574`) |
252
+ | Connexions simultanées | Sockets **ouvertes** en même temps par IP | `wsMaxConnectionsPerIp` | close `1013` — `tryAcquire` refuse (`http-kernel.ts:1583`) |
253
253
 
254
254
  Le cap concurrent est porté par un compteur dédié, `WsConnectionCounter` (`WsConnectionCounter.ts:18`) :
255
255
  `tryAcquire(ip)` (`WsConnectionCounter.ts:33`) réserve un créneau à l'upgrade, `release(ip)`
package/docs/servers.md CHANGED
@@ -128,12 +128,12 @@ Nodefony crée un serveur HTTP/2 sécurisé avec `allowHTTP1: true` (`ServerHttp
128
128
 
129
129
  **Le WebSocket n'est jamais un citoyen de seconde zone.** Il est adossé au serveur HTTP porteur
130
130
  (`server-websocket.ts:80`), passe par le **même** rate-limit d'IP que les requêtes HTTP — un upgrade
131
- _est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1540`) —, hérite de la même
131
+ _est_ une requête HTTP (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1549`) —, hérite de la même
132
132
  session et du même firewall, et se ferme avec le même soin qu'une réponse HTTP.
133
133
 
134
134
  > [!NOTE]
135
135
  > Le serveur HTTP/3 (QUIC) est **réservé, pas implémenté** : la clé `http3` existe dans le schéma,
136
- > marquée `reserved` (`config.ts:1035`). Elle ne fait rien aujourd'hui.
136
+ > marquée `reserved` (`config.ts:1050`). Elle ne fait rien aujourd'hui.
137
137
 
138
138
  ## 🚀 Démarrage rapide
139
139
 
@@ -249,7 +249,7 @@ export default PingController;
249
249
 
250
250
  ### 4. Ce qu'on observe au boot
251
251
 
252
- Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1214`), puis affiche les URL réellement
252
+ Le kernel démarre les serveurs à la phase `onReady` (`Kernel.ts:1255`), puis affiche les URL réellement
253
253
  en écoute — le récap de développement liste HTTP, HTTP/2, WS et WSS dans cet ordre
254
254
  (`BootReporter.ts:389`) :
255
255
 
@@ -264,7 +264,7 @@ en écoute — le récap de développement liste HTTP, HTTP/2, WS et WSS dans ce
264
264
  ```
265
265
 
266
266
  Hors écran animé (production, CI, `--debug`), ce sont les bannières par serveur qui sortent
267
- (`ServerHttp.showBanner()`, `server-http.ts:226`, appelées par le kernel — `Kernel.ts:545`) :
267
+ (`ServerHttp.showBanner()`, `server-http.ts:232`, appelées par le kernel — `Kernel.ts:561`) :
268
268
 
269
269
  ```text
270
270
  Server Listen on http://127.0.0.1:5151 Family: IPv4 Protocol : 1.1
@@ -327,19 +327,19 @@ Choisir en cinq secondes :
327
327
  | `server-websocket-secure` | `wss://` — 5152 | `server-https` actif | WebSocket adossé au serveur TLS. |
328
328
  | `server-static` | (aucun port propre) | toujours enregistré | Fichiers statiques, en **repli** après le routing. |
329
329
 
330
- L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1033`) : chaque serveur est
330
+ L'assemblage est fait par `HttpKernel.initServers()` (`http-kernel.ts:1046`) : chaque serveur est
331
331
  consulté sur son drapeau `active`, un serveur désactivé est **sauté**, pas créé (`http-kernel.ts:235`).
332
332
  Les serveurs WebSocket ne sont montés que si leur porteur l'a été.
333
333
 
334
334
  ### `server-http` — HTTP/1.1 en clair
335
335
 
336
336
  Le plus simple, et celui qui porte le trafic en cloud-native. `ServerHttp.createServer()`
337
- (`server-http.ts:68`) crée le `http.Server`, applique les réglages de transport, branche
337
+ (`server-http.ts:74`) crée le `http.Server`, applique les réglages de transport, branche
338
338
  `onHttpRequest`, puis écoute selon la politique de port.
339
339
 
340
340
  - **Actif** si `servers.http` n'est pas `false` — la décision est prise dès le constructeur
341
341
  (`server-http.ts:56`), et le port est lu de la config d'app (`ServerHttp.setPort()`,
342
- `server-http.ts:61`).
342
+ `server-http.ts:67`).
343
343
  - **Réglages appliqués** : `requestTimeout` (anti slow-loris), `maxHeadersCount`, `timeout` de socket
344
344
  (qui émet un événement `onTimeout`), `keepAliveTimeout`.
345
345
  - **Erreurs de protocole** : l'événement `clientError` est traité explicitement
@@ -347,12 +347,12 @@ Le plus simple, et celui qui porte le trafic en cloud-native. `ServerHttp.create
347
347
 
348
348
  ### `server-https` — TLS, et HTTP/2 par défaut
349
349
 
350
- Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts:88`) :
350
+ Deux branches dans un seul service, choisies sur `servers.https.protocol` (`server-https.ts:39`) :
351
351
 
352
352
  - **`"2.0"` (défaut)** → `http2.createSecureServer` avec `allowHTTP1: true`
353
353
  (`ServerHttps.createServerH2()`, `server-https.ts:174`). Les bornes anti-DoS HTTP/2 ne sont posées
354
354
  **que si elles sont configurées**, pour ne pas écraser les défauts de Node
355
- (`maxSessionMemory`, `server-https.ts:197`).
355
+ (`maxSessionMemory`, `server-https.ts:185`).
356
356
  Les erreurs de session et de flux sont journalisées sans tuer le serveur
357
357
  (`sessionError`, `server-https.ts:274`).
358
358
  - **`"1.1"`** → `https.createServer` classique.
@@ -400,7 +400,7 @@ C'est la distinction la plus utile de cette page, et celle qu'on rate le plus so
400
400
  | Question | Où ça se règle | Source |
401
401
  | ----------------------------------------- | ------------------------------ | --------------------------------------------------------- |
402
402
  | **Quels** serveurs, sur **quels ports** ? | `servers` (config d'app) | `serversSchema` (`src/nodefony/src/config/schema.ts:149`) |
403
- | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:953`) |
403
+ | **Comment** ces serveurs se comportent ? | `use("@nodefony/http", { … })` | `httpConfigSchema` (`config.ts:989`) |
404
404
 
405
405
  Autrement dit : la **topologie** est une propriété du déploiement (elle change entre le poste du dev,
406
406
  la CI et le cluster) ; le **réglage** est une propriété de l'application.
@@ -421,7 +421,7 @@ Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaul
421
421
 
422
422
  ### Niveau 2 — le transport HTTP / HTTPS
423
423
 
424
- Table dérivée de `httpServerSchema` (`config.ts:257`) ; la section `https` reprend les mêmes clés et en
424
+ Table dérivée de `httpServerSchema` (`config.ts:272`) ; la section `https` reprend les mêmes clés et en
425
425
  ajoute une (`httpsServerSchema`, `config.ts:336`).
426
426
 
427
427
  | Option | Type | Défaut | Effet |
@@ -442,7 +442,7 @@ quelle à Node. C'est délibéré — un schéma strict effacerait silencieuseme
442
442
  ### Niveau 2 — HTTP/2
443
443
 
444
444
  Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
445
- (`maxSessionMemory`, `server-https.ts:197`).
445
+ (`maxSessionMemory`, `server-https.ts:185`).
446
446
 
447
447
  | Option | Type | Défaut | Effet |
448
448
  | ---------------------- | ---- | ------ | ----------------------------------------------------------------------------- |
@@ -452,7 +452,7 @@ Depuis `http2Schema` (`config.ts:353`), appliqué seulement si défini
452
452
  ### Niveau 2 — WebSocket (`websocket` et `websocketSecure`)
453
453
 
454
454
  Depuis `websocketSchema` (`config.ts:517`). Les deux sections partagent la forme et les défauts ; le WSS
455
- lit `websocketSecure` (`config.ts:1043`).
455
+ lit `websocketSecure` (`config.ts:1058`).
456
456
 
457
457
  | Option | Type | Défaut | Effet |
458
458
  | ------------------------ | ------------------- | ------- | ---------------------------------------------------------------------------------- |
@@ -484,7 +484,7 @@ panne, c'est une nuisance.
484
484
 
485
485
  ### La règle
486
486
 
487
- `resolvePortPolicy()` (`portBinder.ts:74`) tranche selon l'environnement, et la valeur explicite gagne
487
+ `resolvePortPolicy()` (`portBinder.ts:93`) tranche selon l'environnement, et la valeur explicite gagne
488
488
  toujours :
489
489
 
490
490
  | Environnement | Défaut | Pourquoi |
@@ -507,10 +507,10 @@ détails d'implémentation valent d'être connus, parce qu'ils expliquent des co
507
507
  1. **On retente au `listen()`, jamais après une sonde.** Demander « le port est-il libre ? » puis
508
508
  binder est une course : entre la réponse et le bind, un autre processus peut prendre le port. Le
509
509
  `listen()` est atomique — on retente donc sur l'échec réel (`bindWithFallback()`,
510
- `portBinder.ts:142`).
510
+ `portBinder.ts:281`).
511
511
  2. **Le port de l'autre serveur est réservé.** Si HTTP est chassé de 5151, incrémenter naïvement le
512
512
  ferait voler 5152 à HTTPS, qui se décalerait à son tour. Les ports convoités par les autres serveurs
513
- sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts:104`, réservation `portBinder.ts:111`).
513
+ sont sautés d'emblée (`buildBindPlan()`, `portBinder.ts:123`, réservation `portBinder.ts:123`).
514
514
  3. **Le gestionnaire d'erreur durable est posé APRÈS le bind.** Attaché avant, il verrait passer les
515
515
  `EADDRINUSE` de repli et terminerait le kernel en croyant à une panne
516
516
  (`ServerHttp.attachErrorHandler()`, `server-http.ts:186`).
@@ -537,12 +537,12 @@ serveur qui, lui, écoute très bien.
537
537
  **Générer un certificat est un confort de développement, pas une fonction de production.** Nodefony
538
538
  n'est pas une autorité de certification : en production, on fournit un vrai certificat (Let's Encrypt,
539
539
  ingress k8s, reverse-proxy). Le service crie un avertissement si ce n'est pas le cas
540
- (`Certificate.resolveStrategy()`, `certificates.ts:418`).
540
+ (`Certificate.resolveStrategy()`, `certificates.ts:407`).
541
541
 
542
542
  ### Les quatre stratégies
543
543
 
544
544
  Réglées par `certificates.strategy` (`certificatesSchema`, `config.ts:475`), résolues par
545
- `Certificate.resolveStrategy()` (`certificates.ts:379`).
545
+ `Certificate.resolveStrategy()` (`certificates.ts:407`).
546
546
 
547
547
  | Stratégie | Quand l'utiliser | Ce qui se passe |
548
548
  | --------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
@@ -588,7 +588,7 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
588
588
  | Exigence | Norme | Mise en œuvre |
589
589
  | --------------------------------------- | -------------------- | --------------------------------------------------------- |
590
590
  | Signature SHA-256, **jamais** SHA-1 | RFC 5280, CA/B Forum | `selfSigned.hash` par défaut `sha256` (`config.ts:400`) |
591
- | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:264`) |
591
+ | Numéro de série aléatoire 128 bits | RFC 5280 §4.1.2.2 | `Certificate.generateSerialHex()` (`certificates.ts:292`) |
592
592
  | Le SAN fait foi, pas le CN | RFC 6125 | SAN dérivé du kernel si non fourni (`config.ts:444`) |
593
593
  | `notBefore` reculé (décalage d'horloge) | pratique | `selfSigned.backdateMinutes`, défaut 5 (`config.ts:415`) |
594
594
  | Clé privée non lisible par tous | hygiène | `privateKeyMode` `0600` (`config.ts:499`) |
@@ -596,7 +596,7 @@ invalid »). Celui de Nodefony respecte les règles qui comptent :
596
596
  ### Régénération automatique
597
597
 
598
598
  Un certificat présent sur disque n'est pas forcément **adéquat**. `Certificate.isCertAdequate()`
599
- (`certificates.ts:542`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
599
+ (`certificates.ts:570`) le régénère s'il est expiré, s'il est signé en SHA-1, ou si son SAN ne couvre
600
600
  plus les noms requis — le dernier cas est celui qui sauve : changer le domaine d'écoute sans ce
601
601
  contrôle laisserait un certificat obsolète en place indéfiniment.
602
602
 
@@ -626,7 +626,7 @@ requête. La résolution de l'IP cliente remonte la chaîne **de droite à gauch
626
626
 
627
627
  Barrière testée **avant le routage**, contre l'injection d'en-tête `Host`. Le domaine canonique du
628
628
  kernel est toujours accepté, plus le loopback en développement (`HttpKernel.compileAlias()`,
629
- `http-kernel.ts:936`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
629
+ `http-kernel.ts:941`). `false` (défaut) = ce socle seul ; une liste ajoute des vhosts (exact ou joker
630
630
  d'un seul niveau, `*.cdn.example.com`) ; `true` désactive la barrière — à réserver au cas où le proxy
631
631
  filtre déjà le `Host` (`config.ts:974`).
632
632
 
@@ -658,13 +658,13 @@ redémarrer le pod **en plein drain** par le kubelet, et casserait précisément
658
658
  protéger.
659
659
 
660
660
  Implémentation : court-circuit **total** du pipeline dans `HttpKernel.onHttpRequest()`
661
- (`http-kernel.ts:944`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
661
+ (`http-kernel.ts:949`) — pas de contexte, pas de portée DI, pas de session, pas de journal par sonde,
662
662
  réponses pré-allouées (`HttpKernel.#respondHealth()`, `http-kernel.ts:473`). Et surtout : **avant le
663
663
  rate-limit**. Un kubelet qui reçoit un `429` croit le pod mort → cascade de redémarrages.
664
664
 
665
665
  | Option | Type | Défaut | Effet |
666
666
  | --------------- | ------ | --------- | ---------------------------------------------------------------------- |
667
- | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:925`). |
667
+ | `enabled` | bool | `true` | Expose les probes (`healthSchema`, `config.ts:940`). |
668
668
  | `livenessPath` | string | `/livez` | Chemin de la sonde de vie (`livenessProbe.httpGet.path` k8s). |
669
669
  | `readinessPath` | string | `/readyz` | Chemin de la sonde de disponibilité. |
670
670
  | `shutdownDelay` | ms | `0` | Délai entre la bascule `503` et le début du drain (propagation du LB). |
@@ -733,7 +733,7 @@ load balancer cesse d'envoyer du trafic, et `shutdownDelay` laisse à cette info
733
733
  propager.
734
734
 
735
735
  **Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts:117`)
736
- envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:134`).
736
+ envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts:117`).
737
737
  Sans elle, la coupure TCP ferait voir un **1006** au client — code réservé, jamais émis sur le fil,
738
738
  indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simplement se reconnecter.
739
739
 
@@ -784,13 +784,13 @@ processus à l'arrêt.
784
784
 
785
785
  L'upgrade WebSocket **est** une requête HTTP : il passe donc par le **même** compteur de rate-limit par
786
786
  IP que les requêtes ordinaires, vérifié avant toute allocation de contexte
787
- (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1540`). Le `101` étant déjà émis par `ws`, un `429`
787
+ (`HttpKernel.onWebsocketRequest()`, `http-kernel.ts:1549`). Le `101` étant déjà émis par `ws`, un `429`
788
788
  est impossible → la connexion est fermée en **1013 « Try Again Later »**
789
- (`rateLimiter`, `http-kernel.ts:287`), sans
789
+ (`rateLimiter`, `http-kernel.ts:292`), sans
790
790
  journalisation (un journal par handshake rejeté serait lui-même un amplificateur sous flood).
791
791
 
792
792
  Un second plafond, **désactivé par défaut**, borne le nombre de connexions **simultanées** par IP :
793
- `wsMaxConnectionsPerIp` (`config.ts:1052`). En cloud-native, laisser `null` et déléguer à l'edge —
793
+ `wsMaxConnectionsPerIp` (`config.ts:1067`). En cloud-native, laisser `null` et déléguer à l'edge —
794
794
  nginx `limit_conn`, HAProxy `sc_conn_cur` — qui voit tout le trafic, rejette avant le coût du
795
795
  descripteur et du TLS, et couvre tous les pods. Ne l'activer que sur une machine sans ingress.
796
796
 
@@ -809,7 +809,7 @@ par seconde. Les choix visibles dans le code :
809
809
  - **Aucun timer par connexion** — un `setInterval` par serveur WebSocket, `unref`, et deux `number` par
810
810
  socket (`wsHeartbeat.ts:69`).
811
811
  - **Rien de compilé par requête** — la politique de trust-proxy, celle des `Origin` WS et les motifs de
812
- `trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts:937`, `http-kernel.ts:489`).
812
+ `trustedHosts` sont compilés une fois et mémoïsés (`http-kernel.ts:248`, `http-kernel.ts:248`).
813
813
  - **Rejets avant allocation** — rate-limit HTTP et bornes WS sont vérifiés avant le contexte, la portée
814
814
  DI et l'ALS : un flood coûte une recherche dans une table de hachage.
815
815
  - **Probes hors pipeline** — réponses pré-allouées, aucun objet créé, aucun journal
@@ -834,18 +834,18 @@ demande le backplane realtime.
834
834
 
835
835
  | Domaine | Norme | Ancrage |
836
836
  | ------------------------------------- | ------------------ | -------------------------------------------------------------------------- |
837
- | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:819`) |
837
+ | HTTP/1.1 (sémantique, message) | RFC 9110, 9112 | `node:http` + pipeline `HttpKernel.onHttpRequest()` (`http-kernel.ts:949`) |
838
838
  | HTTP/2 | RFC 9113 | `ServerHttps.createServerH2()` (`server-https.ts:174`) |
839
839
  | HTTP/2 Rapid Reset | CVE-2023-44487 | `maxConcurrentStreams` (`config.ts:355`) |
840
- | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:25`) |
840
+ | En-têtes trop volumineux → 431 | RFC 6585 §5 | `handleClientError()` (`clientError.ts:15`) |
841
841
  | WebSocket — protocole | RFC 6455 | `ws@8` + options (`config.ts:496`) |
842
- | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:134`) |
842
+ | WebSocket — Close 1001 « Going Away » | RFC 6455 §7.4.1 | `Websocket.terminate()` (`server-websocket.ts:117`) |
843
843
  | WebSocket — 1009 « Message Too Big » | RFC 6455 §7.4.1 | `maxPayload` (`config.ts:543`) |
844
844
  | WebSocket — validation UTF-8 | RFC 6455 §8.1 | `skipUTF8Validation` (`config.ts:623`) |
845
845
  | WebSocket — compression | RFC 7692 | `perMessageDeflate` (`config.ts:567`) |
846
846
  | CSWSH (Origin au handshake) | OWASP WSTG-CLNT-10 | `HttpKernel.checkWebsocketOrigin()` (`http-kernel.ts:599`) |
847
847
  | En-têtes forwarded | RFC 7239 | `resolveForwarded()` (`forwarded.ts:253`) |
848
- | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:264`) |
848
+ | Certificat — série, SAN, extensions | RFC 5280 | `Certificate.generateSerialHex()` (`certificates.ts:292`) |
849
849
  | Certificat — identité par le SAN | RFC 6125 | `sanSchema` (`config.ts:442`) |
850
850
 
851
851
  ## ⚠️ Pièges (symptôme → cause → correction)
package/docs/session.md CHANGED
@@ -69,7 +69,7 @@ flowchart TD
69
69
  ```
70
70
 
71
71
  Le point d'activation est **unique et commun aux deux transports** : `HttpKernel.startSession()`
72
- (`http-kernel.ts:1131`). Il commence par la garde paresseuse `if (!intent && !context.hasSession())`
72
+ (`http-kernel.ts:1152`). Il commence par la garde paresseuse `if (!intent && !context.hasSession())`
73
73
  (`http-kernel.ts:1137`) — sans intent de route ni cookie entrant, **aucune session n'est ouverte**.
74
74
 
75
75
  ## 📖 Lexique
@@ -114,11 +114,11 @@ défaut** dans Nodefony :
114
114
 
115
115
  - **Vol du cookie (hijacking).** Un script injecté (XSS) ou un réseau en clair capte le cookie et
116
116
  rejoue la session. → `HttpOnly` et `Secure` sont à `true` par défaut (`sessionCookieSchema`,
117
- `config.ts:718-725`), et le nom du cookie prend le préfixe `__Host-` dès que le transport est TLS
117
+ `config.ts:748-755`), et le nom du cookie prend le préfixe `__Host-` dès que le transport est TLS
118
118
  (`Context.getSessionCookieName()`, `Context.ts:714`).
119
119
  - **Fixation.** L'attaquant pose lui-même un identifiant dans le navigateur de la victime, attend
120
120
  qu'elle se connecte, puis réutilise **le même** identifiant. → double défense : `strictMode` rejette
121
- tout identifiant inconnu du store (`Session.resume()`, `session.ts:189`), et le login **régénère**
121
+ tout identifiant inconnu du store (`Session.resume()`, `session.ts:177`), et le login **régénère**
122
122
  l'identifiant (`AuthFlow` — voir plus bas).
123
123
  - **Exploitation prolongée d'un identifiant volé.** Une session maintenue artificiellement vivante
124
124
  resterait exploitable indéfiniment. → l'**absolute timeout** borne l'âge depuis la création et n'est
@@ -140,11 +140,11 @@ agents (voir [Firewall](../../security/docs/firewall.md)).
140
140
 
141
141
  **2. La session est paresseuse.** Elle n'existe que si une route la demande — `@UseSession`, ou la
142
142
  seule présence d'un paramètre `@Session` — ou si un cookie arrive déjà : c'est la garde de
143
- `HttpKernel.startSession()` (`http-kernel.ts:1131`). Une route publique ne paie **ni lecture de store,
143
+ `HttpKernel.startSession()` (`http-kernel.ts:1152`). Une route publique ne paie **ni lecture de store,
144
144
  ni `Set-Cookie`**.
145
145
 
146
146
  **3. Un seul modèle d'état pour le web et le temps réel.** Le même `startSession()` sert
147
- `HttpKernel.onRequestEnd()` (`http-kernel.ts:1434`) et `HttpKernel.onConnect()` (`http-kernel.ts:1702`) ;
147
+ `HttpKernel.onRequestEnd()` (`http-kernel.ts:1443`) et `HttpKernel.onConnect()` (`http-kernel.ts:1711`) ;
148
148
  l'activité HTTP **ou** WS prolonge la même session (`Session.touchIfNeeded()`, `session.ts:421`).
149
149
 
150
150
  **4. L'administration ne voit jamais un identifiant.** Un opérateur manipule une `ref`, HMAC tronqué
@@ -284,9 +284,9 @@ Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:782`) et
284
284
  | Option | Type | Défaut | Effet |
285
285
  | ------------------- | ------- | ------------ | --------------------------------------------------------------------------------- |
286
286
  | `store` | string | `"auto"` | Backend de persistance — voir la résolution ci-dessous (`config.ts:795`). |
287
- | `name` | string | `"nodefony"` | Nom du cookie, préfixé `__Host-` selon `cookie.hostPrefix` (`config.ts:750`). |
287
+ | `name` | string | `"nodefony"` | Nom du cookie, préfixé `__Host-` selon `cookie.hostPrefix` (`config.ts:770`). |
288
288
  | `strictMode` | bool | `true` | Un identifiant inconnu du store est rejeté → session neuve (anti-fixation). |
289
- | `idleTimeoutS` | int ≥ 0 | `1800` | Inactivité max (30 min). `0` = pas d'expiration par inactivité (`config.ts:796`). |
289
+ | `idleTimeoutS` | int ≥ 0 | `1800` | Inactivité max (30 min). `0` = pas d'expiration par inactivité (`config.ts:823`). |
290
290
  | `absoluteTimeoutS` | int ≥ 0 | `43200` | Âge max depuis la création (12 h), **jamais** prolongé. `0` = désactivé. |
291
291
  | `gcIntervalS` | int ≥ 0 | `600` | Période de purge des sessions expirées, hors requête. `0` = timer désarmé. |
292
292
  | `gcJitter` | bool | `true` | Décale le départ du GC par process (anti _thundering herd_ sur un store partagé). |
@@ -309,8 +309,8 @@ Source unique des défauts : le schéma Zod `sessionSchema` (`config.ts:782`) et
309
309
  ### Comment `store: "auto"` se résout au boot
310
310
 
311
311
  `auto` n'est pas un store : c'est une sentinelle résolue une fois, au boot, par `resolveAutoStore()`
312
- (`config/infra.ts:241`), puis journalisée. Elle suit **l'infra que tu as déclarée**, bornée aux stores
313
- réellement enregistrés (`SessionsService.initializeStorage()`, `sessions-service.ts:231`).
312
+ (`config/infra.ts:297`), puis journalisée. Elle suit **l'infra que tu as déclarée**, bornée aux stores
313
+ réellement enregistrés (`SessionsService.initializeStorage()`, `sessions-service.ts:241`).
314
314
 
315
315
  ```mermaid
316
316
  flowchart TD
@@ -337,7 +337,7 @@ Deux comportements à connaître :
337
337
 
338
338
  ## 🗂️ Choisir son store
339
339
 
340
- Le contrat est unique — `ISessionStorage` (`ISession.ts:127`) — et **tous** les backends le portent.
340
+ Le contrat est unique — `ISessionStorage` (`ISession.ts:142`) — et **tous** les backends le portent.
341
341
  Ce qui change, c'est la topologie et la façon d'expirer.
342
342
 
343
343
  | Store | Où vit l'état | Multi-pod | Expiration idle | `total` admin | Quand le choisir |
@@ -356,7 +356,7 @@ dans une `Map` du process : elles **disparaissent au redémarrage** et ne sont *
356
356
  pods — c'est un choix (mesurer le framework sans le goulot disque/SQL), pas une limite.
357
357
 
358
358
  Il porte quand même **toute** la sémantique du contrat : `createdAt` figé à la création, `updatedAt`
359
- rafraîchi par `MemorySessionStorage.touch()` (`MemorySessionStorage.ts:87`), purge sur les deux bornes
359
+ rafraîchi par `MemorySessionStorage.touch()` (`MemorySessionStorage.ts:96`), purge sur les deux bornes
360
360
  par `gc(idleSeconds, absoluteSeconds)` (`MemorySessionStorage.ts:99`), pagination offset à `total` exact
361
361
  et tri déterministe (`MemorySessionStorage.listPage()`, `MemorySessionStorage.ts:161`).
362
362
 
@@ -390,12 +390,12 @@ Redis jusqu'à son TTL idle, mais elle est **refusée à la reprise**.
390
390
 
391
391
  Capacités réduites, annoncées et non simulées : la pagination est **par curseur** (pas de `total`, pas
392
392
  d'ordre global) et `countSessions()` renvoie **`-1`** = « je ne sais pas »
393
- (`@nodefony/redis/nodefony/src/SessionStorage.ts:318`). L'appelant affiche l'inconnu, il ne l'invente pas.
393
+ (`@nodefony/redis/nodefony/src/SessionStorage.ts:303`). L'appelant affiche l'inconnu, il ne l'invente pas.
394
394
 
395
395
  ### `mongoose` — MongoDB, parité de comportement
396
396
 
397
397
  Même sémantique que le store SQL : `findOneAndUpdate({ upsert: true })` en une passe
398
- (`@nodefony/mongoose/nodefony/src/SessionStorage.ts:102`), `touch` en `updateOne`
398
+ (`@nodefony/mongoose/nodefony/src/SessionStorage.ts:195`), `touch` en `updateOne`
399
399
  (`@nodefony/mongoose/nodefony/src/SessionStorage.ts:174`), GC en deux suppressions `$lt`
400
400
  (`@nodefony/mongoose/nodefony/src/SessionStorage.ts:144-166`). Les horodatages sont des **nombres**
401
401
  (epoch ms) et non des `Date` Mongo, précisément pour que le store reste interchangeable avec Drizzle.
@@ -444,7 +444,7 @@ pose le cookie et marque la session à persister.
444
444
  (depuis `updated`, `session.ts:394`). Échec → `invalidate()` (`session.ts:284`) détruit l'entrée et
445
445
  recrée une session vierge.
446
446
 
447
- **Écriture minimale.** `SessionsService.saveSession()` (`sessions-service.ts:414`) n'écrit **que** si la
447
+ **Écriture minimale.** `SessionsService.saveSession()` (`sessions-service.ts:429`) n'écrit **que** si la
448
448
  session est `dirty` et non `readOnly` ; sinon il appelle `Session.touchIfNeeded()` (`session.ts:421`),
449
449
  qui prolonge l'idle **sans réécrire le blob**, et seulement au-delà d'une demi-vie d'idle
450
450
  (`session.ts:445`). Une requête de lecture coûte donc au pire un `UPDATE` d'horodatage toutes les
@@ -454,7 +454,7 @@ qui prolonge l'idle **sans réécrire le blob**, et seulement au-delà d'une dem
454
454
  pour que la sauvegarde de fin de requête ne réécrive pas ce qu'on vient de supprimer. Surtout, **tout**
455
455
  store est décoré par `RevocationGuardStorage` (`sessions-service.ts:279`) : `destroy()` pose une
456
456
  **pierre tombale** de 5 minutes (`RevocationGuardStorage.ts:144`) qui refuse ensuite tout `write`
457
- (`RevocationGuardStorage.ts:128`) **et tout `touch`** (`RevocationGuardStorage.ts:151`) du même
457
+ (`RevocationGuardStorage.ts:151`) **et tout `touch`** (`RevocationGuardStorage.ts:151`) du même
458
458
  identifiant — ce qui couvre la requête « en vol » d'un autre client.
459
459
 
460
460
  **Purge hors requête.** Un `GcScheduler` est armé au `onReady`, désarmé au `onTerminate`
@@ -498,14 +498,14 @@ C'est le différenciateur du framework appliqué à l'état de session : un seul
498
498
  <!-- prettier-ignore -->
499
499
  | Aspect | HTTP | WebSocket |
500
500
  | --- | --- | --- |
501
- | Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts:1434`) | **une fois** au handshake — `startSession()` dans `onConnect()` (`http-kernel.ts:1702`) |
501
+ | Ouverture | à chaque requête — `startSession()` dans `onRequestEnd()` (`http-kernel.ts:1443`) | **une fois** au handshake — `startSession()` dans `onConnect()` (`http-kernel.ts:1711`) |
502
502
  | Lecture du cookie | constructeur du contexte | constructeur, même nom effectif (`WebsocketContext.ts:172`) |
503
503
  | Sauvegarde | fin de requête | après **chaque frame** traitée (`WebsocketContext.ts:302`) |
504
- | Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts:1185`) |
504
+ | Filet de fermeture | — | `once("onFinish")` sauve si non déjà fait (`http-kernel.ts:1518`) |
505
505
  | Portée ALS | une requête | **handshake + toutes les frames** (`http-kernel.ts:1495`) |
506
506
 
507
507
  La conséquence pratique la plus utile : côté WebSocket, la bulle `AsyncLocalStorage` ouverte au
508
- handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts:431`). L'identité résolue une fois est donc
508
+ handshake par `RequestContext.run()` **enveloppe aussi les messages** (`http-kernel.ts:436`). L'identité résolue une fois est donc
509
509
  disponible à chaque frame sans relire la base — c'est ce dont profite
510
510
  `FirewallRealtimeAuthenticator.supports()` (`FirewallRealtimeAuthenticator.ts:80`), câblé automatiquement
511
511
  par le firewall sur les zones temps réel protégées (`firewall.ts:300`).
@@ -548,7 +548,7 @@ curseur offset. Une révocation « partout » qui en laisserait une n'est pas un
548
548
  faille — on rend donc la main avec la preuve, pas l'espoir.
549
549
 
550
550
  `destroyOwnByRef()` ferme l'IDOR **par construction** : parcours restreint aux sessions du demandeur,
551
- et appartenance **re-vérifiée** avant même de comparer la `ref` (`sessions-service.ts:836`). Une
551
+ et appartenance **re-vérifiée** avant même de comparer la `ref` (`sessions-service.ts:840`). Une
552
552
  `ref` d'autrui est structurellement introuvable.
553
553
 
554
554
  ### Redaction — l'identifiant ne sort jamais du process
@@ -570,7 +570,7 @@ Trois barrières superposées :
570
570
  | Vol par script injecté (XSS) | `HttpOnly` | `sessionCookieSchema` (`config.ts:748`) |
571
571
  | Interception réseau | `Secure` + `__Host-` sur TLS | `getSessionCookieName()` (`Context.ts:714`) |
572
572
  | Requête inter-sites | `SameSite=Lax` par défaut | `defaultCookieOptions` (`cookie.ts:48`) |
573
- | Fixation (cookie pré-posé) | `strictMode` + régénération au login | `Session.resume()` (`session.ts:189`) |
573
+ | Fixation (cookie pré-posé) | `strictMode` + régénération au login | `Session.resume()` (`session.ts:177`) |
574
574
  | Identifiant deviné | 32 octets CSPRNG (43 caractères base64url) | `Session.generateId()` (`session.ts:226`) |
575
575
  | Session volée exploitée longtemps | absolute timeout, jamais prolongé | `absoluteTimeoutS` à la reprise (`session.ts:381`) |
576
576
  | Session oubliée ouverte | idle timeout glissant | `idleTimeoutS` à la reprise (`session.ts:394`) |
@@ -596,30 +596,30 @@ paramètre `@Session()` suffit à déclarer l'intent.
596
596
  | Renouveler l'identifiant | `session.regenerateId()` | Nouvel identifiant, état conservé (`session.ts:236`). |
597
597
  | Savoir si une écriture aura lieu | `session.dirty` | Le drapeau de dirty-tracking (`session.ts:128`). |
598
598
 
599
- **Intent de route** — `UseSession(options)` (`routerDecorators.ts:761`) s'applique à une classe **ou** à
599
+ **Intent de route** — `UseSession(options)` (`routerDecorators.ts:800`) s'applique à une classe **ou** à
600
600
  une méthode ; la méthode l'emporte, par fusion et non par remplacement
601
- (`resolveSessionIntent()`, `routerDecorators.ts:819`). **Une seule** option (`SessionIntent`,
601
+ (`resolveSessionIntent()`, `routerDecorators.ts:833`). **Une seule** option (`SessionIntent`,
602
602
  `ISession.ts:17`) :
603
603
 
604
604
  - `readOnly: true` — la session est reprise et lue mais **jamais** persistée ; une mutation tentée est
605
605
  journalisée en WARNING sans écriture (`Session.save()`, `session.ts:255-264`). C'est le seul champ
606
- propagé par le kernel (`http-kernel.ts:1482`).
606
+ propagé par le kernel (`http-kernel.ts:1194`).
607
607
 
608
- En décorateur de **classe**, `@UseSession` se place **sous** `@controller` (`routerDecorators.ts:761`).
608
+ En décorateur de **classe**, `@UseSession` se place **sous** `@controller` (`routerDecorators.ts:189`).
609
609
 
610
610
  ## 🧩 Extension — brancher son propre store
611
611
 
612
612
  Le registre est une inversion de contrôle complète : `@nodefony/http` ne connaît **aucun** backend.
613
613
  Chaque module fournisseur se déclare lui-même au chargement, par
614
- `SessionsService.registerStorage(nom, ctor)` (`sessions-service.ts:174`) — exactement ce que fait la
614
+ `SessionsService.registerStorage(nom, ctor)` (`sessions-service.ts:184`) — exactement ce que fait la
615
615
  dernière ligne de chaque adaptateur (`@nodefony/redis/nodefony/src/SessionStorage.ts:323`).
616
616
 
617
617
  Pour ajouter un backend :
618
618
 
619
- 1. Implémenter `ISessionStorage` (`ISession.ts:127`). Le **noyau obligatoire** est court :
619
+ 1. Implémenter `ISessionStorage` (`ISession.ts:142`). Le **noyau obligatoire** est court :
620
620
  `read`/`start`/`write`/`open`/`close`/`destroy`/`gc`.
621
- 2. Ajouter les capacités **optionnelles** utiles : `touch` (idle glissant sans réécriture, `ISession.ts:158`),
622
- `listPage` + `countSessions` (administration paginée, `ISession.ts:203`), `listAll` (dump).
621
+ 2. Ajouter les capacités **optionnelles** utiles : `touch` (idle glissant sans réécriture, `ISession.ts:176`),
622
+ `listPage` + `countSessions` (administration paginée, `ISession.ts:236`), `listAll` (dump).
623
623
  3. Appeler `SessionsService.registerStorage("mon-store", MonStore)` au chargement du module.
624
624
  4. Exécuter les bancs de contrat partagés (`sessionStoreContract.ts`, `sessionPaginationContract.ts`)
625
625
  contre l'implémentation — c'est ce qui garantit la parité.
@@ -639,7 +639,7 @@ Trois règles de conception se dégagent du contrat, et méritent d'être respec
639
639
  | ----------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
640
640
  | Attributs et préfixes cookie | RFC 6265bis §4.1.3 | `__Host-` impose `Secure` + `Path=/`, interdit `Domain` (`cookie.ts:386-403`) |
641
641
  | Nom du cookie selon transport | RFC 6265bis / OWASP | `getSessionCookieName()` (`Context.ts:714`) |
642
- | Idle timeout | NIST SP 800-63B-4 / OWASP | défaut 1800 s, glissant par `touch` (`config.ts:796`) |
642
+ | Idle timeout | NIST SP 800-63B-4 / OWASP | défaut 1800 s, glissant par `touch` (`config.ts:831`) |
643
643
  | Absolute timeout | NIST SP 800-63B-4 / OWASP | défaut 43200 s, jamais prolongé (`config.ts:808`) |
644
644
  | Identifiant de session | OWASP Session Management | 32 octets CSPRNG, opaque (`session.ts:226`) |
645
645
  | Identifiant hors URL | OWASP Session Management | cookie uniquement — jamais de réécriture d'URL (`session.ts:20-26`) |
@@ -651,15 +651,15 @@ Trois règles de conception se dégagent du contrat, et méritent d'être respec
651
651
  Le coût d'une session est **payé seulement quand elle sert** :
652
652
 
653
653
  - **Zéro par défaut** — sans intent ni cookie, `startSession()` sort immédiatement
654
- (`http-kernel.ts:1131`) : ni objet `Session`, ni lecture de store.
654
+ (`http-kernel.ts:1152`) : ni objet `Session`, ni lecture de store.
655
655
  - **Objet léger** — trois sacs `{}` à plat, pas de container DI par session (`session.ts:100-104`).
656
- - **Zéro écriture en lecture** — le dirty-tracking court-circuite `save()` (`session.ts:266`) ; le
656
+ - **Zéro écriture en lecture** — le dirty-tracking court-circuite `save()` (`session.ts:255`) ; le
657
657
  `touch` est throttlé à une écriture par demi-vie d'idle (`session.ts:445`).
658
658
  - **GC hors requête** — timer déterministe avec jitter par process, à la place du tirage
659
659
  probabiliste hérité de PHP (`sessions-service.ts:306-312`).
660
660
  - **Révocation quasi gratuite** — la `Map` de pierres tombales est **paresseuse** : sans révocation,
661
661
  `write` ne paie qu'une comparaison `=== null`, sans même un `Date.now()`
662
- (`RevocationGuardStorage.ts:146-151`).
662
+ (`RevocationGuardStorage.ts:163-168`).
663
663
  - **Administration bornée** — jamais plus de `SCAN_PAGE = 200` enregistrements en mémoire
664
664
  (`sessions-service.ts:75`), garde-fou à 5 000 pages (`sessions-service.ts:83`), parcours interrompu
665
665
  **journalisé**.
@@ -700,7 +700,7 @@ dérivation est faite une fois par page (`sessions-service.ts` `currentSessionRe
700
700
  requête ne porte pas de session (appel interne, invocation CLI).
701
701
 
702
702
  Codes de réponse à connaître : **501** si le store courant ne sait pas s'énumérer
703
- (`supportsEnumeration()` faux — `HttpAdminApi.ts:352`), **503** si le service de session est absent, **404** pour une `ref` inconnue ou
703
+ (`supportsEnumeration()` faux — `HttpAdminApi.ts:62`), **503** si le service de session est absent, **404** pour une `ref` inconnue ou
704
704
  une révocation sans effet, **401** sur `mine` sans identité.
705
705
 
706
706
  **Écrans** — la page **Sessions** (`/nodefony/sessions`) de Studio liste les sessions vivantes par `ref` et permet la
@@ -708,7 +708,7 @@ révocation unitaire ou en masse (`@nodefony/studio/frontend/src/routes/sessions
708
708
  affiche le backend réellement résolu, sa provenance et son emplacement physique : ces informations sont
709
709
  publiées au boot par `registerStoreResolution()` (`sessions-service.ts:290`), avec le chemin du fichier
710
710
  SQLite quand c'est pertinent (`SessionStorage.location`,
711
- `@nodefony/drizzle/nodefony/src/SessionStorage.ts:43`).
711
+ `@nodefony/drizzle/nodefony/src/SessionStorage.ts:55`).
712
712
 
713
713
  ## ⚠️ Pièges (symptôme → cause → correction)
714
714
 
package/docs/upload.md CHANGED
@@ -257,8 +257,8 @@ sequenceDiagram
257
257
  Les points d'implémentation qui expliquent des comportements surprenants :
258
258
 
259
259
  1. **L'aiguillage lit le `Content-Type`, pas la méthode** — `parseRequest()`
260
- (`context/http/Request.ts:292`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
261
- des méthodes parsées (`context/http/Request.ts:63`) — l'oubli laissait tout `PATCH` avec un corps vide.
260
+ (`context/http/Request.ts:429`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
261
+ des méthodes parsées (`context/http/Request.ts:79`) — l'oubli laissait tout `PATCH` avec un corps vide.
262
262
  2. **Le multipart draine sur un `finish`, après flush de tous les writes** — `streamMultipart()`
263
263
  (`context/http/Request.ts:537`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
264
264
  qu'une fois tous les fichiers fermés (`context/http/Request.ts:538`).
@@ -283,14 +283,14 @@ non-multipart n'écoute pas `upload.*`.
283
283
  | `maxBodySize` | octets | `1_048_576` (1 MiB) | Plafond d'un corps **JSON / urlencoded / XML / brut** → `413`. `0` = illimité. |
284
284
 
285
285
  Deux rideaux, tous deux `runtimeMutable` (éditable à chaud) : un **pré-check** sur `Content-Length`
286
- qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:296`), puis un **compteur
286
+ qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:407`), puis un **compteur
287
287
  en streaming** qui coupe le socket si le corps déborde sans `Content-Length` honnête — chunked ou
288
- menteur (`Parser.write()`, `context/http/parser.ts:33`, dépassement `context/http/parser.ts:44`).
289
- Champ `maxBodySize` du schéma : `config/config.ts:1005`.
288
+ menteur (`Parser.write()`, `context/http/parser.ts:33`, dépassement `context/http/parser.ts:33`).
289
+ Champ `maxBodySize` du schéma : `config/config.ts:1026`.
290
290
 
291
291
  ### Fichiers multipart — `upload.*`
292
292
 
293
- Table dérivée de `uploadSchema` (`config/config.ts:144`). Toutes les bornes sont `runtimeMutable`.
293
+ Table dérivée de `uploadSchema` (`config/config.ts:159`). Toutes les bornes sont `runtimeMutable`.
294
294
 
295
295
  | Option | Type | Défaut | Effet |
296
296
  | ------------------ | -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
@@ -319,13 +319,13 @@ recommandé) et les **getters** de `Controller` (impératif). Les signatures exa
319
319
  | `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:1240`) |
320
320
  | `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:1239`) |
321
321
  | `@Body() body` | tous les champs parsés (`queryPost`) | `resolveParamArg` `"body"` (`routerDecorators.ts:1178`) |
322
- | `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:1178`) |
323
- | `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1227`) |
322
+ | `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:1223`) |
323
+ | `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1297`) |
324
324
  | `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:239`) |
325
325
  | `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:248`) |
326
326
 
327
327
  Les décorateurs `@UploadedFile` / `@UploadedFiles` sont des fabriques de paramètre
328
- (`routerDecorators.ts:1240`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
328
+ (`routerDecorators.ts:1254`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
329
329
  `IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts:49`, `interfaces/IUpload.ts:7`).
330
330
 
331
331
  ### Un fichier uploadé — `UploadedFile`
@@ -339,7 +339,7 @@ sur disque. Ses membres utiles :
339
339
  | `size` | Taille réellement écrite (octets). | `getSize()` (`service/upload/upload-service.ts:139`) |
340
340
  | `prettySize` | Taille lisible (`« 1.2 MB »`). | `getPrettySize()` (`service/upload/upload-service.ts:143`) |
341
341
  | `mimeType` | Type MIME déclaré (`image/png`…), sinon deviné de l'extension. | `getMimeType()` (`service/upload/upload-service.ts:153`) |
342
- | `hash` / `hashAlgorithm` | Empreinte d'intégrité si `upload.hashAlgorithm` est réglé. | `interfaces/IUpload.ts:49` |
342
+ | `hash` / `hashAlgorithm` | Empreinte d'intégrité si `upload.hashAlgorithm` est réglé. | `interfaces/IUpload.ts:55` |
343
343
  | `moveAsync(target)` | Déplace le temp — **non bloquant, recommandé** dans le pipeline. | `moveAsync()` (`service/upload/upload-service.ts:193`) |
344
344
  | `move(target)` | Variante **synchrone** (compat) — bloque l'event-loop. | `move()` (`service/upload/upload-service.ts:160`) |
345
345
 
@@ -351,7 +351,7 @@ destination est bâtie avec `filename` — d'où l'avertissement de sécurité c
351
351
  Pour piper directement un très gros corps (vidéo, backup) vers le disque ou S3 sans passer par busboy ni
352
352
  par la RAM, `@Body({ stream: true })` court-circuite le parse et injecte l'`IncomingMessage` brut (un
353
353
  `Readable`). Le pipeline sait le sauter en amont via `routeExpectsBodyStream()`
354
- (`routerDecorators.ts:1365`), mémoïsé sur la route.
354
+ (`routerDecorators.ts:1379`), mémoïsé sur la route.
355
355
 
356
356
  ```ts
357
357
  // fragment — le contrôleur pipe le flux lui-même (0 parse, 0 pic RAM)
@@ -378,7 +378,7 @@ piégé. Les défenses en place, et **ce qui reste à ta charge**.
378
378
 
379
379
  > [!WARNING]
380
380
  > **Path traversal sur la destination.** `move(dir)` / `moveAsync(dir)` vers un **dossier** construit le
381
- > chemin final avec `file.filename` — le nom **client** (`service/upload/upload-service.ts:197`). Un nom
381
+ > chemin final avec `file.filename` — le nom **client** (`service/upload/upload-service.ts:101`). Un nom
382
382
  > `../../etc/cron.d/x` s'échapperait du dossier. Le framework protège le chemin du **temporaire**, pas
383
383
  > celui que **tu** choisis. Règle : passe une **cible complète** que tu contrôles, ou assainis toujours
384
384
  > avec `path.basename(file.filename)` — exactement le `safeName` du Démarrage rapide.
@@ -395,7 +395,7 @@ Le parsing du corps est sur le chemin de chaque requête écrivante — les choi
395
395
  - **`stat` non bloquant** — `UploadedFile.create()` résout les stats du fichier en async
396
396
  (`service/upload/upload-service.ts:130`), plus de `lstatSync` par fichier uploadé.
397
397
  - **Listeners jumeaux nettoyés** — le drain de fin de corps retire ses écouteurs `end`/`error`/overflow
398
- à la main (`once` n'auto-détache que celui qui fire) (`context/http/parser.ts:88`).
398
+ à la main (`once` n'auto-détache que celui qui fire) (`context/http/parser.ts:78`).
399
399
  - **Hash opt-in** — `hashAlgorithm: false` par défaut : zéro coût CPU tant que l'intégrité n'est pas
400
400
  demandée.
401
401
 
@@ -423,7 +423,7 @@ pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
423
423
  | `413` sur upload sans message clair | Une borne busboy atteinte en streaming (fichier, cumul, nombre) | Vérifier les bornes `upload.*` — 413 émis sur `stream.on("limit")` (`context/http/Request.ts:481`) |
424
424
  | Un fichier écrit `../../etc/…` après un `move` | `move(dossier)` utilise le nom **client** (`upload-service.ts:197`) | Passer une cible complète, ou `path.basename(file.filename)` |
425
425
  | Des fichiers temporaires s'accumulent dans `uploadDir` | Le contrôleur ne déplace jamais le temp | Appeler `moveAsync()` (ou purger l'ancien temp par TTL) |
426
- | `queryPost` vide sur `PATCH` | Déjà géré : `PATCH` est dans la table des méthodes parsées | Aucune — corps `PATCH` parsé comme `POST` (`context/http/Request.ts:63`) |
426
+ | `queryPost` vide sur `PATCH` | Déjà géré : `PATCH` est dans la table des méthodes parsées | Aucune — corps `PATCH` parsé comme `POST` (`context/http/Request.ts:78`) |
427
427
  | Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:799`) |
428
428
  | `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:502`) |
429
429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/http",
3
- "version": "10.0.0-alpha.8",
3
+ "version": "10.0.0-alpha.9",
4
4
  "description": "Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs pour Nodefony : sessions, contextes de requête, certificats TLS",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "main": "./dist/index.js",
@@ -8,15 +8,16 @@
8
8
  "types": "./dist/types/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
+ "nodefony-source": "./index.ts",
11
12
  "types": "./dist/types/index.d.ts",
12
13
  "import": "./dist/index.js",
13
14
  "default": "./dist/index.js"
14
15
  }
15
16
  },
16
17
  "scripts": {
17
- "build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
18
+ "build": "rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
18
19
  "dev": "rolldown -c rolldown.config.ts --watch",
19
- "clean": "rimraf dist",
20
+ "clean": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
20
21
  "test": "vitest run",
21
22
  "test:integration": "vitest run --config vitest.integration.config.ts",
22
23
  "test:load": "vitest run --config vitest.load.config.ts",
@@ -75,16 +76,15 @@
75
76
  "@types/xml2js": "0.4.14",
76
77
  "@vitest/coverage-v8": "5.0.1",
77
78
  "chai": "6.2.2",
78
- "nodefony": "^10.0.0-alpha.8",
79
- "rimraf": "6.1.3",
80
- "tsx": "4.23.13",
79
+ "nodefony": "^10.0.0-alpha.9",
80
+ "tsx": "4.23.15",
81
81
  "vitest": "5.0.1"
82
82
  },
83
83
  "license": "Apache-2.0",
84
84
  "readmeFilename": "README.md",
85
85
  "contributors": [],
86
86
  "peerDependencies": {
87
- "nodefony": "^10.0.0-alpha.8",
87
+ "nodefony": "^10.0.0-alpha.9",
88
88
  "zod": "^4.6.5"
89
89
  },
90
90
  "files": [