@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.
- package/README.md +19 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +9 -18
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +39 -8
- package/dist/nodefony/controller/ApiKeyController.js +16 -8
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +4 -11
- package/dist/nodefony/controller/OAuth2Controller.js +3 -4
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +13 -4
- package/dist/nodefony/controller/TokenAuthController.js +3 -4
- package/dist/nodefony/controller/TotpController.js +2 -3
- package/dist/nodefony/controller/WebAuthnController.js +2 -3
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +155 -53
- package/dist/nodefony/service/AdminBroker.js +4 -6
- package/dist/nodefony/service/Eta.js +1 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -6
- package/dist/nodefony/service/router.js +49 -15
- package/dist/nodefony/src/Controller.js +32 -39
- package/dist/nodefony/src/FrameworkAdminApi.js +31 -10
- package/dist/nodefony/src/KernelAdminApi.js +7 -9
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +68 -33
- package/dist/nodefony/src/ResourceController.js +13 -6
- package/dist/nodefony/src/Route.js +2 -3
- package/dist/nodefony/src/SecurityApiController.js +1 -2
- package/dist/nodefony/src/SyslogAdminApi.js +1 -2
- package/dist/nodefony/src/Template.js +1 -2
- package/dist/nodefony/src/configMutation.js +1 -2
- package/dist/nodefony/src/docsReader.js +1 -2
- package/dist/nodefony/src/idempotency.js +1 -2
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +2 -3
- package/dist/nodefony/src/requireAuthentication.js +1 -2
- package/dist/nodefony/src/routeSchema.js +81 -0
- package/dist/nodefony/src/scopeCatalog.js +1 -2
- package/dist/nodefony/src/singletonGuard.js +1 -2
- package/dist/nodefony/src/syslogFilters.js +7 -2
- package/dist/types/index.d.ts +5 -10
- package/dist/types/nodefony/controller/AdminApiController.d.ts +13 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +7 -0
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +133 -32
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/service/router.d.ts +27 -1
- package/dist/types/nodefony/src/Controller.d.ts +23 -17
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +33 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +12 -4
- package/dist/types/nodefony/src/Route.d.ts +3 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/dist/types/nodefony/src/syslogFilters.d.ts +6 -0
- package/docs/admin.md +131 -82
- package/docs/controller.md +109 -110
- package/docs/decorateurs.md +214 -161
- package/docs/idempotence.md +154 -136
- package/docs/routing.md +105 -90
- package/docs/templates.md +55 -55
- package/package.json +8 -11
- 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
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
268
|
-
| 2 | Le framework monte tout | `AdminBroker.mountAll()` (`AdminBroker.ts
|
|
269
|
-
| 3 | Une route par endpoint (nom déterministe) | `Router.createRoute()` (`AdminBroker.ts
|
|
270
|
-
| 4 | Le controller pont estampillé une fois | `Router.setController()` idempotent (`AdminBroker.ts
|
|
271
|
-
| 5 | Dispatch : lookup de la route | `AdminBroker.resolve()` (`AdminApiController.ts
|
|
272
|
-
| 6 | Projection du contexte en requête admin | `buildRequest()` (`AdminApiController.ts
|
|
273
|
-
| 7 | Normalisation du retour | `normalizeAdminResult()` (`executeAdmin.ts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
300
|
-
`^/nodefony/[^/]+/api(/|$)` (`config.ts
|
|
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
|
|
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
|
|
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
|
|
314
|
-
- **Endpoint public** : `public: true` (`IAdminApi.ts
|
|
315
|
-
(`role === ""`, `adminRbac.ts
|
|
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
|
|
318
|
-
publique `nodefony-liveness` (`config.ts
|
|
319
|
-
- **Endpoint qui reçoit un mot de passe** : `credentials: true` (`IAdminApi.ts
|
|
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
|
|
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
|
|
330
|
+
## 🔌 HTTP et WebSocket — la même action
|
|
331
331
|
|
|
332
|
-
Toute action admin déclare **aussi** le transport `WEBSOCKET` (`AdminBroker.ts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
356
|
-
2. **S'enregistrer au `onKernelBoot`** via `IAdminRegistry.register()` (`IAdminApi.ts
|
|
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
|
|
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
|
|
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
|
|
371
|
-
| ----------- |
|
|
372
|
-
| `kernel` | `createKernelAdminApi` (`KernelAdminApi.ts
|
|
373
|
-
| `framework` | `createFrameworkAdminApi` (`FrameworkAdminApi.ts
|
|
374
|
-
| `syslog` | `createSyslogAdminApi` (`SyslogAdminApi.ts
|
|
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
|
|
381
|
-
| `security` | `@nodefony/security` | `registerSecurityAdminApi` (`security/index.ts
|
|
382
|
-
| `user` | `@nodefony/user` | `adminNamespace` (`UserAdminApi.ts
|
|
383
|
-
| `orm` | `@nodefony/orm-core` | `adminNamespace` (`OrmAdminApi.ts
|
|
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
|
|
398
|
-
| `unregister(ns)` | Retire un producteur (et ses routes si montées) | `AdminBroker.ts
|
|
399
|
-
| `has(ns)` / `getApi(ns)` | Interrogation du registre | `AdminBroker.ts
|
|
400
|
-
| `list()` | Producteurs enregistrés (immuable) | `AdminBroker.ts
|
|
401
|
-
| `resolvePath(ns, path)` | Chemin absolu d'un endpoint sans le monter | `AdminBroker.ts
|
|
402
|
-
| `mountAll()` | Monte toutes les routes (idempotent) | `AdminBroker.ts
|
|
403
|
-
| `resolve(routeName)` | Lookup O(1) d'une route montée (utilisé par le pont) | `AdminBroker.ts
|
|
404
|
-
| `routes()` | Introspection des routes montées (source du catalogue) | `AdminBroker.ts
|
|
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
|
|
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
|
|
411
|
-
de `nodefony router:dump` ; une variante paginée serveur `routes/page` (`FrameworkAdminApi.ts
|
|
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
|
|
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
|
|
416
|
-
(`PlaygroundAdminApi.ts
|
|
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)
|
|
423
|
-
| ------------------------------------------------ |
|
|
424
|
-
| `register()` throw « routes figées » | Appel **après** `mountAll()` (`AdminBroker.ts
|
|
425
|
-
| `register()` throw « namespace déjà enregistré » | Deux producteurs sur le même `adminNamespace` (`AdminBroker.ts
|
|
426
|
-
| 401 sur toute route `/nodefony/<ns>/api/*` | Zone `nodefony-admin` : pas de session BFF (`config.ts
|
|
427
|
-
| 403 alors qu'on est connecté | Rôle manquant, `isAdminGranted` fail-closed (`adminRbac.ts
|
|
428
|
-
| WS : mutation refusée `400` clé requise | Idempotence : clé obligatoire par socket (`AdminApiController.ts
|
|
429
|
-
| Route admin injoignable / collision Studio | Endpoint mono-segment `/nodefony/<module>` (`IAdminBroker.ts
|
|
430
|
-
| 500 « Admin endpoint not registered » | `adminRoute` absent du registre — incohérence interne (`AdminApiController.ts
|
|
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
|
|