@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +4 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +15 -20
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  6. package/dist/nodefony/controller/AdminApiController.js +45 -11
  7. package/dist/nodefony/controller/ApiKeyController.js +18 -10
  8. package/dist/nodefony/controller/BenchController.js +1 -2
  9. package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
  10. package/dist/nodefony/controller/LoginPageController.js +117 -0
  11. package/dist/nodefony/controller/OAuth2Controller.js +133 -17
  12. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
  13. package/dist/nodefony/controller/SessionAuthController.js +50 -13
  14. package/dist/nodefony/controller/TokenAuthController.js +14 -8
  15. package/dist/nodefony/controller/TotpController.js +7 -8
  16. package/dist/nodefony/controller/WebAuthnController.js +28 -12
  17. package/dist/nodefony/controller/oauthAuthority.js +1 -2
  18. package/dist/nodefony/decorators/routerDecorators.js +268 -65
  19. package/dist/nodefony/service/AdminBroker.js +4 -6
  20. package/dist/nodefony/service/Eta.js +1 -2
  21. package/dist/nodefony/service/IdempotencyStore.js +4 -6
  22. package/dist/nodefony/service/router.js +97 -34
  23. package/dist/nodefony/src/Controller.js +71 -43
  24. package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
  25. package/dist/nodefony/src/KernelAdminApi.js +82 -18
  26. package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
  27. package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
  28. package/dist/nodefony/src/Resolver.js +105 -36
  29. package/dist/nodefony/src/ResourceController.js +2 -3
  30. package/dist/nodefony/src/Route.js +54 -8
  31. package/dist/nodefony/src/SecurityApiController.js +23 -0
  32. package/dist/nodefony/src/SyslogAdminApi.js +1 -2
  33. package/dist/nodefony/src/Template.js +1 -2
  34. package/dist/nodefony/src/configMutation.js +1 -2
  35. package/dist/nodefony/src/docsReader.js +39 -23
  36. package/dist/nodefony/src/idempotency.js +14 -22
  37. package/dist/nodefony/src/idempotencyGc.js +1 -2
  38. package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
  39. package/dist/nodefony/src/loginPage.js +430 -0
  40. package/dist/nodefony/src/requireAuthentication.js +35 -0
  41. package/dist/nodefony/src/routeSchema.js +81 -0
  42. package/dist/nodefony/src/scopeCatalog.js +1 -2
  43. package/dist/nodefony/src/singletonGuard.js +1 -2
  44. package/dist/nodefony/src/syslogFilters.js +1 -2
  45. package/dist/types/index.d.ts +8 -12
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
  47. package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
  48. package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
  49. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
  50. package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
  51. package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
  52. package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
  53. package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
  54. package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
  55. package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
  56. package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
  57. package/dist/types/nodefony/service/router.d.ts +40 -1
  58. package/dist/types/nodefony/src/Controller.d.ts +54 -17
  59. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  60. package/dist/types/nodefony/src/Resolver.d.ts +44 -1
  61. package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
  62. package/dist/types/nodefony/src/Route.d.ts +21 -1
  63. package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
  64. package/dist/types/nodefony/src/docsReader.d.ts +10 -6
  65. package/dist/types/nodefony/src/idempotency.d.ts +12 -10
  66. package/dist/types/nodefony/src/loginPage.d.ts +144 -0
  67. package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
  68. package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
  69. package/docs/admin.md +83 -78
  70. package/docs/controller.md +112 -111
  71. package/docs/decorateurs.md +212 -163
  72. package/docs/idempotence.md +137 -135
  73. package/docs/routing.md +96 -95
  74. package/docs/templates.md +55 -55
  75. package/package.json +10 -13
  76. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/routing.md CHANGED
@@ -49,13 +49,13 @@ Trois faits à retenir avant tout le reste :
49
49
 
50
50
  1. **L'ordre de déclaration EST la priorité.** Une route paramétrée déclarée avant une route
51
51
  littérale gagne sur le chemin littéral — c'est figé par le banc de non-régression
52
- (`routing-nonregression.test.ts:83`).
52
+ (`routing-nonregression.test.ts`).
53
53
  2. **Le routeur ne lève jamais de 404.** Aucun match = `resolver.resolve === false`, sans exception ;
54
54
  le 404 est décidé plus loin, après le repli sur les fichiers statiques
55
- (`HttpError("Not Found", 404)`, `http-kernel.ts:798`).
55
+ (`HttpError("Not Found", 404)`, `http-kernel.ts#HttpKernel.prepareFrontController`).
56
56
  3. **Le chemin est vérifié avant la méthode, et le domaine entre les deux** — c'est ce qui produit un
57
57
  `403` plutôt qu'un `405` bavard quand la route appartient à un autre vhost (`Route.match()`,
58
- `Route.ts:343`).
58
+ `Route.ts#Route.match`).
59
59
 
60
60
  ## 📖 Lexique
61
61
 
@@ -95,30 +95,30 @@ tranchent différemment :
95
95
  **L'arbitrage est explicite, pas calculé.** Beaucoup de routeurs trient les routes par « spécificité »
96
96
  (le motif le plus précis gagne) — pratique jusqu'au jour où l'on ne comprend plus pourquoi telle route
97
97
  passe devant telle autre. Nodefony garde l'**ordre de déclaration** : la table est parcourue de haut en
98
- bas, le premier motif satisfait l'emporte (`Router.resolve()`, `router.ts:260`). Le compromis assumé :
98
+ bas, le premier motif satisfait l'emporte (`Router.resolve()`, `router.ts#Router.resolve`). Le compromis assumé :
99
99
  c'est à toi de déclarer le littéral avant le paramétré. En échange, tu peux **lire** l'ordre dans ton
