@nodefony/framework 10.0.0-beta.3 → 10.0.0-beta.5

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 (66) hide show
  1. package/README.md +19 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +9 -18
  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 +39 -8
  7. package/dist/nodefony/controller/ApiKeyController.js +16 -8
  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 +4 -11
  11. package/dist/nodefony/controller/OAuth2Controller.js +3 -4
  12. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
  13. package/dist/nodefony/controller/SessionAuthController.js +13 -4
  14. package/dist/nodefony/controller/TokenAuthController.js +3 -4
  15. package/dist/nodefony/controller/TotpController.js +2 -3
  16. package/dist/nodefony/controller/WebAuthnController.js +2 -3
  17. package/dist/nodefony/controller/oauthAuthority.js +1 -2
  18. package/dist/nodefony/decorators/routerDecorators.js +155 -53
  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 +49 -15
  23. package/dist/nodefony/src/Controller.js +32 -39
  24. package/dist/nodefony/src/FrameworkAdminApi.js +31 -10
  25. package/dist/nodefony/src/KernelAdminApi.js +7 -9
  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 +68 -33
  29. package/dist/nodefony/src/ResourceController.js +13 -6
  30. package/dist/nodefony/src/Route.js +2 -3
  31. package/dist/nodefony/src/SecurityApiController.js +1 -2
  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 +1 -2
  36. package/dist/nodefony/src/idempotency.js +1 -2
  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 +2 -3
  40. package/dist/nodefony/src/requireAuthentication.js +1 -2
  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 +7 -2
  45. package/dist/types/index.d.ts +5 -10
  46. package/dist/types/nodefony/controller/AdminApiController.d.ts +13 -1
  47. package/dist/types/nodefony/controller/SessionAuthController.d.ts +7 -0
  48. package/dist/types/nodefony/decorators/routerDecorators.d.ts +133 -32
  49. package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
  50. package/dist/types/nodefony/service/router.d.ts +27 -1
  51. package/dist/types/nodefony/src/Controller.d.ts +23 -17
  52. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
  53. package/dist/types/nodefony/src/Resolver.d.ts +33 -1
  54. package/dist/types/nodefony/src/ResourceController.d.ts +12 -4
  55. package/dist/types/nodefony/src/Route.d.ts +3 -0
  56. package/dist/types/nodefony/src/loginPage.d.ts +1 -1
  57. package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
  58. package/dist/types/nodefony/src/syslogFilters.d.ts +6 -0
  59. package/docs/admin.md +131 -82
  60. package/docs/controller.md +109 -110
  61. package/docs/decorateurs.md +214 -161
  62. package/docs/idempotence.md +154 -136
  63. package/docs/routing.md +105 -90
  64. package/docs/templates.md +55 -55
  65. package/package.json +8 -11
  66. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/admin.md CHANGED
@@ -61,9 +61,9 @@ flowchart TD
61
61
  Trois idées à retenir :
62
62
 
63
63
  1. **Le producteur ne connaît pas le transport** — un handler lit un `IAdminRequest`
64
- (`IAdminApi.ts:33`) et rend du JSON. Il ne touche jamais au socket ni à la `Response`.
64
+ (`IAdminApi.ts#IAdminRequest`) et rend du JSON. Il ne touche jamais au socket ni à la `Response`.
65
65
  2. **Un seul controller pont** — toutes les routes admin pointent vers
66
- `AdminApiController.dispatch()` (`AdminApiController.ts:65`). Pas de génération dynamique de
66
+ `AdminApiController.dispatch()` (`AdminApiController.ts#AdminApiController.dispatch`). Pas de génération dynamique de
67
67
  classes ; chaque route reste une vraie `Route` (404/405 du Router intacts).
68
68
  3. **Le broker possède le Router, pas le kernel** — c'est pourquoi il vit dans `@nodefony/framework`,
69
69
  niveau qui monte les routes, alors que le contrat producteur vit dans le core.
@@ -82,7 +82,7 @@ Trois idées à retenir :
82
82
  | RBAC | _Role-Based Access Control_ : l'accès dépend des rôles de l'appelant. |
83
83
  | `ROLE_NODEFONY_*` | Rôles de la **plateforme** (admin du framework), distincts des rôles applicatifs `ROLE_*` d'un tenant. |
84
84
  | Pont / dispatch | Le controller unique qui, à chaque requête, retrouve l'endpoint et exécute son handler. |
