@nodefony/framework 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
  }
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import Route from "./nodefony/src/Route.js";
7
7
  import Controller from "./nodefony/src/Controller.js";
8
8
  import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, controller, controllers, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
9
9
  import Resolver from "./nodefony/src/Resolver.js";
10
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
11
- import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
10
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
11
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
12
12
  import router_default from "./nodefony/service/router.js";
13
13
  import ResourceController from "./nodefony/src/ResourceController.js";
14
14
  import AdminApiController from "./nodefony/controller/AdminApiController.js";
@@ -29,7 +29,7 @@ import { createFrameworkAdminApi } from "./nodefony/src/FrameworkAdminApi.js";
29
29
  import { createSyslogAdminApi } from "./nodefony/src/SyslogAdminApi.js";
30
30
  import Eta from "./nodefony/service/Eta.js";
31
31
  import path from "node:path";
32
- import { AUTO_STORE, EMPTY_INFRA, Kernel, Module, durableStoreRemedy, readStoreLocation, resolveAutoStore, runNeedsExternalServices, services } from "nodefony";
32
+ import { AUTO_STORE, EMPTY_INFRA, Kernel, Module, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, runNeedsExternalServices, services } from "nodefony";
33
33
  import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
34
34
  import { makeExecutableSchema, mergeSchemas } from "@graphql-tools/schema";
35
35
  //#region index.ts
@@ -127,7 +127,8 @@ let Framework = class Framework extends Module {
127
127
  available: listIdempotencyBackends(),
128
128
  reason,
129
129
  configPath: "framework.idempotency.store",
130
- location: readStoreLocation(store)
130
+ location: readStoreLocation(store),
131
+ connector: readStoreConnector(store)
131
132
  });
132
133
  const idem = this.options.idempotency;
133
134
  this.#idempotencyGc = scheduleIdempotencyGc(store, {
@@ -1,5 +1,5 @@
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 router_default from "./router.js";
4
4
  import AdminApiController from "../controller/AdminApiController.js";
5
5
  import { ADMIN_DEFAULT_ROLE, Module, Service, injectable, resolveAdminRole } from "nodefony";
@@ -1,5 +1,5 @@
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 { Module, Service, assertPageQuery, injectable } from "nodefony";
4
4
  //#region nodefony/service/IdempotencyStore.ts
5
5
  const serviceName = "idempotencyStore";
@@ -1,8 +1,8 @@
1
1
  import Route from "../src/Route.js";
2
2
  import { routeExpectsBodyStream } from "../decorators/routerDecorators.js";
3
3
  import Resolver from "../src/Resolver.js";
4
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
5
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
4
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
5
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
6
6
  import { Module, Service, injectable } from "nodefony";
7
7
  import { HttpError, isDomainAllowed } from "@nodefony/http";
8
8
  //#region nodefony/service/router.ts