100
100
  contrôleur.
101
101
 
102
102
  **La performance ne change pas la sémantique.** Sous le capot, la table est partitionnée : les chemins
103
103
  **littéraux** (aucun `{var}`, aucun métacaractère) vivent dans une `Map path → candidates` en lookup
104
- O(1) ; les chemins **dynamiques** restent un scan regex (`buildRouteIndex()`, `router.ts:130`). À la
104
+ O(1) ; les chemins **dynamiques** restent un scan regex (`buildRouteIndex()`, `router.ts#buildRouteIndex`). À la
105
105
  résolution, les deux flux sont fusionnés **par position d'insertion** — la séquence de candidats est
106
106
  exactement celle du scan linéaire complet, moins les littérales d'un autre chemin, qui ne pouvaient de
107
- toute façon pas correspondre (`Router.resolve()`, `router.ts:260`). C'est cette équivalence que fige le
107
+ toute façon pas correspondre (`Router.resolve()`, `router.ts#Router.resolve`). C'est cette équivalence que fige le
108
108
  banc de non-régression : un refacto du routeur doit le repasser à l'identique.
109
109
 
110
110
  **Une seule table pour HTTP et WebSocket.** Il n'y a pas de « routeur WS » séparé : une action WS est
111
111
  une route dont les méthodes déclarées contiennent `WEBSOCKET` (`Route.matchRequirements()`,
112
- `Route.ts:763`). C'est le différenciateur du framework — le même contrôleur, le même contexte, les
112
+ `Route.ts#Route.matchRequirements`). C'est le différenciateur du framework — le même contrôleur, le même contexte, les
113
113
  mêmes décorateurs.
114
114
 
115
115
  **Le routeur passe avant les fichiers statiques.** Une requête qui correspond à une route ne paie
116
116
  jamais le `stat` du serveur de fichiers : le repli statique n'est tenté que si la résolution a échoué
117
- (`serverStatic.handle()`, `http-kernel.ts:762`).
117
+ (`serverStatic.handle()`, `http-kernel.ts#HttpKernel.handle`).
118
118
 
119
119
  > [!NOTE]
120
120
  > **Le routage n'a aucune option de configuration.** Le schéma Zod du module n'expose qu'un sac
121
- > d'options de Service pour le `Router` (`config.ts:36`) — tout se déclare par **décorateurs**, dans le
121
+ > d'options de Service pour le `Router` (`config.ts#serviceOptionsSchema`) — tout se déclare par **décorateurs**, dans le
122
122
  > contrôleur, à côté du code qu'ils servent. Pas de `routes.yaml`, pas de table centrale à maintenir.
123
123
 
124
124
  ## 🚀 Démarrage rapide
@@ -256,22 +256,22 @@ npx wscat -c ws://localhost:5151/api/catalog/live
256
256
  La **syntaxe** des décorateurs est détaillée dans [decorateurs](./decorateurs.md) ; ce qui suit est ce
257
257
  que chaque forme **produit dans la table**.
258
258
 
259
- | Forme | Nom de la route | Méthodes déclarées | Quand l'utiliser |
260
- | ----------------------------------------- | ---------------------------------- | -------------------------------- | -------------------------------------------- |
261
- | `@Get` `@Post` `@Put` `@Delete` `@Patch`… | auto : `` `Classe::methode` `` | exactement une | le cas courant, REST |
262
- | `@All(path)` | auto : `` `Classe::methode` `` | **aucune** → toutes les méthodes | proxy, capture-tout, page de repli |
263
- | `@route(nom, options)` | **le tien** (stable, réutilisable) | `requirements.methods` (libre) | WebSocket, multi-méthodes, contraintes fines |
259
+ | Forme | Nom de la route | Méthodes déclarées | Quand l'utiliser |
260
+ | ----------------------------------------- | ---------------------------------- | ------------------------------------------------------ | -------------------------------------------- |
261
+ | `@Get` `@Post` `@Put` `@Delete` `@Patch`… | auto : `` `Classe::methode` `` | exactement une | le cas courant, REST |
262
+ | `@All(path)` | auto : `` `Classe::methode` `` | **aucune** → tous les verbes HTTP, jamais le WebSocket | proxy, capture-tout, page de repli |
263
+ | `@route(nom, options)` | **le tien** (stable, réutilisable) | `requirements.methods` (libre) | WebSocket, multi-méthodes, contraintes fines |
264
264
 
265
265
  - Les décorateurs de méthode HTTP délèguent tous à `@route` avec un nom auto `Classe::methode`, et
266
- posent `requirements: { methods }` (`httpMethodDecorator()`, `routerDecorators.ts:489`).
266
+ posent `requirements: { methods }` (`httpMethodDecorator()`, `routerDecorators.ts#httpMethodDecorator`).
267
267
  - `@All` n'émet **aucun** requirement de méthode : la route sert alors GET, POST, DELETE… et ne peut
268
- donc jamais produire un 405 sur la méthode (`All()`, `routerDecorators.ts:548`).
268
+ donc jamais produire un 405 sur la méthode (`All()`, `routerDecorators.ts#All`).
269
269
  - `@route` est la forme complète : elle seule permet `protocol` (sous-protocole WS), un nom lisible, et
270
270
  des requirements par variable.
271
271
 
272
272
  **Comment une déclaration devient une route.** Les décorateurs de méthode **accumulent** des métadonnées