85
- | API souveraine | Toute action admin déclare AUSSI le transport WebSocket → invocable par le pont WS-RPC `api.request`. |
85
+ | Deux transports | Toute action admin déclare AUSSI le transport WebSocket → invocable par le pont WS-RPC `api.request`. |
86
86
  | Duplex | Une même action servie sur HTTP **et** WebSocket (le différenciateur Nodefony). |
87
87
  | Catalogue / discovery | La liste des producteurs + endpoints, exposée en JSON pour que Studio bâtisse sa navigation. |
88
88
  | Playground | La console dev qui joue n'importe quel controller depuis le navigateur (`/nodefony/playground`). |
@@ -110,19 +110,19 @@ le broker la monte.
110
110
 
111
111
  Nodefony sépare **deux rôles** par inversion de dépendance :
112
112
 
113
- - **Producteur** (`IAdminApi`, dans le core `IAdminApi.ts:212`) : un module dit _quoi_ il expose —
113
+ - **Producteur** (`IAdminApi`, dans le core `IAdminApi.ts#IAdminApi`) : un module dit _quoi_ il expose —
114
114
  son `adminNamespace` et ses `adminEndpoints()` — **sans importer `@nodefony/framework`**. Le
115
115
  contrat vit au plus bas niveau commun pour qu'un adapter ORM, un service IA ou le kernel lui-même
116
116
  puissent l'implémenter.
117
- - **Transporteur** (`AdminBroker`, dans le framework `AdminBroker.ts:21`) : lui seul possède le
117
+ - **Transporteur** (`AdminBroker`, dans le framework `AdminBroker.ts#AdminBroker`) : lui seul possède le
118
118
  Router. Il collecte les producteurs et monte `/nodefony/<ns>/api/*`.
119
119
 
120
120
  Pour s'enregistrer sans dépendre du framework, un producteur récupère le broker via sa **vue
121
- minimale** `IAdminRegistry` (`IAdminApi.ts:243`) — juste `register()` — depuis le container. Le
121
+ minimale** `IAdminRegistry` (`IAdminApi.ts#IAdminRegistry`) — juste `register()` — depuis le container. Le
122
122
  kernel n'étant **pas** un `Module`, c'est le framework qui construit et enregistre l'`IAdminApi` du
123
123
  kernel à sa place (`createKernelAdminApi()`, cité plus bas).
124
124
 
125
- Le compromis assumé : **un seul controller pont** (`AdminApiController.ts:36`) sert les N endpoints.
125
+ Le compromis assumé : **un seul controller pont** (`AdminApiController.ts#AdminApiController`) sert les N endpoints.
126
126
  On y gagne zéro génération de classe, un dispatch O(1), et une garde RBAC + idempotence appliquée au
127
127
  même endroit pour tout le monde.
128
128
 
@@ -237,7 +237,7 @@ curl -s -b /tmp/jar http://localhost:5151/nodefony/framework/api/admin | head -c
237
237
  ```
238
238
 
239
239
  > [!NOTE]
240
- > Chaque réponse HTTP porte un en-tête `x-nodefony-instance` (`AdminApiController.ts:84`) : en
240
+ > Chaque réponse HTTP porte un en-tête `x-nodefony-instance` (`AdminApiController.ts#AdminApiController.dispatch`) : en
241
241
  > multi-pod, il dit **quel process** a répondu (le data plane est per-instance).
242
242
 
243
243
  ## 🏗️ Architecture interne — register → mountAll → dispatch
@@ -262,63 +262,63 @@ sequenceDiagram
262
262
  AC-->>RT: handler(IAdminRequest) → JSON / RpcError