@@ -2,7 +2,7 @@ import { CORE_PACKAGE, checkOutdated, countModuleDocs, listModuleDocs, listModul
2
2
  import { getResolvedPath, navigateSchemaNode, nodeFlags, notEditableReason, recipeFor, validateLeafValue } from "./configMutation.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { join } from "node:path";
5
- import { GitService, Syslog, aggregateProvenance, applyResolvedPath, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, readResolvedPath } from "nodefony";
5
+ import { GitService, Syslog, aggregateProvenance, applyResolvedPath, collectDevStatus, computeConfigProvenance, defaultAppConfig, extractJsonSchemaDefaults, extractMarkdownSection, flattenConfigSchema, getActiveLogDriver, listLogDrivers, outlineMarkdown, parseNfEnvOverrides, parseStoreManifest, readResolvedPath } from "nodefony";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  //#region nodefony/src/KernelAdminApi.ts
@@ -50,11 +50,11 @@ function readAdapterManifest(pkg) {
50
50
  const pkgJson = join(REPO_ROOT, "node_modules", ...pkg.split("/"), "package.json");
51
51
  try {
52
52
  if (!existsSync(pkgJson)) return null;
53
- const nf = JSON.parse(readFileSync(pkgJson, "utf8")).nodefony;
54
- if (!nf || !Array.isArray(nf.stores)) return null;
53
+ const manifest = parseStoreManifest(JSON.parse(readFileSync(pkgJson, "utf8")));
54
+ if (!manifest?.provides) return null;
55
55
  return {
56
- storeKind: nf.storeKind === "cache" ? "cache" : "durable",
57
- stores: nf.stores.filter((s) => typeof s === "string")
56
+ storeKind: manifest.storeKind,
57
+ stores: manifest.stores
58
58
  };
59
59
  } catch {
60
60
  return null;
@@ -692,7 +692,12 @@ function createKernelAdminApi(kernel) {
692
692
  if ((res.resolved === "drizzle" || res.resolved === "mongoose") && infra.database) return "NF_DATABASE_URL";
693
693
  return null;
694
694
  };
695
- const registered = new Set(kernel.storeResolutions.flatMap((r) => [...r.available]));
695
+ const loadedPackages = /* @__PURE__ */ new Set();
696
+ for (const mod of Object.values(kernel.modules)) {
697
+ const npmName = mod.package?.name;
698
+ if (typeof npmName === "string") loadedPackages.add(npmName);
699
+ loadedPackages.add(mod.name);
700
+ }
696
701
  return {
697
702
  engines: OFFICIAL_STORE_ADAPTERS.map((a) => {
698
703
  const manifest = readAdapterManifest(a.package);
@@ -703,7 +708,7 @@ function createKernelAdminApi(kernel) {
703
708
  kind: manifest?.storeKind ?? "durable",
704
709
  provides: manifest?.stores ?? [],
705
710
  installed: manifest !== null || isPackageInstalled(a.package),
706
- loaded: registered.has(a.engine)
711
+ loaded: loadedPackages.has(a.package) || loadedPackages.has(a.engine)
707
712
  };
708
713
  }),
709
714
  infra: {
package/docs/admin.md CHANGED
@@ -264,8 +264,8 @@ sequenceDiagram
264
264
 
265
265
  | # | Étape | Où |
266
266
  | --- | ----------------------------------------- | ---------------------------------------------------------- |
267
- | 1 | Le producteur s'enregistre | `AdminBroker.register()` (`AdminBroker.ts:45`) |
268
- | 2 | Le framework monte tout | `AdminBroker.mountAll()` (`AdminBroker.ts:104`) |
267
+ | 1 | Le producteur s'enregistre | `AdminBroker.register()` (`AdminBroker.ts:53`) |
268
+ | 2 | Le framework monte tout | `AdminBroker.mountAll()` (`AdminBroker.ts:112`) |
269
269
  | 3 | Une route par endpoint (nom déterministe) | `Router.createRoute()` (`AdminBroker.ts:124`) |
270
270
  | 4 | Le controller pont estampillé une fois | `Router.setController()` idempotent (`AdminBroker.ts:146`) |
271
271
  | 5 | Dispatch : lookup de la route | `AdminBroker.resolve()` (`AdminApiController.ts:94`) |
@@ -275,15 +275,15 @@ sequenceDiagram
275
275
  Points de conception saillants :
276
276
 
277
277
  - **Le montage FIGE les routes.** Après `mountAll()`, tout `register()` lève
278
- (`AdminBroker.ts:46`) : on ne monte pas une route à chaud (Zero surprise en prod). Le broker garde
278
+ (`AdminBroker.ts:53`) : on ne monte pas une route à chaud (Zero surprise en prod). Le broker garde
279
279
  la trace via son drapeau `mounted`.
280
- - **Le nom de route est déterministe** : `admin.<ns>.<method>.<path>` (`AdminBroker.ts:114`) — c'est
280
+ - **Le nom de route est déterministe** : `admin.<ns>.<method>.<path>` (`AdminBroker.ts:123`) — c'est
281
281
  la clé du lookup O(1) que le pont refait à chaque requête.
282
282
  - **`Router.setController` n'est appelé qu'une fois** par process (`AdminBroker.ts:146`) : il pose
283
283
  une propriété non réinscriptible sur le prototype ; une garde `hasOwnProperty` rend l'appel
284
284
  idempotent (multi-broker en test, re-boot).
285
285
  - **Le catalogue se construit à la volée** depuis `AdminBroker.list()` + `AdminBroker.routes()`
286
- (`AdminBroker.ts:100`) — jamais un état retenu.
286
+ (`AdminBroker.ts:108`) — jamais un état retenu.
287
287
 
288
288
  > [!IMPORTANT]
289
289
  > Convention de route **figée** : le data plane est toujours en **≥ 3 segments**
@@ -310,8 +310,8 @@ La décision est **fail-closed** : un authentifié **sans** le rôle requis —
310
310
  laissez-passer.
311
311
 
312
312
  - **Rôle par défaut** : sans `role` explicite, un endpoint exige `ROLE_NODEFONY_ADMIN`
313
- (`AdminBroker.ts:112` ; défaut du champ `IAdminEndpoint.role`, `IAdminApi.ts:152`).
314
- - **Endpoint public** : `public: true` (`IAdminApi.ts:152`) → le RBAC du broker est court-circuité
313
+ (`AdminBroker.ts:112` ; défaut du champ `IAdminEndpoint.role`, `IAdminApi.ts:166`).
314
+ - **Endpoint public** : `public: true` (`IAdminApi.ts:175`) → le RBAC du broker est court-circuité
315
315
  (`role === ""`, `adminRbac.ts:26`). À réserver aux sondes cloud-native (liveness/readiness) et à
316
316
  placer hors d'une zone fermée — sinon le firewall verrouille en amont. Exemple réel :
317
317
  `GET /nodefony/kernel/api/livez` (`KernelAdminApi.ts:615`), sorti de `nodefony-admin` par la zone
@@ -334,7 +334,7 @@ montée avec `[method, "WEBSOCKET"]`. Elle devient donc invocable par le pont WS
334
334
  expose son statut.
335
335
 
336
336
  Les **mutations** sont pontables par socket. La sécurité d'écriture repose alors sur l'**idempotence**
337
- (`idempotencyGate()`, `AdminApiController.ts:158`) : la clé `Idempotency-Key` est **obligatoire en
337
+ (`idempotencyGate()`, `AdminApiController.ts:131`) : la clé `Idempotency-Key` est **obligatoire en
338
338
  WS** (une socket reconnecte et rejoue), **optionnelle en HTTP** (`required: false`,
339
339
  `AdminApiController.ts:184`). Un `GET` n'est jamais idempotenté (`AdminApiController.ts:149`) ; la
340
340
  porte est évaluée **après** le RBAC (un 403 ne consomme aucune entrée). Le helper est le **même** que
@@ -364,18 +364,18 @@ sont enregistrés au `onKernelReady` du framework :
364
364
 
365
365
  | Namespace | Producteur | Rôle |
366
366
  | ----------- | ----------------------------------------------------- | ------------------------------------------------- |
367
- | `kernel` | `createKernelAdminApi` (`KernelAdminApi.ts:468`) | modules, process, uptime, `livez` |
368
- | `framework` | `createFrameworkAdminApi` (`FrameworkAdminApi.ts:40`) | dump du Router + **catalogue** + Playground (dev) |
367
+ | `kernel` | `createKernelAdminApi` (`KernelAdminApi.ts:582`) | modules, process, uptime, `livez` |
368
+ | `framework` | `createFrameworkAdminApi` (`FrameworkAdminApi.ts:86`) | dump du Router + **catalogue** + Playground (dev) |
369
369
  | `syslog` | `createSyslogAdminApi` (`SyslogAdminApi.ts:95`) | viewer de logs (dev) |
370
370
 
371
371
  Les modules externes s'enregistrent depuis leur propre `onKernelBoot` :
372
372
 
373
- | Namespace | Module | Enregistrement |
374
- | ---------- | -------------------- | --------------------------------------------------- |
375
- | `http` | `@nodefony/http` | `createHttpAdminApi` (`http/index.ts:121`) |
376
- | `security` | `@nodefony/security` | `registerSecurityAdminApi` (`security/index.ts:94`) |
377
- | `user` | `@nodefony/user` | `adminNamespace` (`UserAdminApi.ts:879`) |
378
- | `orm` | `@nodefony/orm-core` | `adminNamespace` (`OrmAdminApi.ts:537`) |
373
+ | Namespace | Module | Enregistrement |
374
+ | ---------- | -------------------- | ---------------------------------------------------- |
375
+ | `http` | `@nodefony/http` | `createHttpAdminApi` (`http/index.ts:121`) |
376
+ | `security` | `@nodefony/security` | `registerSecurityAdminApi` (`security/index.ts:104`) |
377
+ | `user` | `@nodefony/user` | `adminNamespace` (`UserAdminApi.ts:945`) |
378
+ | `orm` | `@nodefony/orm-core` | `adminNamespace` (`OrmAdminApi.ts:709`) |
379
379
 
380
380
  Pour le détail de chacun, se reporter à la doc de son module — le broker reste agnostique de leur
381
381
  contenu.
@@ -389,14 +389,14 @@ recopiées ici (elles s'y périmeraient).
389
389
 
390
390
  | Membre (`IAdminBroker`) | Rôle | Ancre |
391
391
  | ------------------------ | -------------------------------------------------------- | -------------------- |
392
- | `register(api)` | Enregistre un producteur (throw si namespace pris/monté) | `AdminBroker.ts:45` |
393
- | `unregister(ns)` | Retire un producteur (et ses routes si montées) | `AdminBroker.ts:61` |
394
- | `has(ns)` / `getApi(ns)` | Interrogation du registre | `AdminBroker.ts:79` |
395
- | `list()` | Producteurs enregistrés (immuable) | `AdminBroker.ts:87` |
396
- | `resolvePath(ns, path)` | Chemin absolu d'un endpoint sans le monter | `AdminBroker.ts:91` |
397
- | `mountAll()` | Monte toutes les routes (idempotent) | `AdminBroker.ts:104` |
398
- | `resolve(routeName)` | Lookup O(1) d'une route montée (utilisé par le pont) | `AdminBroker.ts:96` |
399
- | `routes()` | Introspection des routes montées (source du catalogue) | `AdminBroker.ts:100` |
392
+ | `register(api)` | Enregistre un producteur (throw si namespace pris/monté) | `AdminBroker.ts:53` |
393
+ | `unregister(ns)` | Retire un producteur (et ses routes si montées) | `AdminBroker.ts:69` |
394
+ | `has(ns)` / `getApi(ns)` | Interrogation du registre | `AdminBroker.ts:91` |
395
+ | `list()` | Producteurs enregistrés (immuable) | `AdminBroker.ts:95` |
396
+ | `resolvePath(ns, path)` | Chemin absolu d'un endpoint sans le monter | `AdminBroker.ts:99` |
397
+ | `mountAll()` | Monte toutes les routes (idempotent) | `AdminBroker.ts:112` |
398
+ | `resolve(routeName)` | Lookup O(1) d'une route montée (utilisé par le pont) | `AdminBroker.ts:104` |
399
+ | `routes()` | Introspection des routes montées (source du catalogue) | `AdminBroker.ts:108` |
400
400
 
401
401
  ## 📡 Observabilité — Studio
402
402
 
@@ -416,7 +416,7 @@ recopiées ici (elles s'y périmeraient).
416
416
 
417
417
  | Symptôme | Cause (dans le code) | Correction |
418
418
  | ------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
419
- | `register()` throw « routes figées » | Appel **après** `mountAll()` (`AdminBroker.ts:46`) | Enregistrer au `onKernelBoot`, pas plus tard |
419
+ | `register()` throw « routes figées » | Appel **après** `mountAll()` (`AdminBroker.ts:112`) | Enregistrer au `onKernelBoot`, pas plus tard |
420
420
  | `register()` throw « namespace déjà enregistré » | Deux producteurs sur le même `adminNamespace` (`AdminBroker.ts:51`) | Namespace unique ; garder `register()` idempotent (`has(ns)` avant) |
421
421
  | 401 sur toute route `/nodefony/<ns>/api/*` | Zone `nodefony-admin` : pas de session BFF (`config.ts:141`) | S'authentifier (login BFF) ; pour une sonde publique → `public: true` + zone anonyme |
422
422
  | 403 alors qu'on est connecté | Rôle manquant, `isAdminGranted` fail-closed (`adminRbac.ts:24`) | Doter le compte du rôle requis (défaut `ROLE_NODEFONY_ADMIN`) |
@@ -45,11 +45,11 @@ flowchart LR
45
45
 
46
46
  Trois idées à retenir :
47
47
 
48
- 1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:112`). Tu récupères
48
+ 1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:146`). Tu récupères
49
49
  donc gratuitement le container (`this.get()`), les logs (`this.log()`) et les événements.
50
50
  2. **Tu ne construis rien toi-même** — le `Resolver` instancie ta classe via l'injecteur
51
- (`Resolver.newController()`, `Resolver.ts:236`), jamais un `new` direct.
52
- 3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:697`) traduit la
51
+ (`Resolver.newController()`, `Resolver.ts:254`), jamais un `new` direct.
52
+ 3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:827`) traduit la
53
53
  valeur retournée : objet → JSON, string → corps brut, `void` → « j'ai répondu moi-même ».
54
54
 
55
55
  ## 📖 Lexique
@@ -196,14 +196,14 @@ Le tableau ci-dessous donne la séquence exacte, avec l'ancre qui la prouve :
196
196
  | # | Étape | Où |
197
197
  | --- | --------------------------------------- | --------------------------------------------------- |
198
198
  | 1 | Appariement de la route | `router.resolve()` (`http-kernel.ts:1324`) |
199
- | 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:1334`) |
199
+ | 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:1381`) |
200
200
  | 3 | Parse du corps (sauf `@Body({stream})`) | `http-kernel.ts:1316` |
201
201
  | 4 | Armement de la route (sans instance) | `prepareFrontController()` (`http-kernel.ts:767`) |
202
- | 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:1290`) |
203
- | 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:1131`) |
204
- | 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:1301`) |
202
+ | 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:1479`) |
203
+ | 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:1152`) |
204
+ | 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:1490`) |
205
205
  | 8 | Autorisation `@IsGranted` | `Resolver.executeAction()` (`Resolver.ts:334`) |
206
- | 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:313`) |
206
+ | 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:335`) |
207
207
  | 10 | **Ton action** | `controller[methodKey]()` (`Resolver.ts:382`) |