273
- sur le constructeur (clé `routes:definitions`, `routerDecorators.ts:16`) ; c'est `@controller(prefix)`
274
- qui les lit et appelle `Router.createRoute()` pour chacune (`controller()`, `routerDecorators.ts:189`).
273
+ sur le constructeur (clé `routes:definitions`, `routerDecorators.ts`) ; c'est `@controller(prefix)`
274
+ qui les lit et appelle `Router.createRoute()` pour chacune (`controller()`, `routerDecorators.ts#controller`).
275
275
 
276
276
  > [!WARNING]
277
277
  > **`@route`/`@Get` doivent être SOUS `@controller`** — les décorateurs de classe s'évaluent après ceux
@@ -281,8 +281,8 @@ qui les lit et appelle `Router.createRoute()` pour chacune (`controller()`, `rou
281
281
  ## Motifs de chemin et paramètres
282
282
 
283
283
  Le chemin déclaré est compilé **une fois**, à la création de la route, en une expression régulière
284
- ancrée et **insensible à la casse** (`Route.compile()`, `Route.ts:464`). La grammaire tient en cinq
285
- briques (`REG_ROUTE`, `Route.ts:17`) :
284
+ ancrée et **insensible à la casse** (`Route.compile()`, `Route.ts#Route.compile`). La grammaire tient en cinq
285
+ briques (`REG_ROUTE`, `Route.ts#REG_ROUTE`) :
286
286
 
287
287
  | Écriture | Motif compilé | Capture | Exemple |
288
288
  | ------------------ | ------------- | ----------------- | ----------------------------------------------------- |
@@ -295,10 +295,10 @@ briques (`REG_ROUTE`, `Route.ts:17`) :
295
295
  Et trois comportements qui surprennent la première fois :
296
296
 
297
297
  - **Le slash final est retiré avant le matching** — `/books/` et `/books` désignent la même route
298
- (`Route.cleanPathname()`, `Route.ts:303`). Corollaire : `/files/*` ne matche pas `/files/`, qui a été
298
+ (`Route.cleanPathname()`, `Route.ts#Route.cleanPathname`). Corollaire : `/files/*` ne matche pas `/files/`, qui a été
299
299
  normalisé en `/files`.
300
300
  - **Les valeurs sont URL-décodées** — `%C3%A9t%C3%A9` arrive dans l'action comme `été`
301
- (`decode()`, `Route.ts:79`).
301
+ (`decode()`, `Route.ts#decode`).
302
302
  - **La query string n'entre jamais dans le matching** — seul le `pathname` est comparé. Les paramètres
303
303
  de query se lisent avec `@Query` (voir [decorateurs](./decorateurs.md)).
304
304
 
@@ -306,8 +306,8 @@ Et trois comportements qui surprennent la première fois :
306
306
 
307
307
  C'est le mécanisme le moins évident, et le plus utile. Déclarer un `defaults` pour une variable change
308
308
  le motif compilé : le segment devient facultatif (`[^/]*`) **et son slash aussi** (`/?`), puis la valeur
309
- par défaut est réinjectée quand la capture est vide (`checkDefaultParameters()`, `Route.ts:99` ·
310
- `Route.hydrateDefaultParameters()`, `Route.ts:572`).
309
+ par défaut est réinjectée quand la capture est vide (`checkDefaultParameters()`, `Route.ts#checkDefaultParameters` ·
310
+ `Route.hydrateDefaultParameters()`, `Route.ts#Route.hydrateDefaultParameters`).
311
311
 
312
312
  ```ts ignore
313
313
  @route("route-page", { path: "/page/{slug}", defaults: { slug: "home" } })
@@ -327,7 +327,7 @@ async page(slug: string) {
327
327
  Les captures sont passées **positionnellement**, dans l'ordre des variables du chemin — c'est pourquoi
328
328
  la signature `async method6(metier: string, format: string)` suit l'ordre de `/{metier}/{format}`. Un
329
329
  wildcard est exposé sous les clés `wildcard` et `*`. Le `Resolver` en fabrique aussi un instantané
330
- nom → valeur par requête (`Resolver.getMatchedParams()`, `Resolver.ts:220`), lu par le contexte pour
330
+ nom → valeur par requête (`Resolver.getMatchedParams()`, `Resolver.ts#Resolver.getMatchedParams`), lu par le contexte pour
331
331
  les métadonnées et par les décorateurs `@Param`.
332
332
 
333
333
  > [!IMPORTANT]
@@ -354,20 +354,20 @@ correspondent à `/books/new` : `{id}` capturerait `"new"` comme un identifiant.
354
354
  ```
355
355
 
356
356
  Aucune spécificité n'est calculée : la première route déclarée qui correspond gagne
357
- (`routing-nonregression.test.ts:83`). La même règle vaut pour le catch-all `*`, qui absorbe tout ce qui
357
+ (`routing-nonregression.test.ts`). La même règle vaut pour le catch-all `*`, qui absorbe tout ce qui
358
358
  le suit — un `@All("*")` déclaré tôt masque le reste du contrôleur.
359
359
 
360
360
  > [!TIP]
361
361
  > Une exception utile : dans un contrôleur, une route dont le chemin vaut **exactement** `"*"` est
362
362
  > repoussée **en dernier** au moment du montage — la capture-tout d'un contrôleur ne masque donc jamais
363
- > ses propres routes, quel que soit l'ordre d'écriture (`hasMagic`, `routerDecorators.ts:253`). Ça ne
363
+ > ses propres routes, quel que soit l'ordre d'écriture (`hasMagic`, `routerDecorators.ts#controller`). Ça ne
364
364
  > vaut **que** pour `"*"` seul : `/files/*` reste ordonné comme les autres.
365
365
 
366
366
  ### Situation 2 — le même chemin, deux méthodes
367
367
 
368
368
  Deux routes peuvent partager un chemin et se distinguer par la méthode. La passe 1 essaie la première,
369
369
  qui **lève** un 405 sur la méthode ; l'exception est mémorisée et le scan **continue** jusqu'à la route
370
- qui accepte la méthode (`Router.resolve()`, `router.ts:260`).
370
+ qui accepte la méthode (`Router.resolve()`, `router.ts#Router.resolve`).
371
371
 
372
372
  ```ts ignore
373
373
  @Get("/book/{id}") show() {}
@@ -376,12 +376,12 @@ qui accepte la méthode (`Router.resolve()`, `router.ts:260`).
376
376
 
377
377
  Si **aucune** route n'accepte la méthode, la **passe 2** entre en scène : elle reparcourt la table,
378
378
  collecte toutes les méthodes servies par ce chemin **sur ce vhost**, et lève un 405 dont l'en-tête
379
- `Allow` est l'**agrégat** (`collectSupportedMethods()`, `router.ts:37`, qui lit le `methodsSet`
379
+ `Allow` est l'**agrégat** (`collectSupportedMethods()`, `router.ts#collectSupportedMethods`, qui lit le `methodsSet`
380
380
  compilé de chaque route — la MÊME source que le match). C'est la conformité RFC 9110 §15.5.6 :