263
263
  ```
264
264
 
265
- | # | Étape | Où |
266
- | --- | ----------------------------------------- | ---------------------------------------------------------- |
267
- | 1 | Le producteur s'enregistre | `AdminBroker.register()` (`AdminBroker.ts:53`) |
268
- | 2 | Le framework monte tout | `AdminBroker.mountAll()` (`AdminBroker.ts:112`) |
269
- | 3 | Une route par endpoint (nom déterministe) | `Router.createRoute()` (`AdminBroker.ts:124`) |
270
- | 4 | Le controller pont estampillé une fois | `Router.setController()` idempotent (`AdminBroker.ts:146`) |
271
- | 5 | Dispatch : lookup de la route | `AdminBroker.resolve()` (`AdminApiController.ts:102`) |
272
- | 6 | Projection du contexte en requête admin | `buildRequest()` (`AdminApiController.ts:199`) |
273
- | 7 | Normalisation du retour | `normalizeAdminResult()` (`executeAdmin.ts:90`) |
265
+ | # | Étape | Où |
266
+ | --- | ----------------------------------------- | ----------------------------------------------------------------------------- |
267
+ | 1 | Le producteur s'enregistre | `AdminBroker.register()` (`AdminBroker.ts#AdminBroker.register`) |
268
+ | 2 | Le framework monte tout | `AdminBroker.mountAll()` (`AdminBroker.ts#AdminBroker.mountAll`) |
269
+ | 3 | Une route par endpoint (nom déterministe) | `Router.createRoute()` (`AdminBroker.ts#AdminBroker.mountAll`) |
270
+ | 4 | Le controller pont estampillé une fois | `Router.setController()` idempotent (`AdminBroker.ts#AdminBroker.mountAll`) |
271
+ | 5 | Dispatch : lookup de la route | `AdminBroker.resolve()` (`AdminApiController.ts#AdminApiController.runAdmin`) |
272
+ | 6 | Projection du contexte en requête admin | `buildRequest()` (`AdminApiController.ts#AdminApiController.buildRequest`) |
273
+ | 7 | Normalisation du retour | `normalizeAdminResult()` (`executeAdmin.ts#normalizeAdminResult`) |
274
274
 
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:53`) : on ne monte pas une route à chaud (Zero surprise en prod). Le broker garde
278
+ (`AdminBroker.ts#AdminBroker.register`) : 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:123`) — c'est
280
+ - **Le nom de route est déterministe** : `admin.<ns>.<method>.<path>` (`AdminBroker.ts#AdminBroker.mountAll`) — c'est
281
281
  la clé du lookup O(1) que le pont refait à chaque requête.
282
- - **`Router.setController` n'est appelé qu'une fois** par process (`AdminBroker.ts:146`) : il pose
282
+ - **`Router.setController` n'est appelé qu'une fois** par process (`AdminBroker.ts#AdminBroker.mountAll`) : 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:108`) — jamais un état retenu.
286
+ (`AdminBroker.ts#AdminBroker.routes`) — jamais un état retenu.
287
287
 
288
288
  > [!IMPORTANT]
289
289
  > Convention de route **figée** : le data plane est toujours en **≥ 3 segments**
290
- > `/nodefony/<module>/api/*` (`IAdminBroker.ts:42`). Jamais une route admin mono-segment
290
+ > `/nodefony/<module>/api/*` (`IAdminBroker.ts#IAdminBroker`). Jamais une route admin mono-segment
291
291
  > `/nodefony/<module>` — elle entrerait en collision avec le fallback SPA de Studio. Le chemin
292
- > relatif d'un endpoint a **≥ 1 segment** (`types/IAdminApi.ts:155`) : la racine `/nodefony/<ns>/api` est
292
+ > relatif d'un endpoint a **≥ 1 segment** (`types/IAdminApi.ts#IAdminEndpoint.path`) : la racine `/nodefony/<ns>/api` est
293
293
  > réservée.
294
294
 
295
295
  ## 🔐 RBAC — autorisation du data plane
296
296
 
297
297
  Deux gardes se succèdent, dans cet ordre :
298
298
 
299
- 1. **Le firewall AUTHENTIFIE en amont.** La zone `nodefony-admin` (`config.ts:137`) couvre
300
- `^/nodefony/[^/]+/api(/|$)` (`config.ts:141`) avec l'authenticator `session` (cookie BFF) : sans
299
+ 1. **Le firewall AUTHENTIFIE en amont.** La zone `nodefony-admin` (`config.ts`) couvre
300
+ `^/nodefony/[^/]+/api(/|$)` (`config.ts`) avec l'authenticator `session` (cookie BFF) : sans
301
301
  session, c'est **401** avant même le controller.
302
302
  2. **Le broker tranche le RÔLE.** À l'exécution, le pont compare le rôle exigé aux rôles de
303
- l'appelant via la fonction pure `isAdminGranted()` (`adminRbac.ts:24`). Rôle absent → **403**.
303
+ l'appelant via la fonction pure `isAdminGranted()` (`adminRbac.ts#isAdminGranted`). Rôle absent → **403**.
304
304
  Le refus est prononcé au CŒUR (`executeAdmin.ts`), pas dans le controller HTTP : c'est ce
305
305
  qui fait que les deux chemins d'appel — la route et le pont MCP — refusent à l'identique.