208
208
 
209
209
  > [!IMPORTANT]
@@ -257,7 +257,7 @@ Si ton `initialize()` lève, l'exception remonte le pipeline et sort en réponse
257
257
  Chaque étape est chronométrée sous le nom d'une **phase**, lisible dans la debug bar et le profileur :
258
258
  `resolve` · `initialize` (DI + ton hook) · `parse` · `firewall` · `action` · `render` · `send`.
259
259
  La phase `initialize` existe précisément pour que le temps passé dans ton hook et dans la résolution
260
- DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:281`).
260
+ DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:100`).
261
261
 
262
262
  ## 🔌 HTTP et WebSocket — le même contrôleur
263
263
 
@@ -300,7 +300,7 @@ class ChatController extends Controller {
300
300
  | `initialize()` | À chaque requête | **Une seule fois**, au handshake |
301
301
 
302
302
  La réutilisation de l'instance vient du cache posé sur le container du contexte
303
- (`Resolver.newController()`, `Resolver.ts:232`) : le contexte WS étant partagé par la connexion, le
303
+ (`Resolver.newController()`, `Resolver.ts:254`) : le contexte WS étant partagé par la connexion, le
304
304
  contrôleur l'est aussi. Un garde-fou vérifie que l'instance cachée est bien de la classe de la route
305
305
  courante et la reconstruit sinon (`Resolver.ts:344-347`) — sans quoi un message invoquant une autre
306
306
  action se tromperait d'objet.
@@ -310,9 +310,9 @@ action se tromperait d'objet.
310
310
  > frame 2 — pratique pour un état de conversation, piège si tu comptais sur une instance neuve. En
311
311
  > HTTP, l'inverse : chaque requête repart d'une instance vierge.
312
312
 
313
- Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1702`)
313
+ Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1711`)
314
314
  appelle `handleFrontController()` (donc `initialize()`) **avant** `startSession()`