381
381
  `Allow` liste tout ce que la ressource accepte, pas seulement ce que la dernière route scannée
382
382
  acceptait — `HEAD` compris dès qu'une route sert `GET`.
383
383
 
384
- **`HEAD` est implicite sur `GET`** (`Route.compileRequirements`, `Route.ts:518`) : RFC 9110 §9.1
384
+ **`HEAD` est implicite sur `GET`** (`Route.compileRequirements`, `Route.ts#Route.compileRequirements`) : RFC 9110 §9.1
385
385
  impose à tout serveur généraliste de servir `GET` et `HEAD`, et `HEAD` rend la réponse de `GET`
386
386
  sans corps (§9.3.2) — le serveur Node écarte le corps lui-même, en HTTP/1.1 comme en HTTP/2.
387
387
  L'ajout se fait à la compilation de la route : aucun coût par requête.
@@ -395,11 +395,11 @@ L'ajout se fait à la compilation de la route : aucun coût par requête.
395
395
  ### Situation 3 — une route réservée à un domaine
396
396
 
397
397
  Une route restreinte par `@Domain` est **invisible** aux requêtes des autres vhosts : elle lève un 403
398
- au lieu de participer au match (`Route.matchHostname()`, `Route.ts:701`). Le point de sécurité est
398
+ au lieu de participer au match (`Route.matchHostname()`, `Route.ts#Route.matchHostname`). Le point de sécurité est
399
399
  l'**ordre des vérifications** : le domaine est vérifié **avant** la méthode. Sans cela, une route d'un
400
400
  autre vhost pourrait répondre 405 en révélant SES méthodes — une fuite d'information cross-domaine
401
- (`Route.match()`, `Route.ts:343`). La passe 2 applique la même règle : les routes d'un autre vhost sont
402
- exclues du calcul de `Allow` (`isDomainAllowed`, `router.ts:379`).
401
+ (`Route.match()`, `Route.ts#Route.match`). La passe 2 applique la même règle : les routes d'un autre vhost sont
402
+ exclues du calcul de `Allow` (`isDomainAllowed`, `router.ts#Router.resolve`).
403
403
 
404
404
  Si une autre route du même chemin sert **tous** les vhosts, le scan continue jusqu'à elle : le 403
405
405
  n'interrompt pas la recherche, il ne conclut que s'il ne reste aucune candidate.
@@ -420,45 +420,45 @@ async chat(room: string, message: string | Buffer | null) { /* … */ }
420
420
  Ce qui change par rapport au HTTP :
421
421
 
422
422
  - **La route est résolue AVANT l'acceptation du handshake.** Le contexte WS passe par le même
423
- `handleFrontController()`, puis seulement `context.connect()` (`http-kernel.ts:1537`). Un chemin
423
+ `handleFrontController()`, puis seulement `context.connect()` (`http-kernel.ts#HttpKernel.handleHttp`). Un chemin
424
424
  inconnu ou un sous-protocole non conforme ferme la connexion **sans jamais l'ouvrir**.
425
425
  - **Le sous-protocole est un requirement de route.** Un `protocol` déclaré et non satisfait lève une
426
426
  erreur de code **1002** (Protocol Error, RFC 6455 §7.4) au lieu d'un statut HTTP
427
- (`acceptedProtocol`, `Route.ts:822`).
427
+ (`acceptedProtocol`, `Route.ts#Route.matchRequirements`).
428
428
  - **Le 405 ne s'applique pas au WebSocket.** La passe 2 est réservée au HTTP : sur un contexte WS,
429
- l'exception d'origine est préservée (`Router.resolve()`, `router.ts:260`).
429
+ l'exception d'origine est préservée (`Router.resolve()`, `router.ts#Router.resolve`).
430
430
  - **Un `Resolver` par connexion, réutilisé à chaque frame.** Il est créé au handshake, puis chaque
431
431
  message rejoue `match()` sur la route déjà trouvée avant d'appeler l'action
432
- (`WebsocketContext.handle()`, `WebsocketContext.ts:297` · boucle message,
433
- `callController`, `WebsocketContext.ts:59`).
432
+ (`WebsocketContext.handle()`, `WebsocketContext.ts#WebsocketContext.handle` · boucle message,
433
+ `callController`, `WebsocketContext.ts#callAsPromise`).
434
434
  L'action est donc invoquée une fois au handshake (`message` vaut `null`), puis une fois par frame.
435
435
 
436
436
  ### Duplex — le même chemin en HTTP et en WS
437
437
 