306
306
 
307
307
  La décision est **fail-closed** : un authentifié **sans** le rôle requis — y compris `roles=[]`
308
- (compte non doté) — est **rejeté** (`adminRbac.ts:27`). C'était l'ex-fail-open historique (un
308
+ (compte non doté) — est **rejeté** (`adminRbac.ts#isAdminGranted`). C'était l'ex-fail-open historique (un
309
309
  `roles.length > 0 &&` héritait du « mode mock » d'avant l'auth) : l'absence de rôle ne vaut pas
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:166`).
314
- - **Endpoint public** : `public: true` (`IAdminApi.ts:175`) → le RBAC du broker est court-circuité
315
- (`role === ""`, `adminRbac.ts:26`). À réserver aux sondes cloud-native (liveness/readiness) et à
313
+ (`AdminBroker.ts#AdminBroker.mountAll` ; défaut du champ `IAdminEndpoint.role`, `IAdminApi.ts#IAdminEndpoint.role`).
314
+ - **Endpoint public** : `public: true` (`IAdminApi.ts#IAdminEndpoint.public`) → le RBAC du broker est court-circuité
315
+ (`role === ""`, `adminRbac.ts#isAdminGranted`). À 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
- `GET /nodefony/kernel/api/livez` (`KernelAdminApi.ts:513`), sorti de `nodefony-admin` par la zone
318
- publique `nodefony-liveness` (`config.ts:132`).
319
- - **Endpoint qui reçoit un mot de passe** : `credentials: true` (`IAdminApi.ts:189`). En
317
+ `GET /nodefony/kernel/api/livez` (`KernelAdminApi.ts#auditLogLevelChange`), sorti de `nodefony-admin` par la zone
318
+ publique `nodefony-liveness` (`config.ts`).
319
+ - **Endpoint qui reçoit un mot de passe** : `credentials: true` (`IAdminApi.ts#IAdminEndpoint.credentials`). En
320
320
  production, une requête arrivée en clair est refusée en **403 avant le rôle et le handler**
321
- (`executeAdmin.ts:144`) — la décision est celle de security (`credentialTransport`), le
321
+ (`executeAdmin.ts#executeAdminEndpoint`) — la décision est celle de security (`credentialTransport`), le
322
322
  contrôleur la fournit pour HTTP et pour le pont WS. La CLI n'a pas de transport : rien n'est
323
323
  jugé. Exemples : `POST me/password`, `POST users/{id}/password`, `POST users` (module `user`).
324
324
 
@@ -327,21 +327,21 @@ laissez-passer.
327
327
  > applicatifs `ROLE_*` d'un tenant. Un endpoint peut exiger un rôle plus fin (`role: "ROLE_…"`) ou
328
328
  > graduer l'information **dans** son handler en lisant `request.roles`.
329
329
 
330
- ## 🔌 HTTP et WebSocket — la même action (API souveraine)
330
+ ## 🔌 HTTP et WebSocket — la même action
331
331
 
332
- Toute action admin déclare **aussi** le transport `WEBSOCKET` (`AdminBroker.ts:123`) : la route est
332
+ Toute action admin déclare **aussi** le transport `WEBSOCKET` (`AdminBroker.ts#AdminBroker.mountAll`) : la route est
333
333
  montée avec `[method, "WEBSOCKET"]`. Elle devient donc invocable par le pont WS-RPC `api.request`
334
- (`WebsocketContext.ts:354`) — même action, même handler, même réponse. Seul l'emballage diffère :
334
+ (`WebsocketContext.ts#WebsocketContext.request`) — même action, même handler, même réponse. Seul l'emballage diffère :
335
335
 
336
- - **HTTP** : `renderJson` + statut + en-tête `x-nodefony-instance` (`AdminApiController.ts:82`).
336
+ - **HTTP** : `renderJson` + statut + en-tête `x-nodefony-instance` (`AdminApiController.ts#AdminApiController.dispatch`).
337
337
  - **WS-RPC** : la valeur **nue** (le pont l'enveloppe `{id, result}`) ; un statut ≥ 400 devient un
338
- `RpcError` avec `data.status`/`data.body` (`AdminApiController.ts:66`), symétrie d'un `fetch` qui
338
+ `RpcError` avec `data.status`/`data.body` (`AdminApiController.ts#AdminApiController.dispatch`), symétrie d'un `fetch` qui
339
339
  expose son statut.
340
340
 
341
341
  Les **mutations** sont pontables par socket. La sécurité d'écriture repose alors sur l'**idempotence**
342
- (`idempotencyGate()`, `AdminApiController.ts:155`) : la clé `Idempotency-Key` est **obligatoire en
342
+ (`idempotencyGate()`, `AdminApiController.ts#AdminApiController.idempotencyGate`) : la clé `Idempotency-Key` est **obligatoire en
343
343
  WS** (une socket reconnecte et rejoue), **optionnelle en HTTP** (`required: false`,
344
- `AdminApiController.ts:157`). Un `GET` n'est jamais idempotenté (`AdminApiController.ts:143`) ; la
344
+ `AdminApiController.ts#AdminApiController.idempotencyGate`). Un `GET` n'est jamais idempotenté (`AdminApiController.ts#AdminApiController.idempotencyGate`) ; la
345
345
  porte est évaluée **après** le RBAC (un 403 ne consomme aucune entrée). Le helper est le **même** que
346
346
  le seam `@Idempotent` des controllers userland — voir [Idempotence](idempotence.md).
347
347
 
@@ -350,37 +350,86 @@ le seam `@Idempotent` des controllers userland — voir [Idempotence](idempotenc
350
350
  Trois pas, du point de vue d'un module :
351
351
 
352
352
  1. **Écrire un `IAdminApi`** : `adminNamespace` (url-safe, stable), `adminDescriptor()` (sidebar
353
- Studio, `IAdminApi.ts:227`) et `adminEndpoints()` (`IAdminApi.ts:229`). Un endpoint peut renvoyer
353
+ Studio, `IAdminApi.ts#IAdminApi.adminDescriptor`) et `adminEndpoints()` (`IAdminApi.ts#IAdminApi.adminEndpoints`). Un endpoint peut renvoyer
354
354
  la donnée brute (assumée `{status:200, body}`) ou une `IAdminResponse` pour piloter statut/en-têtes
355
- (`IAdminApi.ts:67`).
356
- 2. **S'enregistrer au `onKernelBoot`** via `IAdminRegistry.register()` (`IAdminApi.ts:256`), récupéré
355
+ (`IAdminApi.ts#IAdminResponse`).
356
+ 2. **S'enregistrer au `onKernelBoot`** via `IAdminRegistry.register()` (`IAdminApi.ts#IAdminRegistry.register`), récupéré
357
357
  par `container.get("adminBroker")`. Rendre l'appel **idempotent** (`registry.has(ns)` avant
358
358
  `register`) — modèle de tous les producteurs.