315
- (`http-kernel.ts:1131`), avant l'acceptation de la socket, et avant le firewall
315
+ (`http-kernel.ts:1152`), avant l'acceptation de la socket, et avant le firewall
316
316
  (`http-kernel.ts:1457`).
317
317
 
318
318
  ## 🧠 D'où viennent `request`, `response`, `session`
@@ -322,16 +322,16 @@ sont des accesseurs qui dérivent du contexte **vivant**, selon le motif `champ
322
322
 
323
323
  | Raccourci | Ce que tu obtiens | Ancre |
324
324
  | ---------------- | -------------------------------------------------- | ------------------- |
325
- | `this.context` | Le contexte transport de la requête courante | `Controller.ts:146` |
325
+ | `this.context` | Le contexte transport de la requête courante | `Controller.ts:180` |
326
326
  | `this.route` | La route matchée | `Controller.ts:158` |
327
327
  | `this.request` | La requête (HTTP, HTTP/2 ou WS) | `Controller.ts:162` |
328
- | `this.response` | La réponse du transport | `Controller.ts:169` |
328
+ | `this.response` | La réponse du transport | `Controller.ts:203` |
329
329
  | `this.method` | La méthode HTTP (ou `WEBSOCKET`) | `Controller.ts:212` |
330
330
  | `this.queryGet` | Les paramètres de la query string | `Controller.ts:221` |
331
331
  | `this.queryPost` | Le corps parsé | `Controller.ts:248` |
332
332
  | `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:248` |
333
333
  | `this.queryFile` | Les fichiers uploadés | `Controller.ts:239` |
334
- | `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:229` |
334
+ | `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:279` |
335
335
 
336
336
  Pourquoi des accesseurs plutôt que des champs recopiés au constructeur : **la fraîcheur et le coût**.
337
337
  Une valeur recopiée vieillit dès que le pipeline modifie le contexte, et recopier quatre structures
@@ -340,7 +340,7 @@ gratuitement.
340
340
 
341
341
  ### La session est **lazy** — elle n'existe que si tu la demandes
342
342
 
343
- `this.session` est un simple getter sur `context.session` (`Controller.ts:229`). Il n'y a **pas** de
343
+ `this.session` est un simple getter sur `context.session` (`Controller.ts:279`). Il n'y a **pas** de
344
344
  `startSession()` à appeler depuis un contrôleur : l'activation se déclare sur la route, avec
345
345
  `@UseSession()` (ou un paramètre `@Session()`, qui vaut déclaration implicite), et le pipeline
346
346
  l'exécute à son point unique. Sans déclaration et sans cookie de session entrant, **aucune session
@@ -387,26 +387,26 @@ Ce que ça change, concrètement :
387
387
 
388
388
  ## 🧰 Répondre — ce que ton `return` déclenche
389
389
 
390
- Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:697`). Il regarde le **type**
390
+ Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:827`). Il regarde le **type**
391
391
  de ce que tu as retourné :
392
392
 
393
393
  <!-- prettier-ignore -->
394
394
  | Tu retournes… | Ce qui se passe | Ancre |
395
395
  | --- | --- | --- |
396
- | Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:700-710` |
396
+ | Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:830-840` |
397
397
  | Une `string` | Envoyée telle quelle en corps | `Resolver.ts:711` |
398
- | Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:760` |
398
+ | Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:870` |
399
399
  | Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts:734` |