438
438
  Déclarer `methods: ["GET", "WEBSOCKET"]` rend une action joignable par les deux transports. C'est ce
439
439
  que fait le data plane d'administration pour toutes ses lectures (`AdminBroker.mountAll()` →
440
- `Router.createRoute()`, `AdminBroker.ts:125`). Deux conséquences :
440
+ `Router.createRoute()`, `AdminBroker.ts#AdminBroker.mountAll`). Deux conséquences :
441
441
 
442
442
  - **La pseudo-méthode `WEBSOCKET` n'apparaît jamais dans `Allow`** : ce n'est pas une méthode HTTP
443
443
  (RFC 9110 §10.2.1), elle est retirée de l'agrégat. Un chemin qui n'existe qu'en WebSocket, appelé
444
444
  en HTTP, répond `426 Upgrade Required` avec `Upgrade: websocket` plutôt qu'un 405
445
- (`routing-nonregression.test.ts:164`).
445
+ (`routing-nonregression.test.ts`).
446
446
  - **Une invocation WS d'une mutation doit dire quelle méthode logique elle vise.** Sur une socket,
447
447
  `context.method` vaut toujours `WEBSOCKET` : insuffisant pour distinguer un GET d'un POST sur le même
448
- chemin. Le pont transporte donc une **méthode logique** (`methodOverride`, `Resolver.ts:152`) que la
448
+ chemin. Le pont transporte donc une **méthode logique** (`methodOverride`, `Resolver.ts#Resolver.methodOverride`) que la
449
449
  route doit déclarer **en plus** du transport — une route `POST` qui n'annonce pas `WEBSOCKET` reste
450
- **injoignable** par socket (zéro contournement, `Route.ts:785`).
450
+ **injoignable** par socket (zéro contournement, `Route.ts#Route.matchRequirements`).
451
451
 
452
452
  Le routage par **message** (invoquer un chemin porté par une frame, sans toucher l'URL de la connexion)
453
453
  passe par le même `resolve()`, avec un chemin fourni en argument — l'état partagé de la socket n'est
454
- jamais muté (`Router.resolve()`, `router.ts:260`). Détails côté socket :
454
+ jamais muté (`Router.resolve()`, `router.ts#Router.resolve`). Détails côté socket :
455
455
  [socket Nodefony](../../../../../docs/architecture/realtime-socket-nodefony.md).
456
456
 
457
457
  ## Vhosts — une route par domaine
458
458
 
459
459
  `@Domain` restreint une méthode (ou tout un contrôleur) à un ou plusieurs noms d'hôte. Les motifs
460
460
  acceptent l'exact (`"marseille.fr"`) et le joker d'un label (`"*.cdn.example.com"`), compilés une fois
461
- au boot en expressions ancrées (`Route.compileHost()`, `Route.ts:687`).
461
+ au boot en expressions ancrées (`Route.compileHost()`, `Route.ts#Route.compileHost`).
462
462
 
463
463
  ```ts ignore
464
464
  @controller("/")
@@ -470,14 +470,14 @@ class MarseilleController extends Controller {
470
470
  ```
471
471
 
472
472
  Précédence, du plus fort au plus faible : `@route({ host })` › `@Domain` sur la méthode › `@Domain` sur
473
- la classe (`controller()`, `routerDecorators.ts:189`). Une route sans domaine est servie sur **tous** les
473
+ la classe (`controller()`, `routerDecorators.ts#controller`). Une route sans domaine est servie sur **tous** les
474
474
  vhosts, et ne coûte rien au matching (`hostRegexp` absent → aucun test, `Route.matchHostname()`,
475
- `Route.ts:701`).
475
+ `Route.ts#Route.matchHostname`).
476
476
 
477
477
  > [!WARNING]
478
478
  > `@Domain` déclare quels vhosts une route **sert** ; il ne remplace pas la barrière d'entrée. Un
479
479
  > `Host` inconnu du serveur est rejeté en amont (421 Misdirected Request, `checkValidDomain()`,
480
- > `http-kernel.ts:2050`) via la liste `trustedHosts` de `@nodefony/http`.
480
+ > `http-kernel.ts#HttpKernel.checkValidDomain`) via la liste `trustedHosts` de `@nodefony/http`.
481
481
 
482
482
  ## Préfixes — contrôleur, module, data plane
483
483
 
@@ -485,14 +485,14 @@ Trois niveaux de préfixe coexistent, et un seul est à ta main.
485
485
 
486
486
  1. **Le préfixe de contrôleur** — `@controller("/api/catalog")` est concaténé devant le chemin de
487
487
  chaque route de la classe, puis le chemin est normalisé : les `//` sont réduits et le slash final
488
- retiré (`Route.setPattern()`, `Route.ts:662`). Un chemin vide (`@Get("")`) désigne donc le préfixe
488
+ retiré (`Route.setPattern()`, `Route.ts#Route.setPattern`). Un chemin vide (`@Get("")`) désigne donc le préfixe
489
489
  lui-même.
490
490
  2. **Le module propriétaire** — il n'ajoute **aucun** préfixe d'URL. `@controllers([…])` enregistre la
491
491
  classe au boot et propage le nom du module sur les routes déjà créées, pour l'introspection et les
492
- logs (`Router.setController()`, `router.ts:489`). Un module tiers et ton app peuvent porter deux
493
- contrôleurs homonymes sans collision : la clé du registre est `module:Classe` (`router.ts:164`).
492
+ logs (`Router.setController()`, `router.ts#Router.setController`). Un module tiers et ton app peuvent porter deux
493
+ contrôleurs homonymes sans collision : la clé du registre est `module:Classe` (`router.ts#Router.constructor`).
494
494
  3. **Le data plane d'administration** — réservé, non négociable : `/nodefony/<namespace>/api/<endpoint>`