359
359
  3. **Laisser le framework monter** : à `onKernelReady`, `Framework.onKernelReady()` enregistre les
360
- producteurs internes puis appelle `broker.mountAll()` (`index.ts:369`).
360
+ producteurs internes puis appelle `broker.mountAll()` (`index.ts#Framework.onKernelReady`).
361
361
 
362
362
  **Handlers lazy** : résous tes services **dans** le handler (à la requête), jamais au montage — un
363
363
  service désactivable renvoie alors `503` proprement au lieu de casser le boot.
364
364
 
365
+ ### Ce que publie un endpoint — l'état réel, aucun secret, ses capacités déclarées
366
+
367
+ Tout ce que renvoie un handler part tel quel vers la console, le pont socket et les agents. Trois
368
+ règles en découlent.
369
+
370
+ **1. Un DTO ne porte aucun secret : il en dit la présence.** Un mot de passe, une clé ou un jeton
371
+ se décrivent par un booléen. Le résumé d'un utilisateur expose `hasPassword`, jamais le hash
372
+ (`UserAdminApi.ts#IUserSummary`). La description du pare-feu expose `synchronizerToken: boolean`
373
+ (`IFirewallDescription.ts#IFirewallDescription`). La redaction se fait **en construisant** le DTO,
374
+ champ par champ. Copier l'objet puis retirer les clés sensibles laisserait passer la prochaine qui
375
+ sera ajoutée. Un test le prouve : `JSON.stringify(dto)` ne contient pas la valeur sentinelle posée
376
+ dans la configuration.
377
+
378
+ **2. Une description montre ce qui TOURNE, pas la configuration relue.** Une méthode `describe()`
379
+ lit les structures que le service a bâties au démarrage
380
+ (`IFirewall.ts#IFirewall.describe`, `IBackplane.ts#IBackplane.describe`). Un authenticator mal
381
+ orthographié dans la configuration n'y figure pas, puisqu'il n'est pas monté. Relire la
382
+ configuration montrerait l'intention, et la console afficherait comme active une défense qui ne
383
+ tourne pas.
384
+
385
+ **3. Une capacité de liste non déclarée est refusée en `400`, jamais ignorée.** Tri, filtres,
386
+ recherche et facettes se déclarent dans `page` (`IAdminApi.ts#IAdminPageCapabilities`), et le
387
+ handler lit **la même donnée** : il passe ses champs triables à `parsePageQuery()`
388
+ (`pageQuery.ts#parsePageQuery`) et son vocabulaire de filtres à `parseFilters()`
389
+ (`pageFilters.ts#parseFilters`). Ces deux lecteurs refusent en `400` un tri hors liste, un `?q=`
390
+ sur un endpoint qui ne cherche pas, une valeur mal formée et un paramètre que personne ne
391
+ reconnaît. Lire `request.query.order` à la main renvoie une page non triée que le client prend pour
392
+ triée. Avec un filtre mal orthographié, c'est la collection entière qui revient.
393
+
394
+ ```typescript ignore
395
+ import { parseFilters, parsePageQuery, type IAdminEndpoint } from "nodefony";
396
+
397
+ // Une constante, deux lectures : le catalogue et le handler ne peuvent pas diverger.
398
+ const SORTABLE = ["createdAt", "total"] as const;
399
+ const FILTERS = { status: ["paid", "pending", "refunded"] } as const;
400
+
401
+ const listOrders: IAdminEndpoint = {
402
+ path: "orders",
403
+ method: "GET",
404
+ summary: "Commandes, paginées.",
405
+ page: { sortable: () => SORTABLE, filters: FILTERS },
406
+ handler: async (request) => {
407
+ const query = parsePageQuery(request.query, { sortable: SORTABLE }); // ?order=x → 400
408
+ const filters = parseFilters(request.query, FILTERS); // ?status=zzz, ?stauts= → 400
409
+ return orders.listPage({ ...query, ...filters });
410
+ },
411
+ };
412
+ ```
413
+
365
414
  ### Les producteurs réels (à imiter, sans les redocumenter)