400
- | Un `Buffer` | Envoyé brut | `Resolver.ts:723` |
401
- | Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:716` |
402
- | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:855`) |
403
- | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:801` |
404
- | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:770-777` |
400
+ | Un `Buffer` | Envoyé brut | `Resolver.ts:854` |
401
+ | Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:581` |
402
+ | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:948`) |
403
+ | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:932` |
404
+ | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:906-913` |
405
405
 
406
406
  > [!WARNING]
407
407
  > **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
408
408
  > répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
409
- > `NO_BODY_STATUS` (`Resolver.ts:855`) contient 204, 205 et 304. Donc un `@Delete` qui fait
409
+ > `NO_BODY_STATUS` (`Resolver.ts:948`) contient 204, 205 et 304. Donc un `@Delete` qui fait
410
410
  > `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
411
411
  > laisse la requête pendue.
412
412
 
@@ -419,12 +419,12 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
419
419
 
420
420
  | Helper | Pour… | Ancre |
421
421
  | -------------------------------------------- | -------------------------------------------------------- | ------------------- |
422
- | `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:379` |
422
+ | `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:481` |
423
423
  | `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:377` |
424
424
  | `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:410` |
425
425
  | `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:392` |
426
- | `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:382` |
427
- | `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:432` |
426
+ | `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:500` |
427
+ | `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:534` |
428
428
  | `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:319` |
429
429
 
430
430
  `renderView()` mesure sa propre phase `render` et injecte automatiquement les aides frontend
@@ -437,7 +437,7 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
437
437
  > [!TIP]
438
438
  > **Redirection : le code par défaut est 302** (Found), pas 301. Un statut absent ou hors de la liste
439
439
  > RFC 9110 §15.4 (301, 302, 303, 307, 308) retombe sur 302 avec un log d'avertissement
440
- > (`Response.redirect()`, `Response.ts:595`). Un 301 par défaut piégeait : les navigateurs le mettent
440
+ > (`Response.redirect()`, `Response.ts:611`). Un 301 par défaut piégeait : les navigateurs le mettent
441
441
  > en cache de façon quasi irréversible.
442
442
 
443
443
  ## 📁 Servir un fichier — téléchargement et flux média
@@ -446,7 +446,7 @@ Deux besoins distincts, deux helpers.
446
446
 
447
447
  ### Téléchargement — `renderFileDownload()`
448
448
 
449
- `renderFileDownload(file, options?, headers?)` (`Controller.ts:473`) pose
449
+ `renderFileDownload(file, options?, headers?)` (`Controller.ts:625`) pose
450
450
  `Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