495
- (`AdminBroker.resolvePath()`, `AdminBroker.ts:99`). Trois segments minimum, pour ne jamais entrer en
495
+ (`AdminBroker.resolvePath()`, `AdminBroker.ts#AdminBroker.resolvePath`). Trois segments minimum, pour ne jamais entrer en
496
496
  collision avec les routes de l'application ni avec la SPA de Studio.
497
497
 
498
498
  > [!TIP]
@@ -503,46 +503,46 @@ Trois niveaux de préfixe coexistent, et un seul est à ta main.
503
503
  ## Nommer une route, la retrouver, l'appeler
504
504
 
505
505
  Chaque route porte un **nom unique** dans le processus : celui que tu donnes à `@route`, ou l'auto-nom
506
- `Classe::methode` des décorateurs de méthode (`routerDecorators.ts:348`). Le nom est le handle stable
506
+ `Classe::methode` des décorateurs de méthode (`routerDecorators.ts#extractControllerFilePath`). Le nom est le handle stable
507
507
  d'une route — il survit à un changement de chemin.
508
508
 
509
- | Besoin | Comment |
510
- | ---------------------------------------- | ------------------------------------------------------------------------- |
511
- | Retrouver une route par son nom | `router.getRoutes("ma-route")` → l'objet `Route` (`router.ts:326`) |
512
- | Lister toutes les routes | `router.getRoutes("")` → la table complète (`router.ts:442`) |
513
- | Savoir quelles routes couvrent un chemin | `router.matchRoutes("/api/x")` → les résultats de regex (`router.ts:431`) |
514
- | Appeler une autre action, en interne | `this.forward("module:Controller:action")` (`Controller.ts:697`) |
515
- | Retirer une route | `router.removeRoutes("ma-route")` (`router.ts:451`) |
509
+ | Besoin | Comment |
510
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
511
+ | Retrouver une route par son nom | `router.getRoutes("ma-route")` → l'objet `Route` (`router.ts#IndexedRoute.route`) |
512
+ | Lister toutes les routes | `router.getRoutes("")` → la table complète (`router.ts#Router.getRoutes`) |
513
+ | Savoir quelles routes couvrent un chemin | `router.matchRoutes("/api/x")` → les résultats de regex (`router.ts#Router.matchRoutes`) |
514
+ | Appeler une autre action, en interne | `this.forward("module:Controller:action")` (`Controller.ts#Controller.forward`) |
515
+ | Retirer une route | `router.removeRoutes("ma-route")` (`router.ts#Router.removeRoutes`) |
516
516
 
517
517
  **Il n'existe pas de générateur d'URL inverse côté serveur** (pas de `path("ma-route", {id})` à la
518
518
  Symfony). Le chemin déclaré est lisible sur l'objet `Route` (`route.path`), et la substitution des
519
519
  `{var}` est faite là où on en a besoin — par exemple par la console Studio, qui remplace chaque
520
- variable par sa valeur encodée (`buildUrl()`, `PlaygroundModel.ts:88`). Pour un lien interne, écris le
520
+ variable par sa valeur encodée (`buildUrl()`, `PlaygroundModel.ts#buildUrl`). Pour un lien interne, écris le
521
521
  chemin ; pour un appel interne, utilise `forward()`.
522
522
 
523
523
  **`forward()` n'est pas une redirection** : il résout `module:Controller:action` et exécute l'action
524
524
  dans le **même** contexte de requête, sans repasser par le réseau (`Resolver.parsePathernController()`,
525
- `Resolver.ts:247`). Une vraie redirection HTTP passe par `this.redirect(url, 302)` ou `@Redirect`.
525
+ `Resolver.ts#Resolver.parsePathernController`). Une vraie redirection HTTP passe par `this.redirect(url, 302)` ou `@Redirect`.
526
526
 
527
527
  ## 🧰 API publique
528
528
 
529
529
  Le routage s'utilise **par décorateurs** ; l'API impérative sert l'outillage (introspection, tests,
530
530
  modules qui montent des routes dynamiquement). Signatures complètes : `.ai/symbols.json`.
531
531
 
532
- | Symbole | Usage réel |
533
- | ------------------------------------------ | ----------------------------------------------------------------------- |
534
- | `Router.createRoute(nom, options)` | Monter une route sans décorateur (data plane, module dynamique). |
535
- | `Router.setController(classe, module)` | Rattacher une classe à un module (fait par `@controllers`). |
536
- | `router.resolve(context)` | Le cœur : rend un `Resolver` (`resolve === true` si trouvé). |
537
- | `router.getRoutes(nom)` · `removeRoutes()` | Introspection et démontage. |
538
- | `Route#path` · `#variables` · `#pattern` | Ce que la route déclare, après compilation. |
539
- | `Route#toObject()` · `#toLogLine()` | Sérialisation pour l'API admin · ligne de log lisible (`Route.ts:614`). |
540
- | `Resolver#route` · `#variables` | Ce que la requête courante a matché. |
541
- | `Resolver#getMatchedParams()` | Les variables en `nom → valeur` (`Resolver.ts:200`). |
532
+ | Symbole | Usage réel |
533
+ | ------------------------------------------ | ----------------------------------------------------------------------------------- |
534
+ | `Router.createRoute(nom, options)` | Monter une route sans décorateur (data plane, module dynamique). |
535
+ | `Router.setController(classe, module)` | Rattacher une classe à un module (fait par `@controllers`). |
536
+ | `router.resolve(context)` | Le cœur : rend un `Resolver` (`resolve === true` si trouvé). |
537
+ | `router.getRoutes(nom)` · `removeRoutes()` | Introspection et démontage. |
538
+ | `Route#path` · `#variables` · `#pattern` | Ce que la route déclare, après compilation. |
539
+ | `Route#toObject()` · `#toLogLine()` | Sérialisation pour l'API admin · ligne de log lisible (`Route.ts#Route.toLogLine`). |
540
+ | `Resolver#route` · `#variables` | Ce que la requête courante a matché. |
541
+ | `Resolver#getMatchedParams()` | Les variables en `nom → valeur` (`Resolver.ts#Resolver.match`). |
542
542
 