366
415
 
367
- Le broker lui-même est déclaré comme service du framework (`index.ts:144`). Les producteurs internes
416
+ Le broker lui-même est déclaré comme service du framework (`index.ts`). Les producteurs internes
368
417
  sont enregistrés au `onKernelReady` du framework :
369
418
 
370
- | Namespace | Producteur | Rôle |
371
- | ----------- | ----------------------------------------------------- | ------------------------------------------------- |
372
- | `kernel` | `createKernelAdminApi` (`KernelAdminApi.ts:646`) | modules, process, uptime, `livez` |
373
- | `framework` | `createFrameworkAdminApi` (`FrameworkAdminApi.ts:86`) | dump du Router + **catalogue** + Playground (dev) |
374
- | `syslog` | `createSyslogAdminApi` (`SyslogAdminApi.ts:95`) | viewer de logs (dev) |
419
+ | Namespace | Producteur | Rôle |
420
+ | ----------- | -------------------------------------------------------------------------- | ------------------------------------------------- |
421
+ | `kernel` | `createKernelAdminApi` (`KernelAdminApi.ts#createKernelAdminApi`) | modules, process, uptime, `livez` |
422
+ | `framework` | `createFrameworkAdminApi` (`FrameworkAdminApi.ts#createFrameworkAdminApi`) | dump du Router + **catalogue** + Playground (dev) |
423
+ | `syslog` | `createSyslogAdminApi` (`SyslogAdminApi.ts#createSyslogAdminApi`) | viewer de logs (dev) |
375
424
 
376
425
  Les modules externes s'enregistrent depuis leur propre `onKernelBoot` :
377
426
 
378
- | Namespace | Module | Enregistrement |
379
- | ---------- | -------------------- | ---------------------------------------------------- |
380
- | `http` | `@nodefony/http` | `createHttpAdminApi` (`http/index.ts:121`) |
381
- | `security` | `@nodefony/security` | `registerSecurityAdminApi` (`security/index.ts:114`) |
382
- | `user` | `@nodefony/user` | `adminNamespace` (`UserAdminApi.ts:900`) |
383
- | `orm` | `@nodefony/orm-core` | `adminNamespace` (`OrmAdminApi.ts:709`) |
427
+ | Namespace | Module | Enregistrement |
428
+ | ---------- | -------------------- | ---------------------------------------------------------------------- |
429
+ | `http` | `@nodefony/http` | `createHttpAdminApi` (`http/index.ts#Http.onKernelRegister`) |
430
+ | `security` | `@nodefony/security` | `registerSecurityAdminApi` (`security/index.ts#Security.onKernelBoot`) |
431
+ | `user` | `@nodefony/user` | `adminNamespace` (`UserAdminApi.ts#createUserAdminApi`) |
432
+ | `orm` | `@nodefony/orm-core` | `adminNamespace` (`OrmAdminApi.ts#createOrmAdminApi`) |
384
433
 