451
451
  de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:586`) ;
452
452
  la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
@@ -454,7 +454,7 @@ et gèle le process le temps du stat.
454
454
 
455
455
  ### Lecture en continu — `renderMediaStream()`
456
456
 
457
- `renderMediaStream(file, headers?, options?)` (`Controller.ts:609`) implémente les **requêtes par
457
+ `renderMediaStream(file, headers?, options?)` (`Controller.ts:780`) implémente les **requêtes par
458
458
  plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
459
459
 
460
460
  | Le client envoie… | Réponse |
@@ -474,7 +474,7 @@ donc testable sans serveur.
474
474
  **nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
475
475
  téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
476
476
  écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
477
- résout la promesse (`Controller.ts:553-558`), puis se retire lui-même (`Controller.ts:574`). Un
477
+ résout la promesse (`Controller.ts:729-734`), puis se retire lui-même (`Controller.ts:746`). Un
478
478
  téléchargement interrompu ne coûte donc **rien** en ressource retenue.
479
479
 
480
480
  ## ⚠️ Erreurs — lever, rendre, observer
@@ -489,11 +489,11 @@ throw new nodefonyError("Article introuvable", 404); // statut porté par l'erre
489
489
  throw new HttpError("Not Found", 404, this.context); // variante enrichie du contexte
490
490
  ```