543
543
  > [!CAUTION]
544
544
  > **La table de routes est un état de processus, pas d'instance** : `Router.routes` est une liste
545
- > module-level partagée par tout le processus (`router.ts:48`). `removeRoutes()` sans argument la
545
+ > module-level partagée par tout le processus (`router.ts#routes`). `removeRoutes()` sans argument la
546
546
  > **vide pour tout le monde** — réservé aux bancs de test, qui sauvegardent et restaurent la table
547
547
  > autour de chaque cas.
548
548
 
@@ -553,50 +553,51 @@ alloué par requête.
553
553
 
554
554
  - **Compilation unique au montage** : motif d'URL, motifs de domaine, `Set` de méthodes en majuscules,
555
555
  chaîne `Allow`, et regex des requirements par variable sont figés à la création de la route
556
- (`Route.compileRequirements()`, `Route.ts:518`). Le matching ne fait plus que des lookups.
556
+ (`Route.compileRequirements()`, `Route.ts#Route.compileRequirements`). Le matching ne fait plus que des lookups.
557
557
  - **Un seul calcul de chemin par requête** : le `pathname` normalisé est calculé une fois puis passé à
558
558
  chaque route scannée — sinon le getter `URL.pathname`, la regex de normalisation et l'allocation de
559
- chaîne seraient refaits pour **chaque** route de la table (`Route.cleanPathname()`, `Route.ts:303`).
559
+ chaîne seraient refaits pour **chaque** route de la table (`Route.cleanPathname()`, `Route.ts#Route.cleanPathname`).
560
560
  - **Lookup O(1) pour les chemins littéraux**, scan pour les seuls chemins à motif — sans changer la
561
- séquence de candidats (`buildRouteIndex()`, `router.ts:130`). L'index est invalidé par toute mutation
561
+ séquence de candidats (`buildRouteIndex()`, `router.ts#buildRouteIndex`). L'index est invalidé par toute mutation
562
562
  de la table, avec un garde-fou sur une photo `longueur/première/dernière` qui rattrape même les
563
- mutations directes de la liste (`routeIndex`, `router.ts:127`).
564
- - **Zéro journalisation en production** : le log « route trouvée » est promu au niveau NOTICE hors
565
- production seulement, et le test est résolu une fois puis mémoïsé — en production, aucune chaîne
566
- n'est même construite (`routeNoticePromoted`, `router.ts:342`).
563
+ mutations directes de la liste (`routeIndex`, `router.ts#invalidateRouteIndex`).
564
+ - **Zéro journalisation en production** : le log « route trouvée » est émis en DEBUG hors
565
+ production seulement — imprimé avec `-d`, toujours lu par le suivi de requête de Studio — et le
566
+ test est résolu une fois puis mémoïsé : en production, aucune chaîne n'est même construite
567
+ (`routeMatchLogged`, `router.ts#routeMatchLogged`).
567
568
  - **Métadonnées d'action mémoïsées par route** au premier passage (`@HttpCode`, `@Header`, `@Redirect`,
568
569
  paramètres, intention de session) : plus aucune lecture `Reflect` par requête
569
- (`resolveActionMeta`, `Resolver.ts:458`).
570
+ (`resolveActionMeta`, `Resolver.ts#Resolver.executeAction`).
570
571
 
571
572
  ## 📜 Normes appliquées
572
573
 
573
- | Sujet | Norme | Où le code s'y conforme |
574
- | ---------------------------------------- | ----------------- | --------------------------------------------------------------- |
575
- | 405 + en-tête `Allow` agrégé | RFC 9110 §15.5.6 | passe 2 (`collectSupportedMethods()`, `router.ts:37`) |
576
- | `HEAD` servi par toute route `GET` | RFC 9110 §9.1 | `Route.compileRequirements` (`Route.ts:518`) |
577
- | Cible identifiée par l'URI, hôte compris | RFC 9110 §7.2 | hôte vérifié avant la méthode (`Route.match()`, `Route.ts:343`) |
578
- | 403 sur ressource d'un autre vhost | RFC 9110 §15.5.4 | `Route.matchHostname()` (`Route.ts:701`) |
579
- | 404 quand rien ne correspond | RFC 9110 §15.5.5 | après repli statique (`http-kernel.ts:688`) |
580
- | 421 sur `Host` non servi | RFC 9110 §15.5.20 | `checkValidDomain()` (`http-kernel.ts:2050`) |
581
- | Erreur de sous-protocole WS = 1002 | RFC 6455 §7.4 | `Route.matchRequirements()` (`Route.ts:763`) |
582
- | Décodage pourcent des segments | RFC 3986 §2.1 | `decode()` (`Route.ts:79`) |
574
+ | Sujet | Norme | Où le code s'y conforme |
575
+ | ---------------------------------------- | ----------------- | --------------------------------------------------------------------------- |
576
+ | 405 + en-tête `Allow` agrégé | RFC 9110 §15.5.6 | passe 2 (`collectSupportedMethods()`, `router.ts#collectSupportedMethods`) |
577
+ | `HEAD` servi par toute route `GET` | RFC 9110 §9.1 | `Route.compileRequirements` (`Route.ts#Route.compileRequirements`) |
578
+ | Cible identifiée par l'URI, hôte compris | RFC 9110 §7.2 | hôte vérifié avant la méthode (`Route.match()`, `Route.ts#Route.match`) |
579
+ | 403 sur ressource d'un autre vhost | RFC 9110 §15.5.4 | `Route.matchHostname()` (`Route.ts#Route.matchHostname`) |
580
+ | 404 quand rien ne correspond | RFC 9110 §15.5.5 | après repli statique (`http-kernel.ts#HttpKernel.isWebsocketOriginAllowed`) |
581
+ | 421 sur `Host` non servi | RFC 9110 §15.5.20 | `checkValidDomain()` (`http-kernel.ts#HttpKernel.checkValidDomain`) |
582
+ | Erreur de sous-protocole WS = 1002 | RFC 6455 §7.4 | `Route.matchRequirements()` (`Route.ts#Route.matchRequirements`) |
583
+ | Décodage pourcent des segments | RFC 3986 §2.1 | `decode()` (`Route.ts#decode`) |
583
584
 