385
434
  Pour le détail de chacun, se reporter à la doc de son module — le broker reste agnostique de leur
386
435
  contenu.
@@ -392,42 +441,42 @@ Depuis une app : `AdminBroker` (le service) et les types `IAdminApi`, `IAdminEnd
392
441
  `@nodefony/framework` et `nodefony`. Les signatures exactes vivent dans `.ai/symbols.json` — jamais
393
442
  recopiées ici (elles s'y périmeraient).
394
443
 
395
- | Membre (`IAdminBroker`) | Rôle | Ancre |
396
- | ------------------------ | -------------------------------------------------------- | -------------------- |
397
- | `register(api)` | Enregistre un producteur (throw si namespace pris/monté) | `AdminBroker.ts:53` |
398
- | `unregister(ns)` | Retire un producteur (et ses routes si montées) | `AdminBroker.ts:69` |
399
- | `has(ns)` / `getApi(ns)` | Interrogation du registre | `AdminBroker.ts:91` |
400
- | `list()` | Producteurs enregistrés (immuable) | `AdminBroker.ts:95` |
401
- | `resolvePath(ns, path)` | Chemin absolu d'un endpoint sans le monter | `AdminBroker.ts:99` |
402
- | `mountAll()` | Monte toutes les routes (idempotent) | `AdminBroker.ts:112` |
403
- | `resolve(routeName)` | Lookup O(1) d'une route montée (utilisé par le pont) | `AdminBroker.ts:104` |
404
- | `routes()` | Introspection des routes montées (source du catalogue) | `AdminBroker.ts:108` |
444
+ | Membre (`IAdminBroker`) | Rôle | Ancre |
445
+ | ------------------------ | -------------------------------------------------------- | ---------------------------------------- |
446
+ | `register(api)` | Enregistre un producteur (throw si namespace pris/monté) | `AdminBroker.ts#AdminBroker.register` |
447
+ | `unregister(ns)` | Retire un producteur (et ses routes si montées) | `AdminBroker.ts#AdminBroker.unregister` |
448
+ | `has(ns)` / `getApi(ns)` | Interrogation du registre | `AdminBroker.ts#AdminBroker.getApi` |
449
+ | `list()` | Producteurs enregistrés (immuable) | `AdminBroker.ts#AdminBroker.list` |
450
+ | `resolvePath(ns, path)` | Chemin absolu d'un endpoint sans le monter | `AdminBroker.ts#AdminBroker.resolvePath` |
451
+ | `mountAll()` | Monte toutes les routes (idempotent) | `AdminBroker.ts#AdminBroker.mountAll` |
452
+ | `resolve(routeName)` | Lookup O(1) d'une route montée (utilisé par le pont) | `AdminBroker.ts#AdminBroker.resolve` |
453
+ | `routes()` | Introspection des routes montées (source du catalogue) | `AdminBroker.ts#AdminBroker.routes` |
405
454
 
406
455
  ## 📡 Observabilité — Studio
407
456
 
408
- - **Catalogue / discovery** : `GET /nodefony/framework/api/admin` (`FrameworkAdminApi.ts:203`) —
457
+ - **Catalogue / discovery** : `GET /nodefony/framework/api/admin` (`FrameworkAdminApi.ts#createFrameworkAdminApi`) —
409
458
  producteurs + descriptors + endpoints. C'est ce que Studio lit pour générer sa navigation admin.
410
- - **Routes** : `GET /nodefony/framework/api/routes` (`FrameworkAdminApi.ts:122`) — l'équivalent web
411
- de `nodefony router:dump` ; une variante paginée serveur `routes/page` (`FrameworkAdminApi.ts:129`).
459
+ - **Routes** : `GET /nodefony/framework/api/routes` (`FrameworkAdminApi.ts#createFrameworkAdminApi`) — l'équivalent web
460
+ de `nodefony router:dump` ; une variante paginée serveur `routes/page` (`FrameworkAdminApi.ts#createFrameworkAdminApi`).
412
461
  - **Playground** (`/nodefony/playground`, **dev only**) : `buildPlaygroundSnapshot()`