491
491
 
492
- L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:874`), qui délègue la mise en
492
+ L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:879`), qui délègue la mise en
493
493
  forme au rendeur d'erreurs. Ce qui en sort :
494
494
 
495
495
  - **statut normalisé** — un code absent (ou l'ancien quirk `200`) devient **500**
496
- (`normalizeHttpStatus()`, `error-renderer.ts:355`) ;
496
+ (`normalizeHttpStatus()`, `error-renderer.ts:642`) ;
497
497
  - **corps structuré** : `{ code, message, result: null, error: {…}, nodefony: {…} }`, l'enveloppe
498
498
  `nodefony` portant l'environnement, l'URL et l'**identifiant de requête** — de quoi retrouver la
499
499
  trace complète dans les logs ;
@@ -501,13 +501,13 @@ forme au rendeur d'erreurs. Ce qui en sort :
501
501
  ne tente pas de rendre — il journalise et s'arrête (`http-kernel.ts:770-775`).
502
502
 
503
503
  En **WebSocket**, il n'y a pas de statut : l'erreur devient un **code de fermeture** RFC 6455
504
- (`renderWebsocket()`, `error-renderer.ts:393`) — 401/403 → 1008 (violation de politique),
504
+ (`renderWebsocket()`, `error-renderer.ts:518`) — 401/403 → 1008 (violation de politique),
505
505
  5xx → 1011 (erreur interne), le reste → 4004 (plage privée). Si la socket n'est pas encore acceptée,
506
506
  c'est un **rejet** de handshake.
507
507
 
508
508
  > [!NOTE]
509
509
  > Les erreurs de ton action remontent **seules** : le Resolver n'enveloppe pas l'appel dans un
510
- > `try/catch` inutile (`Resolver.ts:405-406`). Inutile d'attraper pour re-lever — sauf si tu veux
510
+ > `try/catch` inutile (`Resolver.ts:444-445`). Inutile d'attraper pour re-lever — sauf si tu veux
511
511
  > vraiment traduire l'erreur en un autre statut.
512
512
 
513
513
  ## 🧩 Services injectés — trois façons
@@ -521,7 +521,7 @@ explicite :
521
521
  const catalog = this.get<CatalogService>("catalog"); // null si absent ou container nettoyé
522
522
  ```
523
523
 
524
- `Service.get()` (`Service.ts:472`) est une **façade sûre** : elle retourne `null` au lieu de lever si
524
+ `Service.get()` (`Service.ts:518`) est une **façade sûre** : elle retourne `null` au lieu de lever si
525
525
  le container a déjà été détaché. C'est le style à privilégier dans `initialize()`.
526
526
 
527
527
  ### 2. Injection par le constructeur — `@inject`
@@ -568,7 +568,7 @@ code du framework applique — et attend de toi — les règles suivantes :
568
568
  (`Controller.ts:574`).
569
569
  - **Métadonnées d'action figées** : `@HttpCode`, `@Header`, les paramètres décorés et l'intention de
570
570
  session sont calculés **une fois** par route puis mémorisés, au lieu d'être relus par `Reflect` à
571
- chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:402`).
571
+ chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:469`).
572
572
  - **Gardes payées seulement si présentes** : sans `@IsGranted`, la vérification d'autorisation est
573
573
  un test de nullité (`Resolver.ts:334`) — 0 lookup, 0 `await`, 0 allocation.
574
574
  - **Ta part du contrat** : pas de structure allouée « au cas où » dans le constructeur ni dans
@@ -578,13 +578,13 @@ code du framework applique — et attend de toi — les règles suivantes :
578
578
 
579
579
  | Domaine | Norme | Comment le code s'y conforme |
580
580
  | -------------------------------- | ------------------------ | -------------------------------------------------------------- |
581
- | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:855`) |
581
+ | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:948`) |
582
582
  | Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts:107`) |