584
585
  ## 📡 Observabilité — Studio
585
586
 
586
587
  La table de routes est introspectable en ligne, sans lire le code :
587
588
 
588
589
  - **`GET /nodefony/framework/api/routes`** — dump de toutes les routes enregistrées : nom, chemin,
589
- méthodes, contrôleur (`FrameworkAdminApi.ts:123`). Variante paginée/triée/filtrée côté serveur :
590
+ méthodes, contrôleur (`FrameworkAdminApi.ts#createFrameworkAdminApi`). Variante paginée/triée/filtrée côté serveur :
590
591
  `routes/page`.
591
592
  - **`GET /nodefony/framework/api/info`** — résumé : nombre de routes, méthodes servies, modules
592
- propriétaires (`FrameworkAdminApi.ts:356`).
593
+ propriétaires (`FrameworkAdminApi.ts#createFrameworkAdminApi`).
593
594
  - **Écran Routes** de Studio (`/nodefony/routes`) — la même table, filtrable.
594
595
  - **Playground** (`/nodefony/playground`, développement uniquement) — un formulaire par action, généré depuis la table :
595
596
  transports (dont le duplex), paramètres décorés, gardes de sécurité. Il **exécute** de vraies actions,
596
597
  donc il n'est monté qu'en développement (`PlaygroundAdminApi.ts`).
597
598
 
598
599
  Au boot, avec le debug actif, chaque route est aussi journalisée en une ligne
599
- `[MÉTHODES] chemin → @module/Controller.action` (`Route.toLogLine()`, `Route.ts:614`).
600
+ `[MÉTHODES] chemin → @module/Controller.action` (`Route.toLogLine()`, `Route.ts#Route.toLogLine`).
600
601
 
601
602
  ## ⚠️ Pièges (symptôme → cause → correction)
602
603
 
@@ -605,11 +606,11 @@ Au boot, avec le debug actif, chaque route est aussi journalisée en une ligne
605
606
  | --- | --- | --- |
606
607
  | 404 sur toutes les routes d'un contrôleur | `@controller` évalué avant les `@route`/`@Get` de la classe | Placer `@controller` **au-dessus** de la classe, décorateurs de méthode dans la classe |
607
608
  | 404 sur une route pourtant écrite | Le fichier du contrôleur n'est jamais importé — les routes naissent à l'import | Le déclarer dans `@controllers([…])` du module |
608
- | `405` alors que la méthode « est déclarée » | `method: "GET"` dans `@route` n'est **pas** filtrant | Utiliser `requirements: { methods: ["GET"] }` ou `@Get` |
609
+ | `405` sur un `POST` qui marchait | `method: "GET"` dans `@route` est **filtrant** : il est versé dans `requirements.methods` (`Route.ts`), `HEAD` reste servi avec `GET` | Déclarer toutes les méthodes servies : `method: ["GET", "POST"]` |
609
610
  | Une route paramétrée avale un chemin littéral | Premier match dans l'ordre de déclaration, aucune spécificité | Déclarer le littéral **avant** le paramétré |
610
611
  | `/files/*` ne répond pas sur `/files` | Le slash final est retiré avant le matching ; le motif exige `/files/` | Déclarer une seconde route pour le chemin nu |
611
612
  | `{id}` ne capture pas `a/b` | Une variable vaut `[^/]+` — un seul segment, par construction | Utiliser un wildcard `*` si le `/` doit être capturé |
612
- | `500` au lieu d'un non-match sur une contrainte | Un requirement par variable non satisfait **lève** (chaîne brute, `Route.ts:286`) | Préférer la contrainte inline `{id}(\d+)`, qui ne matche pas |
613
+ | `500` au lieu d'un non-match sur une contrainte | Un requirement par variable non satisfait **lève** (chaîne brute, `Route.ts#Route.methodsSet`) | Préférer la contrainte inline `{id}(\d+)`, qui ne matche pas |
613
614
  | `403` inattendu sur une route qui « existe » | La route est restreinte à un autre vhost (`@Domain`) | Retirer la restriction, ou servir ce vhost |
614
615
  | Action WebSocket jamais atteinte | Transport `WEBSOCKET` absent des méthodes déclarées | `requirements: { methods: ["WEBSOCKET"] }` |
615
616
  | Une action nommée `session`/`request`/`method` est refusée | Le décorateur refuse tout nom déjà porté par `Controller` — il masquerait l'action | Renommer l'action (réservés : tout membre de `Controller`/`Service` — `session`, `get`, `set`, `remove`, `request`, `response`, `method`…) |