413
- (`PlaygroundAdminApi.ts:167`) sérialise controllers + actions + transports + params + gardes — la
462
+ (`PlaygroundAdminApi.ts#buildPlaygroundSnapshot`) sérialise controllers + actions + transports + params + gardes — la
414
463
  page Studio bâtit ses formulaires depuis ces métadonnées, **sans code généré**. Le montage est
415
- conditionné à l'environnement (`index.ts:342`). Le pont admin lui-même est **exclu** du snapshot
416
- (`PlaygroundAdminApi.ts:180`) pour ne pas noyer les controllers applicatifs.
464
+ conditionné à l'environnement (`index.ts#Framework.onKernelBoot`). Le pont admin lui-même est **exclu** du snapshot
465
+ (`PlaygroundAdminApi.ts#buildPlaygroundSnapshot`) pour ne pas noyer les controllers applicatifs.
417
466
  - Les écrans admin de Studio (Routes, Sessions, Firewall, Users…) sont des vues qui consomment ces
418
467
  data planes — voir le [module Studio](../../studio/docs/index.md).
419
468
 
420
469
  ## ⚠️ Pièges (symptôme → cause → correction)
421
470
 
422
- | Symptôme | Cause (dans le code) | Correction |
423
- | ------------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
424
- | `register()` throw « routes figées » | Appel **après** `mountAll()` (`AdminBroker.ts:112`) | Enregistrer au `onKernelBoot`, pas plus tard |
425
- | `register()` throw « namespace déjà enregistré » | Deux producteurs sur le même `adminNamespace` (`AdminBroker.ts:51`) | Namespace unique ; garder `register()` idempotent (`has(ns)` avant) |
426
- | 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 |
427
- | 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`) |
428
- | WS : mutation refusée `400` clé requise | Idempotence : clé obligatoire par socket (`AdminApiController.ts:154`) | Fournir `Idempotency-Key` sur la mutation WS |
429
- | Route admin injoignable / collision Studio | Endpoint mono-segment `/nodefony/<module>` (`IAdminBroker.ts:42`) | Toujours `≥ 3` segments `/nodefony/<ns>/api/<path>` |
430
- | 500 « Admin endpoint not registered » | `adminRoute` absent du registre — incohérence interne (`AdminApiController.ts:109`) | Vérifier que le producteur a bien été enregistré avant `mountAll()` |
471
+ | Symptôme | Cause (dans le code) | Correction |
472
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
473
+ | `register()` throw « routes figées » | Appel **après** `mountAll()` (`AdminBroker.ts#AdminBroker.mountAll`) | Enregistrer au `onKernelBoot`, pas plus tard |
474
+ | `register()` throw « namespace déjà enregistré » | Deux producteurs sur le même `adminNamespace` (`AdminBroker.ts#AdminBroker.register`) | Namespace unique ; garder `register()` idempotent (`has(ns)` avant) |
475
+ | 401 sur toute route `/nodefony/<ns>/api/*` | Zone `nodefony-admin` : pas de session BFF (`config.ts`) | S'authentifier (login BFF) ; pour une sonde publique → `public: true` + zone anonyme |
476
+ | 403 alors qu'on est connecté | Rôle manquant, `isAdminGranted` fail-closed (`adminRbac.ts#isAdminGranted`) | Doter le compte du rôle requis (défaut `ROLE_NODEFONY_ADMIN`) |
477
+ | WS : mutation refusée `400` clé requise | Idempotence : clé obligatoire par socket (`AdminApiController.ts#AdminApiController.idempotencyGate`) | Fournir `Idempotency-Key` sur la mutation WS |
478
+ | Route admin injoignable / collision Studio | Endpoint mono-segment `/nodefony/<module>` (`IAdminBroker.ts#IAdminBroker`) | Toujours `≥ 3` segments `/nodefony/<ns>/api/<path>` |
479
+ | 500 « Admin endpoint not registered » | `adminRoute` absent du registre — incohérence interne (`AdminApiController.ts#AdminApiController.runAdmin`) | Vérifier que le producteur a bien été enregistré avant `mountAll()` |
431
480
 
432
481
  ## 🧪 Tests & couverture
433
482