583
583
  | Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:392`) |
584
584
  | Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts:534`) |
585
- | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:760`), vérifié par le banc `auto-json` |
585
+ | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:892`), vérifié par le banc `auto-json` |
586
586
  | Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts:734`) |
587
- | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:393`) |
587
+ | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:518`) |
588
588
 
589
589
  ## 📡 Observabilité — Studio
590
590
 
@@ -601,11 +601,11 @@ code du framework applique — et attend de toi — les règles suivantes :
601
601
  <!-- prettier-ignore -->
602
602
  | Symptôme | Cause (dans le code) | Correction |
603
603
  | --- | --- | --- |
604
- | La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:801`) | Retourner une valeur, ou poser `@HttpCode(204)` |
605
- | Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:775`) | Retourner un objet simple, ou `renderJson(entity.toJSON())` |
604
+ | La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:906`) | Retourner une valeur, ou poser `@HttpCode(204)` |
605
+ | Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:906`) | Retourner un objet simple, ou `renderJson(entity.toJSON())` |
606
606
  | `Route Action not found` | L'action porte un nom déjà utilisé par un membre de `Controller` | Renommer : `session`, `request`, `response`, `context`, `route`, `method`, `query*`, `get`, `set`, `render*`, `redirect`, `forward` sont réservés |
607
607
  | `this.session` est `null` dans `initialize()` | La session est activée **après** (`http-kernel.ts:1142`) | Lire la session dans l'action, pas dans le hook |
608
- | Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:1294`) | Déplacer l'effet de bord dans l'action |
608
+ | Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:1490`) | Déplacer l'effet de bord dans l'action |
609
609
  | Redirection permanente non voulue | Un statut invalide retombe sur 302, un `301` explicite reste 301 | Passer le code voulu : `this.redirect(url, 302)` |
610
610
  | WS : l'état d'une frame « bave » sur la suivante | L'instance est partagée par toute la connexion (`Resolver.ts:262`) | Réinitialiser l'état en tête d'action, ou le porter par message |
611
611
  | WS : l'action n'est jamais appelée | Route sans transport `WEBSOCKET` déclaré | `requirements: { methods: ["WEBSOCKET"] }` |