@nodefony/devkit 10.0.0-alpha.7 → 10.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
2
2
  function __decorateMetadata(k, v) {
3
3
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
4
  }
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
2
2
  import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
3
3
  import { buildCard } from "./nodefony/src/card.js";
4
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
5
- import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
4
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
5
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
6
6
  import DevkitService_default from "./nodefony/service/DevkitService.js";
7
7
  import DevkitController_default from "./nodefony/controllers/DevkitController.js";
8
8
  import McpController_default from "./nodefony/controllers/McpController.js";
@@ -1,5 +1,5 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
1
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
3
  import { Controller, controller, route } from "@nodefony/framework";
4
4
  //#region nodefony/controllers/DevkitController.ts
5
5
  /**
@@ -1,6 +1,6 @@
1
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
2
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
3
- import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
1
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
3
+ import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
4
4
  import { ACCESS_TOKEN_VERIFIER, JsonRpcError, Nodefony, authorizeProtectedResource, checkMcpAccess, collectMcpTools, handleMcpMessage, jsonRpcFailure, mcpCallerRoles, protectedResourceMetadataUrl, readBearerHeader } from "nodefony";
5
5
  import { Body, Controller, Headers, controller, route } from "@nodefony/framework";
6
6
  //#region nodefony/controllers/McpController.ts
@@ -1,7 +1,7 @@
1
1
  import defaults from "../config/config.js";
2
2
  import { buildCard } from "../src/card.js";
3
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
4
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
3
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
4
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
5
5
  import { Module, Nodefony, Service, extend, injectable, mcpDeclaredScopes } from "nodefony";
6
6
  //#region nodefony/service/DevkitService.ts
7
7
  /**
package/docs/index.md CHANGED
@@ -106,9 +106,13 @@ la rend éprouvable sans serveur, et ce qui l'empêche d'inventer quoi que ce so
106
106
  npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
107
107
  ```
108
108
 
109
- Quatre outils, qui sont les commandes que vous connaissez déjà :
109
+ Sept outils. Quatre sont les commandes que vous connaissez déjà :
110
110
  `nodefony_inspect` (ce qui est monté), `nodefony_check` (ce qui manque),
111
111
  `nodefony_symbols` (ce qu'une API signifie), `nodefony_card` (par où commencer).
112
+ `nodefony_docs` sert la documentation des modules. `nodefony_admin_list` et
113
+ `nodefony_admin_call` ouvrent les lectures du data plane d'administration — le
114
+ catalogue, puis l'appel — et exigent un jeton porteur du scope `admin:read` :
115
+ sans lui, ils n'apparaissent pas dans la liste.
112
116
 
113
117
  **Il n'y a pas de process à lancer.** Depuis la révision `2026-07-28` du
114
118
  transport, un serveur MCP est un endpoint `POST` sans session : c'est donc une
@@ -264,8 +268,8 @@ est absent de `tools/list` **et** inappelable en le nommant, et le refus dit
264
268
 
265
269
  > 🔴 **Tant que la porte n'authentifie personne, un outil à scopes ne sortira
266
270
  > jamais.** C'est le comportement voulu — fermé par défaut — mais il faut le
267
- > savoir avant de chercher une panne : `caller` vaut `{ authenticated: false,
268
- scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
271
+ > savoir avant de chercher une panne : `caller` vaut
272
+ > `{ authenticated: false, scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
269
273
  > branché. Le jour où il le sera, ces déclarations prendront effet sans qu'une
270
274
  > ligne d'outil change.
271
275
 
@@ -280,16 +284,20 @@ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
280
284
  service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
281
285
  dans le corps de l'action.
282
286
 
283
- Le paquet livre donc cinq **skills** au format [Agent Skills](https://agentskills.io),
284
- un par tâche où l'invention coûte cher :
285
-
286
- | Skill | Le besoin qu'il couvre |
287
- | ------------------------------- | ----------------------------------------------------------- |
288
- | `nodefony-add-crud` | exposer une ressource REST complète, entité comprise |
289
- | `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur |
290
- | `nodefony-protect-route` | réserver une route à qui est habilité |
291
- | `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse |
292
- | `nodefony-browser` | voir **et mesurer** un écran, sans navigateur sur le poste |
287
+ Le paquet livre donc huit **skills** au format [Agent Skills](https://agentskills.io) :
288
+ un qui porte la conduite commune de toute tâche, et un par geste où l'invention
289
+ coûte cher :
290
+
291
+ | Skill | Le besoin qu'il couvre |
292
+ | ------------------------------- | --------------------------------------------------------------- |
293
+ | `nodefony-dev` | conduire une tâche de bout en bout, avant toute modification |
294
+ | `nodefony-add-crud` | exposer une ressource REST complète, entité comprise |
295
+ | `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur |
296
+ | `nodefony-protect-route` | réserver une route à qui est habilité |
297
+ | `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse |
298
+ | `nodefony-browser` | voir **et mesurer** un écran, sans navigateur sur le poste |
299
+ | `nodefony-migrate-schema` | faire évoluer le schéma d'une base et le porter en production |
300
+ | `nodefony-devops` | exploiter l'application en conteneur, sans réécrire son contrat |
293
301
 
294
302
  Tous portent le préfixe `nodefony-` : leurs pointeurs arrivent dans le dossier où
295
303
  vous écrivez aussi les vôtres, et sans namespace un skill maison du même nom
@@ -330,13 +338,13 @@ absente sans `NODE_ENV`) ; les skills, eux, doivent se mettre à jour par npm.
330
338
 
331
339
  ## Configuration
332
340
 
333
- | Clé | Type | Défaut | Rôle |
334
- | -------------------- | ---------- | -------------------------------------- | ---------------------------------------------- |
335
- | `enabled` | `boolean` | `true` | Interrupteur du module |
336
- | `mcp.enabled` | `boolean` | `true` | Répond-on aux requêtes MCP ? Coupé → `404` |
337
- | `mcp.allowedOrigins` | `string[]` | `[]` | Origines de navigateur admises ; vide = aucune |
338
- | `mcp.allowRemote` | `boolean` | `false` | Accepter un appel d'une adresse non locale |
339
- | `mcp.tools` | `string[]` | `["inspect","check","symbols","card"]` | Allowlist des outils — lecture seule |
341
+ | Clé | Type | Défaut | Rôle |
342
+ | -------------------- | ---------- | -------------------------------------------------- | ---------------------------------------------- |
343
+ | `enabled` | `boolean` | `true` | Interrupteur du module |
344
+ | `mcp.enabled` | `boolean` | `true` | Répond-on aux requêtes MCP ? Coupé → `404` |
345
+ | `mcp.allowedOrigins` | `string[]` | `[]` | Origines de navigateur admises ; vide = aucune |
346
+ | `mcp.allowRemote` | `boolean` | `false` | Accepter un appel d'une adresse non locale |
347
+ | `mcp.tools` | `string[]` | les sept outils intégrés (`BUILTIN_MCP_TOOL_KEYS`) | Allowlist des outils — lecture seule |
340
348
 
341
349
  Les clés, leurs types et leurs défauts viennent du schéma Zod
342
350
  (`nodefony/config/config.ts`) — **source unique** dont dérivent la
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/devkit",
3
- "version": "10.0.0-alpha.7",
3
+ "version": "10.0.0-alpha.9",
4
4
  "type": "module",
5
5
  "description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
6
6
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
@@ -25,17 +25,17 @@
25
25
  "coverage": "vitest run --coverage"
26
26
  },
27
27
  "peerDependencies": {
28
- "@nodefony/framework": "^10.0.0-alpha.7",
29
- "@nodefony/http": "^10.0.0-alpha.7",
30
- "nodefony": "^10.0.0-alpha.7",
28
+ "@nodefony/framework": "^10.0.0-alpha.9",
29
+ "@nodefony/http": "^10.0.0-alpha.9",
30
+ "nodefony": "^10.0.0-alpha.9",
31
31
  "zod": "^4.6.5",
32
32
  "playwright": "^1.50.0",
33
33
  "lighthouse": "^13.0.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@nodefony/framework": "^10.0.0-alpha.7",
37
- "@nodefony/http": "^10.0.0-alpha.7",
38
- "nodefony": "^10.0.0-alpha.7"
36
+ "@nodefony/framework": "^10.0.0-alpha.9",
37
+ "@nodefony/http": "^10.0.0-alpha.9",
38
+ "nodefony": "^10.0.0-alpha.9"
39
39
  },
40
40
  "dependencies": {
41
41
  "axe-core": "4.13.0",
@@ -1,18 +1,18 @@
1
1
  ---
2
2
  name: nodefony-add-crud
3
3
  description: >
4
- Crée une ressource complète dans une application Nodefony — table, schémas de validation,
4
+ Crée une ressource complète dans une application Nodefony — entité, schémas de validation,
5
5
  service CRUD, controller REST+WebSocket et tests — par le générateur `nodefony create entity`,
6
- au lieu de l'écrire à la main. Porte la grammaire de champs (types, relations, index simples et
7
- composites), les réglages pour épouser une table SQL existante, et les trois vérités qu'on
8
- découvre autrement en production : la table naît au démarrage, un champ ajouté n'est rattrapé que
9
- s'il accepte le vide, et la production s'applique par des migrations — dont le cycle complet vit
10
- dans le skill `nodefony-migrate-schema`. À charger AVANT d'écrire une entité, un repository ou un
11
- controller de ressource.
6
+ sur SQL comme sur MongoDB. Porte la grammaire de champs, la
7
+ table des types moteur par moteur, les relations et clés étrangères,
8
+ les réglages pour épouser une table SQL existante, et ce qu'on découvre autrement en
9
+ production : la table naît au démarrage, un champ ajouté n'est rattrapé que s'il accepte le
10
+ vide, et la production s'applique par des migrations (skill `nodefony-migrate-schema`). À
11
+ charger AVANT d'écrire une entité, un repository ou un controller de ressource.
12
12
  Déclencheurs : "ajoute une entité", "crée un CRUD", "nouvelle table", "modèle de données",
13
- "ressource REST", "endpoint CRUD", "je veux stocker des articles/commandes/utilisateurs",
14
- "comment définir un champ", "une relation entre deux entités", "index composite",
15
- "épouser une table existante", "renommer les colonnes en snake_case".
13
+ "ressource REST", "je veux stocker des articles/commandes", "comment définir un champ", "quels types de champ ?", "une relation entre
14
+ deux entités", "clé étrangère", "index composite", "épouser une table existante",
15
+ "entité MongoDB", "schéma Mongoose".
16
16
  ---
17
17
 
18
18
  # add-crud — une ressource complète, générée
@@ -44,29 +44,48 @@ se connecte qu'au démarrage), la pagination bornée **et son tri déclaré**, l
44
44
 
45
45
  ## La grammaire de champs
46
46
 
47
- `nom:type[?|!][:index]` — **non-null par défaut**, `?` rend facultatif, `!` pose une contrainte
48
- d'unicité, `:index` un index simple.
49
-
50
- | Type | Ce que ça produit |
51
- | -------- | ----------------------- |
52
- | `string` | texte court (une ligne) |
53
- | `text` | texte long |
54
- | `int` | entier |
55
- | `float` | décimal |
56
- | `bool` | booléen |
57
- | `json` | document |
58
- | `date` | horodatage |
59
- | `uuid` | identifiant |
60
-
61
- **Une relation** s'écrit `ref:<Entité>` :
47
+ `nom:type[?][=défaut][:index|:unique]` — **non-null par défaut**, `?` rend facultatif, `=valeur`
48
+ fixe un défaut LITTÉRAL, `:unique` pose une contrainte d'unicité, `:index` un index simple.
49
+ 🔴 **`!` est REFUSÉ** : il ne veut pas dire « obligatoire » ici — un champ l'est déjà.
50
+
51
+ **Casse** : l'entité et la cible d'une relation en PascalCase (`Post`, `ref:User`), le champ en
52
+ camelCase (`publishedAt`). Une faute de casse ou un type d'un autre outil (`boolean`, `integer`)
53
+ est refusé avec la forme juste proposée.
54
+
55
+ | Type | Ce que ça produit | SQLite | PostgreSQL | MySQL / MariaDB | MongoDB |
56
+ | -------------- | ----------------------------- | --------------- | ---------------- | --------------- | ---------------------- |
57
+ | `string(n)` | texte court, borné (255) | `text` ¹ | `varchar(n)` | `varchar(n)` | `String` + `maxlength` |
58
+ | `text` | texte long | `text` | `text` | `text` | `String` |
59
+ | `int` | entier | `integer` | `integer` | `int` | `Number` |
60
+ | `float` | nombre à virgule | `real` | `double` | `double` | `Number` |
61
+ | `decimal(p,s)` | décimal EXACT (voyage chaîne) | `numeric` | `numeric(p,s)` | `decimal(p,s)` | `String` |
62
+ | `bool` | booléen | `integer` (0/1) | `boolean` | `boolean` | `Boolean` |
63
+ | `json` | document libre | `text` (JSON) | `jsonb` | `json` | `Mixed` |
64
+ | `date` | horodatage (ms) | `integer` (ms) | `timestamptz(3)` | `datetime(3)` | `Date` |
65
+ | `uuid` | identifiant | `text` | `uuid` | `varchar(36)` | `String` |
66
+ | `char(n)` | longueur EXACTE | `text` ¹ | `char(n)` | `char(n)` | `String`, `n` exact |
67
+ | `enum(a,b)` | valeurs admises ² | `text` | `varchar(255)` | `varchar(255)` | `String` + `enum` |
68
+ | `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
69
+
70
+ ¹ SQLite n'applique aucune longueur : c'est le schéma d'entrée (Zod) qui borne, sur TOUS les
71
+ transports. ² Même colonne partout, sans type SQL nommé (qui exigerait une migration) : le type
72
+ TypeScript et le schéma Zod bornent les valeurs. ³ En SQL, la colonne prend le type de la clé
73
+ visée (`uuid`, entier pour `--id serial`, texte pour `User`) ; en MongoDB, aucune clé étrangère.
74
+
75
+ La table exacte, par moteur, telle que CETTE version l'écrit :
76
+ `npx nodefony create entity --describe-json` (champ `context.columnTypes`).
77
+
78
+ **Une relation** s'écrit `<champ>:ref:<Entité>` — le champ d'abord, toujours :
62
79
 
63
80
  ```bash
64
- npx nodefony create entity Comment body:text ref:Article
81
+ npx nodefony create entity Comment body:text article:ref:Article
65
82
  ```
66
83
 
67
- La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`.
68
- Les clés étrangères ne sont pas émises : une contrainte déclarée dans le `CREATE TABLE`
69
- n'atteindrait jamais une base déjà en place. C'est le domaine des migrations.
84
+ La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`. En SQL,
85
+ la **clé étrangère est émise** (`.references()`), du type de la clé visée, et son effacement se
86
+ déduit de la nullabilité : `article:ref:Article` (obligatoire) → `restrict`, le parent ne peut pas
87
+ partir ; `article:ref:Article?` → `set null`. `cascade` n'est jamais un défaut : il s'écrit à la
88
+ main dans la table générée.
70
89
 
71
90
  **Un index de table** porte plusieurs colonnes, et c'est le seul à le pouvoir :
72
91
 
@@ -77,12 +96,32 @@ npx nodefony create entity Visit siteId:uuid path:string at:date --index "siteId
77
96
  Les deux options sont **répétables** — un couple par index. Sur un schéma réel, la majorité des
78
97
  index utiles sont composites : c'est ainsi qu'une table est réellement interrogée.
79
98
 
99
+ ## Sur une application MongoDB
100
+
101
+ La même commande écrit une entité **document** : schéma Mongoose, service CRUD, controller et
102
+ tests — sans table ni migration (la collection naît à la première écriture). Une ligne, toutes
103
+ les options qui ont un sens ici :
104
+
105
+ ```bash
106
+ npx nodefony create entity Post title:string(120) body:text? views:int=0 status:enum(draft,published)=draft slug:string:unique tags:json? author:ref:User --soft-delete --route /api/posts --dry-run
107
+ ```
108
+
109
+ - La clé est l'`_id` natif, servie en `id` ; `ref:<Entité>` est un `ObjectId` indexé, chargé
110
+ par `?include=`, que le schéma d'entrée exige bien formé (24 caractères hexadécimaux → 422).
111
+ - **MongoDB ne tient aucune clé étrangère, l'ORM tient l'effacement** : supprimer un parent
112
+ encore désigné par une référence obligatoire est refusé (409), une facultative est remise à
113
+ `null` — la politique du SQL. Hors transaction, ce contrôle n'est pas atomique ; et
114
+ l'INSERTION n'est pas gardée (un identifiant inexistant est accepté, `?include=` rend `null`).
115
+ - Refusées en le disant (options SQL) : `--table`, `--column-case`, `--id-name`, `--dialect`,
116
+ `--id`, `--index`, `--unique`. Un index composite s'écrit à la main dans le schéma.
117
+ - `User` ne se régénère pas : il s'étend dans `nodefony/entity/User.ts` (son TSDoc donne le geste).
118
+
80
119
  ## Épouser une table qui existe déjà
81
120
 
82
121
  Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
83
122
 
84
123
  ```bash
85
- npx nodefony create entity Session token:string! --table user_sessions --column-case snake --id-name session_id
124
+ npx nodefony create entity Session token:string:unique --table user_sessions --column-case snake --id-name session_id
86
125
  ```
87
126
 
88
127
  Faire suivre le TypeScript aurait transformé un réglage de nommage en refonte : le service, le
@@ -149,7 +188,7 @@ Ce contrat vaut aussi quand tu écris une liste **à la main** (un endpoint d'ad
149
188
  listing filtré) : le côté serveur déclare ce qu'il sait trier, le point d'entrée le demande, et le
150
189
  refus tombe tout seul.
151
190
 
152
- ## Les trois vérités à savoir avant de livrer
191
+ ## Les trois vérités à savoir avant de livrer (SQL)
153
192
 
154
193
  1. **La table naît au prochain démarrage en développement** (`CREATE TABLE IF NOT EXISTS`).
155
194
  2. **La modifier n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
@@ -164,7 +203,8 @@ Le générateur s'arrête plutôt que de produire un fichier bancal — lis le m
164
203
  geste :
165
204
 
166
205
  - **hors projet** (aucun `nodefony.config.ts` au-dessus) ;
167
- - **`@nodefony/drizzle` absent** de la cible ;
206
+ - **aucun ORM** dans l'application (ni `@nodefony/drizzle`, ni `@nodefony/mongoose`) ;
207
+ - **une option SQL sur une application MongoDB** (liste ci-dessus) ;
168
208
  - **entité déjà déclarée** ;
169
209
  - **nom réservé par un module du framework** (`session`, `access_token`, `audit_event`…) — un
170
210
  homonyme dépossède le module, et l'application ne démarre plus sur un message parlant d'une
@@ -32,12 +32,17 @@ description: >
32
32
  ## Le geste — sur ta machine
33
33
 
34
34
  ```bash
35
- npm run see:setup # dans une application générée — pose `playwright` et `axe-core`
36
- node node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/inspect.mjs /
35
+ npx nodefony see / # dans une application : mesure l'écran
36
+ npx nodefony see / --install # la même, en posant d'abord `playwright` et `axe-core`
37
37
  ```
38
38
 
39
- Hors d'une application générée, le premier pas s'écrit en toutes lettres : `npm i -D playwright
40
- axe-core`, puis `npx playwright install chromium` si aucun navigateur n'est déjà là (§ suivant).
39
+ La commande résout elle-même la sonde et **dit ce qui manque** plutôt que d'échouer sur une trace de
40
+ résolution. Elle n'installe rien sans `--install` : l'outillage d'un navigateur piloté pèse des
41
+ centaines de mégaoctets une fois les navigateurs posés.
42
+
43
+ Hors d'une application Nodefony, le premier pas s'écrit en toutes lettres : `npm i -D playwright
44
+ axe-core`, puis `npx playwright install chromium` si aucun navigateur n'est déjà là (§ suivant), et
45
+ la sonde se lance par son chemin (`node node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/inspect.mjs /`).
41
46
 
42
47
  C'est tout. Les sondes **constatent** où elles s'exécutent : sur ta machine elles visent
43
48
  `https://127.0.0.1:5152` et déposent leurs captures dans `tmp/browser/`. Rien à configurer tant que
@@ -278,9 +283,9 @@ Lighthouse complet, y compris **derrière une authentification** — ce que l'ex
278
283
  ne sait pas faire sur une application protégée.
279
284
 
280
285
  ```bash
281
- npm run audit:setup # pose `lighthouse` — SÉPARÉ de `see:setup` : une vingtaine
282
- # de mégaoctets, que tu ne paies que si tu audites
283
- npm run audit:web -- /tableau-de-bord
286
+ npx nodefony see /tableau-de-bord --audit # audite
287
+ npx nodefony see /tableau-de-bord --audit --install # pose `lighthouse` (une vingtaine de
288
+ # mégaoctets, payés seulement si tu audites)
284
289
  ```
285
290
 
286
291
  Hors d'une application générée, ou pour passer des identifiants :
@@ -14,7 +14,7 @@ description: >
14
14
  est-il fini", "je ne trouve rien sur ce sujet", "ce n'est pas documenté", "je touche au
15
15
  frontend", "mon composant charge des données", "mon écran n'affiche rien".
16
16
  metadata:
17
- version: 1.1.0
17
+ version: 1.2.0
18
18
  ---
19
19
 
20
20
  # nodefony-dev — développer dans cette application sans rien inventer
@@ -112,17 +112,160 @@ dans la doc chargée) et `nodefony_symbols` (rend la SIGNATURE réelle d'un symb
112
112
  ne porte pas). Elles supposent un serveur démarré et la porte câblée (`npx nodefony ai:mcp`) ;
113
113
  `docs.mjs`, lui, répond toujours.
114
114
 
115
- ## 3. Conduire une tâche — la séquence, et ses points d'arrêt
116
-
117
- 1. **Demande à l'application, ne déduis pas du code.** `npx nodefony inspect routes`,
118
- `inspect services`, `inspect config` rendent l'état RÉEL — routes montées, services résolus,
119
- valeur effective **et sa provenance**. Une route lue dans un fichier peut n'être montée nulle
120
- part ; l'inverse aussi.
121
- **Un argument RESTREINT la réponse** — `inspect routes auth` ne rend que les routes dont le
122
- chemin, le nom, le contrôleur, l'action, le module ou les méthodes portent `auth` ;
123
- `inspect schema http` fait de même sur les réglages d'un module. Ne tronque JAMAIS une sortie
124
- d'inspection (`| head`) pour la faire tenir : une application en sert facilement plusieurs
125
- centaines, et ce qu'on cherche est presque toujours dans la partie coupée — c'est ainsi qu'on
115
+ ## 3. Interroger l'application — elle répond mieux que ses sources
116
+
117
+ **Perdu ? La carte de visite dit qui répond, ce qui est chargé, où lire et quoi lancer :**
118
+
119
+ ```bash
120
+ npx nodefony card # -j pour du JSON
121
+ ```
122
+
123
+ Elle répond **toujours** — application pas encore construite, terminal sans aucune variable
124
+ d'environnement : elle ne lit que des fichiers. Elle le DIT alors (« modules installés », pas
125
+ « chargés ») et renvoie à `inspect modules` pour ce qui est vraiment monté.
126
+
127
+ ```bash
128
+ npx nodefony inspect routes --json # routes réelles (chemin MONTÉ, méthodes, controller)
129
+ npx nodefony inspect services --json # services enregistrés, et le module qui les porte
130
+ npx nodefony inspect config --json # config EFFECTIVE, et d'où vient chaque valeur
131
+ npx nodefony inspect schema http # ce qu'on a le DROIT d'écrire : clés, type, défaut, description
132
+ npx nodefony inspect modules --json # modules CHARGÉS — pas ceux que le manifeste déclare
133
+ npx nodefony inspect entities --json # entités déclarées à l'ORM
134
+ npx nodefony inspect stores --json # où sont RÉELLEMENT écrites les données (sessions, cache…)
135
+ npx nodefony inspect graph --json # graphe des entités et de leurs relations
136
+ ```
137
+
138
+ Ces commandes bootent l'application **sans ouvrir un seul port** et rendent exactement ce que sert
139
+ la console d'administration — même code, deux portes.
140
+
141
+ > **Ce que rend `inspect` ENGLOBE tes sources et les dépasse.** Les modules installés montent leurs
142
+ > propres routes : une application qui en définit une poignée en expose couramment plus d'une
143
+ > centaine. Un écart d'un ordre de grandeur entre tes fichiers et `inspect routes --json | jq
144
+ 'length'` n'est PAS une anomalie de l'outil — c'est la différence entre ce que TU as écrit et ce
145
+ > que l'application MONTE.
146
+
147
+ **« Que fait cette classe, où est-elle définie, qu'étend-elle ? » — une commande, pas une fouille :**
148
+
149
+ ```bash
150
+ npx nodefony symbols AbstractCrudService # définition, TSDoc, parenté — en O(1)
151
+ npx nodefony symbols --module @nodefony/http # toute la surface exportée d'un paquet
152
+ ```
153
+
154
+ Le graphe symbolique de tout le framework est livré avec le paquet `nodefony` : la réponse ne
155
+ dépend ni d'un serveur, ni d'un build, ni de ta connexion. Va y chercher un symbole AVANT d'ouvrir
156
+ un `.d.ts` — et avant, surtout, d'inventer une signature.
157
+
158
+ **Si la commande te résiste, répare l'APPEL — ne te rabats pas sur les sources.** C'est le réflexe
159
+ le plus cher, parce qu'il produit une réponse d'allure normale : un shell qui manque un outil
160
+ (`timeout` n'existe pas sur macOS), un `jq` mal formé, et l'on se replie sur ce qu'on sait lire.
161
+ Les fichiers répondront toujours quelque chose — mais pas à la question posée. Relance sans le
162
+ tube, puis remets ton filtre.
163
+
164
+ **Tu préfères des OUTILS à des commandes ?** Cette application les expose en Model Context Protocol
165
+ (`npx nodefony ai:mcp` écrit `.mcp.json`) — mêmes réponses, et tes propres modules peuvent publier
166
+ les leurs. La porte est une ROUTE : elle n'existe que serveur démarré, et un client qui la trouve
167
+ éteinte la marque en échec pour toute sa session — démarre l'application D'ABORD, ta session
168
+ ENSUITE. Tout le reste (déclarer un outil métier, le réserver à des scopes, l'autorisation OAuth) :
169
+ `node_modules/@nodefony/devkit/docs/index.md`.
170
+
171
+ ### L'environnement : ne devine JAMAIS, demande
172
+
173
+ ```bash
174
+ npx nodefony env # cascade des .env, valeur EFFECTIVE de chaque variable, sa PROVENANCE
175
+ ```
176
+
177
+ **Encadre toute modification de configuration par cette commande** : une fois AVANT, pour savoir ce
178
+ qui s'applique et d'où ça vient ; une fois APRÈS, pour prouver que ta valeur est celle qui gagne.
179
+ Lire les `.env` toi-même donne des contenus ; la précédence est un mécanisme — tu ne peux que la
180
+ supposer, et une supposition fausse ne se voit qu'en production. La commande ne boote rien, donc
181
+ elle répond aussi quand l'application ne démarre plus.
182
+
183
+ **Précédence, du plus FORT au plus faible** — le premier qui pose une valeur gagne, les suivants
184
+ sont ignorés en silence :
185
+
186
+ ```
187
+ process.env > .env.<déploiement>.local > .env.<mode>.local > .env.local
188
+ > .env.<déploiement> > .env.<mode> > .env
189
+ ```
190
+
191
+ `<mode>` = `NODE_ENV` · `<déploiement>` = `APP_ENV` (plus spécifique, donc plus fort). Les `*.local`
192
+ ne sont jamais committés : les secrets y vont, et nulle part ailleurs.
193
+
194
+ | Forme | Ce que c'est | Où c'est déclaré |
195
+ | ------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ |
196
+ | `NF_PORT=5151` | variable de l'APP, typée et validée | `env.ts` (`defineEnv`) — non déclarée = **sans effet** |
197
+ | `NF__HTTP__SERVERS__HTTPS__PORT=8443` | surcharge DIRECTE d'une clé de config d'un module | rien à déclarer — double `__` = séparateur |
198
+ | `NF_TOTP_KEY_FILE=/run/secrets/x` | la même variable, lue depuis un fichier (secret K8s) | idem `NF_TOTP_KEY` |
199
+
200
+ Une variable `NF_` mal orthographiée n'échoue pas : elle est **ignorée**, et le défaut s'applique en
201
+ silence. `npx nodefony env` est le seul endroit qui la montre, avec la correction probable.
202
+
203
+ ## 4. Les commandes — demande la liste, ne la devine pas
204
+
205
+ ```bash
206
+ npx nodefony --help # TOUTES les commandes, celles des modules installés comprises
207
+ npx nodefony <commande> --help # les options exactes de l'une d'elles
208
+ ```
209
+
210
+ La liste **dépend des modules installés** : elle n'est pas la même d'une application à l'autre, et
211
+ elle s'allonge dès que tu en ajoutes un. C'est pour ça qu'elle se demande au lieu de se retenir.
212
+
213
+ **Toujours `npx`, jamais `nodefony` nu.** Le binaire vit dans les `node_modules` de CETTE
214
+ application : la forme nue rend un code 127 tant que rien n'est installé globalement. Une
215
+ installation globale existe (elle sert à créer une application HORS projet) et, dans un projet, elle
216
+ passe la main au binaire local — mais elle peut être plus ANCIENNE. `npx` prend la version que cette
217
+ application a choisie, sans dépendre de ce qui traîne sur la machine.
218
+
219
+ Celles qu'on n'invente pas, faute de savoir qu'elles existent :
220
+
221
+ - Mettre l'application derrière **nginx ou haproxy** — `npx nodefony proxy:generate <nginx|haproxy>`
222
+ - **Servir les fichiers statiques depuis un CDN** — `npx nodefony assets:publish [--clean]`
223
+ - **Certificat TLS de développement** — `npx nodefony http:certificates`
224
+ - **Dépendances en retard**, agrégées et non le brut de npm — `npx nodefony outdated`
225
+ - **Cohérence du projet** (classe non câblée, route qui répondra 404) — `npx nodefony doctor`
226
+ - **Plusieurs processus, un cœur chacun** — `npx nodefony production -w <n|auto>` · `NF_WORKERS`
227
+ - **Complétion au TAB** — `source <(nodefony completion zsh)`
228
+
229
+ Ce tableau ne remplace pas `--help` : lui seul connaît les modules de CETTE application, et il fait
230
+ foi le jour où les deux divergent.
231
+
232
+ ## 5. Piloter le serveur — et l'ARRÊTER
233
+
234
+ ```bash
235
+ npm run dev # développement : rechargement auto, Ctrl+C pour arrêter
236
+ npx nodefony development --no-watch # développement SANS rechargement : un process, stable
237
+ npx nodefony status # que tourne-t-il ? ports, PID — ne boote rien
238
+ npx nodefony stop # arrêt PROPRE de tout runtime de cette application
239
+ npx nodefony stop <nom|chemin> # arrêter un AUTRE projet, sans changer de dossier
240
+ npx nodefony production --detach --wait # boot réel en arrière-plan ; rend la main ports OUVERTS
241
+ ```
242
+
243
+ **Arrête ce que tu démarres.** Un serveur laissé derrière garde les ports : le run suivant échoue
244
+ sur une erreur qui ne parle jamais de lui (`EADDRINUSE`) — ou pire, un test interroge l'ANCIENNE
245
+ version du code. Et **jamais `… &`** : le processus reçoit SIGHUP et meurt ; tuer le PID du port ne
246
+ tue pas le superviseur, qui respawne.
247
+
248
+ **Ces commandes ne voient QUE cette application.** Plusieurs projets Nodefony peuvent tourner sur la
249
+ même machine ; `status` ne compte jamais les processus du voisin comme les tiens, il les NOMME dans
250
+ une table à part — et ce nom est ce que `stop` accepte. Donc « aucune instance » veut dire « aucune
251
+ À MOI », pas « rien ne tourne » ; et une cible que `stop` ne peut pas désigner sans ambiguïté est
252
+ REFUSÉE, avec un code de sortie non nul et rien d'arrêté — **lis ce code**, un refus ressemble
253
+ sinon à un succès.
254
+
255
+ **Pour faire tourner une suite contre un serveur, prends `--no-watch`.** Le mode développement
256
+ relance le serveur dès qu'un fichier bouge : pendant un run, le redémarrage coupe les connexions
257
+ sous les tests, et le rouge qui en sort accuse le code alors que le fautif est le décor.
258
+
259
+ **N'invente pas d'attente** : `--wait` ne rend la main qu'une fois les ports en écoute — un `sleep`
260
+ arbitraire est soit trop court (test rouge sans raison), soit du temps perdu à chaque exécution.
261
+
262
+ ## 6. Conduire une tâche — la séquence, et ses points d'arrêt
263
+
264
+ 1. **Demande à l'application, ne déduis pas du code** (§3). Une route lue dans un fichier peut
265
+ n'être montée nulle part ; l'inverse aussi. **Un argument RESTREINT la réponse** — `inspect
266
+ routes auth` ne rend que les routes dont le chemin, le nom, le contrôleur, l'action, le module
267
+ ou les méthodes portent `auth`. Ne tronque JAMAIS une sortie d'inspection (`| head`) pour la
268
+ faire tenir : ce qu'on cherche est presque toujours dans la partie coupée — c'est ainsi qu'on
126
269
  conclut « le framework ne fournit pas ça » et qu'on le réécrit à la main.
127
270
  2. **Cherche la référence** (§2) avant de choisir une façade. Le framework en a presque toujours
128
271
  une, et la contourner compile — c'est tout le piège.
@@ -137,7 +280,20 @@ ne porte pas). Elles supposent un serveur démarré et la porte câblée (`npx n
137
280
  **Le point d'arrêt qu'on rate** : `npm test` seul ne prouve pas que ça compile — vitest
138
281
  n'inspecte aucun type. Une application peut être verte et ne pas compiler.
139
282
 
140
- ## 4. Les pièges du serveur
283
+ ### Le poids du modèle est un CHOIX, et il est mesuré
284
+
285
+ Si ton outil sait déléguer à des sous-agents : une tâche couverte par un **générateur** ne demande
286
+ pas un gros modèle — c'est le générateur qui porte le savoir. Mesuré sur ce framework, « ajoute une
287
+ ressource REST » rend le MÊME résultat en modèle léger et en modèle fort (mêmes contrôles verts,
288
+ écart d'étapes dans le bruit) pour **~3× moins cher**. À l'inverse, le socle SANS générateur (flux,
289
+ session, cycle de vie) fait échouer le modèle léger environ une fois sur deux.
290
+
291
+ Donc : **léger** pour appeler un générateur, inventorier, lire, vérifier un fait, appliquer un
292
+ patron ; **fort** pour écrire du socle sans générateur et pour arbitrer une architecture. Le test
293
+ qui tranche en une seconde : _la tâche a-t-elle une bonne réponse vérifiable ?_ Aucun nom de modèle
294
+ ici — ils changent tous les trimestres ; raisonne en poids.
295
+
296
+ ## 7. Les pièges du serveur
141
297
 
142
298
  Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers sont les plus fréquents.
143
299
 
@@ -154,6 +310,33 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
154
310
  indolore sur les quelques lignes du poste de développement, fatal sur les dizaines de milliers
155
311
  de la production. Le service d'une entité hérite `findPage({ limit: 25 })`.
156
312
 
313
+ - **Des dizaines de tests d'intégration rouges d'un coup** (`ECONNREFUSED`) : ils FRAPPENT un
314
+ serveur, ils ne le lancent pas — il est éteint. `npx nodefony status` d'abord ; en e2e, laisse
315
+ la commande gérer le cycle.
316
+ - **Ta route NEUVE répond 404 et le `dist/` est à jour** : elle n'est pas montée où tu crois. Le
317
+ chemin réel est le PRÉFIXE de son controller suivi du `path` de la route — une action
318
+ `path: "/widget"` dans un controller `@controller("/api")` répond sur `/api/widget`.
319
+ `npx nodefony inspect routes --json` donne le chemin MONTÉ.
320
+ - **TOUT répond 404, même les routes du gabarit** : un AUTRE serveur tient les ports — ou LE TIEN
321
+ a glissé, le port voulu étant pris. `npx nodefony status` montre les ports RÉELS, pas ceux que
322
+ tu as configurés, et NOMME le projet voisin.
323
+ - 🔴 **`nodefony <commande>` échoue là où `npm run dev` réussit, sur la MÊME application** : DEUX
324
+ paquets `nodefony` tournent dans le processus — un binaire lié ailleurs (installation globale,
325
+ `npm link`) exécute un noyau pendant que l'application importe le sien. Chaque copie a ses
326
+ propres classes, son contexte de requête et ses registres d'injection : un module construit à la
327
+ frontière perd son container SANS la moindre erreur — il figure dans la liste des modules et ne
328
+ fait rien. ⚠️ Les deux copies peuvent être la MÊME version ; deux fichiers distincts suffisent,
329
+ il n'y a aucune incohérence à repérer. `npm ls nodefony` liste les copies (« deduped » partout =
330
+ une seule) ; `NF_CLI_DEBUG=1 nodefony --version` dit quel CLI s'exécute.
331
+ - **`localhost` et `127.0.0.1` te jouent des tours** : ce sont deux ORIGINES distinctes — cookies,
332
+ cache et passkeys ne les partagent pas. Une seule origine en développement, partout, URL ouverte
333
+ comme callbacks.
334
+
335
+ - **Les routes authentifiées plafonnent quand le reste tient la charge** : le stockage de session
336
+ par défaut est SYNCHRONE — chaque reprise bloque la boucle d'événements. Compare une route
337
+ anonyme et une route authentifiée AVANT d'accuser TLS ou le pare-feu ; passe le stockage sur
338
+ redis pour la charge.
339
+
157
340
  ### Ce qui tue le processus, pas la requête
158
341
 
159
342
  - 🔴 **Une promesse lancée sans `await` porte TOUJOURS un `.catch()`.** Audit, courriel,
@@ -185,7 +368,34 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
185
368
  - **L'application ne démarre plus et le superviseur avale la sortie** :
186
369
  `NF_DEV_CHILD=1 npx nodefony development` lance l'enfant seul et montre le crash brut.
187
370
 
188
- ## 5. Les pièges du front
371
+ ## 8. Données et fichiers — les façades qu'on ne recompose pas
372
+
373
+ - **Un adaptateur de données ne remplace pas l'autre : ils se COMPLÈTENT.** Chacun déclare les
374
+ _stores_ qu'il sait tenir (`nodefony.stores` de son `package.json`) — `drizzle` les huit,
375
+ `mongoose` cinq (ni `totp`, ni `audit`, ni `idempotency`), `redis` quatre. Ce n'est pas un retard
376
+ de développement mais un CHOIX : un journal d'audit n'a rien à faire dans un moteur documentaire.
377
+ Ne promets jamais une parité qui n'existe pas — `npx nodefony inspect stores` dit où atterrit
378
+ chaque donnée.
379
+ - **Les violations de contrainte sont DÉJÀ traduites en HTTP — ne les rattrape pas.** Un doublon
380
+ sur une colonne unique ressort en **409**, une donnée qui viole le schéma Zod en **422**, chacun
381
+ avec son corps JSON : le rendu d'erreur lit le code du pilote (`23505`, `ER_DUP_ENTRY`,
382
+ `SQLITE_CONSTRAINT_UNIQUE`, `11000`) et le mappe, quel que soit le moteur. N'écris donc JAMAIS un
383
+ `throw … 409` pour un identifiant déjà pris. Le vérifier toi-même d'abord (« existe-t-il ? » puis
384
+ insertion) est plus lent ET **faux sous concurrence** : deux requêtes simultanées passent toutes
385
+ les deux le test avant que l'une n'écrive. La contrainte de la base est le seul arbitre exact.
386
+ - **Un fichier ne se sert pas à la main.** Trois façades, choisies sur l'usage :
387
+ `this.renderMediaStream(file)` implémente les **requêtes par plage** (`Range` → 206 +
388
+ `Content-Range`, 416 hors plage) — ce qu'exige un lecteur vidéo ou audio pour se déplacer ;
389
+ `this.streamFile(file)` envoie le fichier ENTIER en flux ; `this.renderFileDownload(file)` force
390
+ le téléchargement. Recomposer ça avec `createReadStream` et `response.write` compile, passe les
391
+ tests — et rend une réponse **incohérente** : un statut posé à la main n'atteint jamais la socket
392
+ (le pipeline écrit statut et en-têtes à SON tour), donc le client reçoit **200 avec un corps
393
+ partiel** et croit tenir le fichier complet. Mesuré au banc, pas supposé.
394
+ - **Un pod dont la base est en retard répond 503 sur `/readyz`** (jamais sur `/livez`) et reste
395
+ hors du répartiteur : c'est voulu, ce n'est pas une panne. `npx nodefony orm:migrate:status` dit
396
+ qui est en retard ; applique les migrations, les pods se mettent en service SEULS.
397
+
398
+ ## 9. Les pièges du front
189
399
 
190
400
  Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les quatre moteurs**
191
401
  (React, Vue, Angular, Svelte) : le framework ne t'en impose aucun.
@@ -233,6 +443,20 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
233
443
  seules, `role="img"` + `aria-label` sur un graphe SVG, jamais une information portée par la
234
444
  **couleur seule**, `rel="noopener noreferrer"` sur les liens externes.
235
445
 
446
+ - **Une route d'API répond du HTML** : un repli SPA générique avale les routes voisines — le
447
+ premier motif qui correspond gagne. Repli en préfixe LITTÉRAL ; `npx nodefony inspect routes
448
+ --json` montre l'ordre réel.
449
+ - **Des utilisateurs « déconnectés au hasard »** : le traitement global « 401 = session expirée »
450
+ frappe aussi les sondes d'authentification, où 401 est NORMAL — et détruit une session valide.
451
+ Exempte les sondes du traitement global.
452
+ - **Ta page répond 200 et son script ne s'exécute pas** : la politique de contenu exige un `nonce`
453
+ sur les scripts, et le navigateur refuse un `<script>` en ligne qui n'en porte pas (« Refused to
454
+ execute inline script »). Un `curl` ne le voit JAMAIS, il ne lit que le corps. Signe le script
455
+ (valeur `this.context?.cspNonce`) ou sors-le dans un fichier servi ; ne desserre PAS la politique.
456
+ - **En production, la modification front n'apparaît jamais** : hors développement il n'y a PAS de
457
+ rechargement à chaud, et le manifeste est lu AU BOOT. `npm run build`, puis **redémarre le
458
+ serveur**, puis rechargement forcé.
459
+
236
460
  ### Le piège qui fait croire à un bug de code
237
461
 
238
462
  - **Vite affirme qu'un export n'existe pas** (`does not provide an export named …`) alors qu'il
@@ -240,7 +464,7 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
240
464
  ajoutée, ou un `git pull`. C'est son cache : `rm -rf node_modules/.vite`, puis relance. Ne
241
465
  purge pas sans raison, ça coûte 5 à 20 s de ré-optimisation.
242
466
 
243
- ## 6. Avant de dire « fait »
467
+ ## 10. Avant de dire « fait »
244
468
 
245
469
  ```bash
246
470
  npm run verify # types + style + tests + câblage
@@ -94,6 +94,27 @@ reprendre après une coupure sans lire d'état préalable.
94
94
  > dépose un fichier vide et son entrée de journal. Le gabarit déposé explique comment séparer les
95
95
  > instructions ; suis-le à la lettre.
96
96
 
97
+ **En développement, les deux premiers gestes se disent en un seul** :
98
+
99
+ ```bash
100
+ npx nodefony orm:generate --name ajout_slug --apply
101
+ ```
102
+
103
+ `--apply` ENCHAÎNE `orm:migrate` — il ne rejoue pas l'application pour son compte, donc toutes ses
104
+ gardes valent (refus du destructif sans `--allow-destructive`, verrou, historique). En `--json`, un
105
+ seul document sort : celui de l'application, qui porte le verdict. Le drapeau **n'existe qu'en
106
+ développement** et rien ne le déverrouille ailleurs : hors développement, la migration a été écrite,
107
+ relue et versionnée en amont, et c'est `orm:migrate` seul qui l'applique.
108
+
109
+ > **Le démarrage bascule tout seul, et il faut le savoir avant d'être surpris.** Tant que
110
+ > l'application ne versionne AUCUNE migration, le démarrage en développement dérive les tables
111
+ > depuis le code (`ddl: "auto"`) : on n'a rien à taper. Dès la première migration écrite, il
112
+ > applique les migrations (`ddl: "migrate"`) et cesse de dériver. C'est ce qui empêche deux
113
+ > fabricants du même schéma de se contredire — le DDL dérivé pose une unicité collée à la colonne,
114
+ > le générateur de migrations raisonne sur un index nommé, et la migration qui tente de retirer le
115
+ > second échoue sur un objet qui n'a jamais existé. Un `ddl` écrit dans la configuration gagne
116
+ > toujours ; sous `NODE_ENV=test`, rien ne change.
117
+
97
118
  ### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
98
119
 
99
120
  Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
@@ -286,18 +307,19 @@ nodefony orm:migrate:status
286
307
  Un refus n'est pas une panne : c'est le produit qui s'arrête devant une décision qui t'appartient.
287
308
  Le `code` est stable — **lis-le, il désigne le geste**.
288
309
 
289
- | Code | Ce qui s'est passé | Le geste |
290
- | ---------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
291
- | `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique | `orm:migrate:baseline` (l'adopter) |
292
- | `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables | `orm:migrate:baseline --from-database`, PUIS regénérer |
293
- | `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent | `orm:migrate:baseline` sans option |
294
- | `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | LIRE l'erreur, puis `orm:migrate:repair` |
295
- | `NF_MIGRATE_HASH_MISMATCH` | un fichier déjà appliqué a été modifié | RÉTABLIR le fichier (1er geste) ; `--update-hashes` seulement si la modification était sans effet |
296
- | `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière posée | renommer la nouvelle après la dernière appliquée |
297
- | `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
298
- | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
299
- | `NF_MIGRATE_LOCK_TIMEOUT` | un autre travail de migration tient le verrou | ATTENDRE puis rejouer — ce n'est pas une panne, et le verbe est idempotent |
300
- | `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
310
+ | Code | Ce qui s'est passé | Le geste |
311
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
312
+ | `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique | `orm:migrate:baseline` (l'adopter) |
313
+ | `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables | `orm:migrate:baseline --from-database`, PUIS regénérer |
314
+ | `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent | `orm:migrate:baseline` sans option |
315
+ | `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
316
+ | `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | LIRE l'erreur, puis `orm:migrate:repair` |
317
+ | `NF_MIGRATE_HASH_MISMATCH` | un fichier déjà appliqué a été modifié | RÉTABLIR le fichier (1er geste) ; `--update-hashes` seulement si la modification était sans effet |
318
+ | `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière posée | renommer la nouvelle après la dernière appliquée |
319
+ | `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
320
+ | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
321
+ | `NF_MIGRATE_LOCK_TIMEOUT` | un autre travail de migration tient le verrou | ATTENDRE puis rejouer — ce n'est pas une panne, et le verbe est idempotent |
322
+ | `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
301
323
 
302
324
  Les codes exhaustifs, avec un exemple de charge utile pour chacun :
303
325
  [`references/verdicts.md`](references/verdicts.md).
@@ -113,24 +113,25 @@ n'a jamais les deux — c'est le discriminant à tester.
113
113
 
114
114
  ### Refus d'usage — la demande elle-même
115
115
 
116
- | Code | Ce qui s'est passé | Le geste |
117
- | ------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
118
- | `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
119
- | `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
120
- | `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
121
- | `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
122
- | `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
123
- | `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
124
- | `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
125
- | `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
126
- | `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
127
- | `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
128
- | `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
129
- | `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
130
- | `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
131
- | `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
132
- | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
133
- | `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
116
+ | Code | Ce qui s'est passé | Le geste |
117
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
118
+ | `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
119
+ | `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
120
+ | `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
121
+ | `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
122
+ | `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
123
+ | `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
124
+ | `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
125
+ | `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
126
+ | `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
127
+ | `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
128
+ | `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
129
+ | `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
130
+ | `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
131
+ | `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
132
+ | `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
133
+ | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
134
+ | `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
134
135
 
135
136
  ## Un contrat qui ne bougera pas
136
137
 
@@ -10,6 +10,7 @@ description: >
10
10
  Déclencheurs : "protège cette route", "réserver aux administrateurs", "@IsGranted", "firewall",
11
11
  "zone protégée", "403", "401", "un rôle qui en implique un autre", "roleHierarchy",
12
12
  "un partenaire doit pouvoir poster", "erreur CSRF", "origine refusée", "@CsrfExempt",
13
+ "@CsrfProtect", "mon POST passe sans jeton", "faut-il protéger une écriture",
13
14
  "API pour un programme", "clé d'API", "désactiver la sécurité pour tester".
14
15
  ---
15
16
 
@@ -42,6 +43,36 @@ Les deux se combinent : la zone décide **qui entre**, la garde décide **qui fa
42
43
  async list() { … }
43
44
  ```
44
45
 
46
+ ### Une zone peut exiger un rôle PAR DÉFAUT
47
+
48
+ Une zone qui ne liste que des authentificateurs exige une **identité**, rien de plus : toute route
49
+ qu'elle couvre est ouverte à **n'importe quel compte connecté**. Pour un espace d'administration,
50
+ c'est un défaut ouvert — l'oubli d'une garde s'y solde par une fuite.
51
+
52
+ ```ts
53
+ areas: {
54
+ backoffice: {
55
+ pattern: "^/back-office",
56
+ authenticators: ["session"],
57
+ roles: ["ROLE_ADMIN"], // ← exigé par défaut dans toute la zone
58
+ },
59
+ }
60
+ ```
61
+
62
+ Les rôles sont en **OU** — un seul suffit — et la hiérarchie s'applique. Un refus est un **403**
63
+ (connecté, mais pas autorisé), jamais un 401. Omettre la clé ne change rien au comportement
64
+ existant.
65
+
66
+ C'est un **défaut**, pas une exclusivité : une route qui porte sa propre garde (`@IsGranted`)
67
+ décide seule, ce qui laisse exister une page réservée à un autre rôle dans une zone par ailleurs
68
+ fermée. Écris donc le rôle **une fois sur la zone**, et ne le répète sur une route que pour en
69
+ exiger un autre.
70
+
71
+ 🔴 **`@RequireScope` seul ne remplace PAS un rôle.** Un scope dit ce qu'une **clé déléguée** peut
72
+ faire en ton nom ; sur une session humaine il n'a aucune prise. Une route gardée par un scope seul
73
+ n'a donc rien décidé de ton identité — et le rôle de sa zone continue de s'appliquer, ce qui est
74
+ voulu. Les deux axes se cumulent, ils ne se substituent pas.
75
+
45
76
  🔴 **Le `pattern` d'une zone est un PRÉFIXE, jamais la liste des routes du jour.**
46
77
 
47
78
  ```ts
@@ -54,6 +85,19 @@ pattern: "^/api/account/(profile|invoices)"; // ❌ les routes d'aujourd'hui
54
85
  elle a l'air de couvrir l'espace, et l'introspection montre bien une route protégée à côté. Quand
55
86
  des routes partagent un préfixe, ne les protège pas une par une.
56
87
 
88
+ ## 🔴 La PROVENANCE n'est pas une PREUVE D'INTENTION — une mutation exige `@CsrfProtect`
89
+
90
+ Le raisonnement qui vient, et qui est faux : « le pare-feu vérifie déjà `Sec-Fetch-Site`, donc une
91
+ écriture est protégée ». Ces en-têtes sont posés par un NAVIGATEUR ; un programme qui parle en HTTP
92
+ n'en envoie aucun, et la défense de provenance le laisse alors passer — c'est son rôle, elle
93
+ distingue les sites, pas les intentions. Résultat mesuré : un `POST /api/cart/items` sans jeton rend
94
+ `201`, et l'application croit avoir une défense.
95
+
96
+ Toute action qui ÉCRIT porte donc `@CsrfProtect` explicitement. Le jeton ne se demande à AUCUN
97
+ endpoint : une requête sûre (`GET`) vers la route protégée sème le cookie lisible `csrf-token`, et
98
+ la mutation le rejoue dans l'en-tête `x-csrf-token` — c'est le double-submit, sinon `403`. La
99
+ provenance et le jeton se **cumulent** ; l'une ne remplace jamais l'autre.
100
+
57
101
  ## 🔴 Ce qu'il ne faut jamais écrire
58
102
 
59
103
  ```ts
@@ -92,10 +136,14 @@ travail. **Le geste juste est de DÉCLARER l'origine**, jamais de retirer la dé
92
136
 
93
137
  ```ts
94
138
  csrf: {
139
+ secret: ctx.env.NF_CSRF_SECRET, // la clé que la config pose DÉJÀ — ne la perds pas
95
140
  trustedOrigins: ["https://partenaire.example"],
96
141
  }
97
142
  ```
98
143
 
144
+ ⚠️ Ce bloc est fait pour être recopié dans `nodefony/config/security.ts`. Recopié **sans** son
145
+ `secret`, il coupe le jeton anti-falsification — en silence, tests verts.
146
+
99
147
  🔴 `@CsrfExempt`, `csrf.enabled: false`, ou couper le contrôle de provenance **résolvent le
100
148
  symptôme et ouvrent l'application** : n'importe quel site peut alors faire poster le navigateur
101
149
  d'une personne connectée, à son insu et avec ses droits.
@@ -179,9 +227,16 @@ marche, les tests passent, et le diff ne contient aucune faute visible — il co
179
227
  | `@BypassFirewall`, `@Anonymous` | la route sort de sa zone |
180
228
  | `anonymous` ajouté aux authentificateurs d'une zone | toute la zone devient publique |
181
229
  | `rateLimit: { enabled: false }` | le bourrage de mots de passe redevient gratuit |
230
+ | `areaRoleExempt: true` sur une route | elle perd le rôle par défaut de sa zone |
182
231
 
183
232
  Relever un **seuil** est un réglage légitime. L'**éteindre** ne l'est pas.
184
233
 
234
+ Le dernier de la liste est le plus trompeur : son nom dit bien ce qu'il fait — il **retire** le rôle
235
+ exigé par la zone —, mais il n'installe rien à la place. Posé sur une route qui ne décide pas
236
+ elle-même de son autorisation, il la rend accessible à tout compte connecté, sans erreur ni trace.
237
+ Il est **réservé aux mécanismes internes du framework** ; ce que tu veux, quand une route de ta zone
238
+ doit être plus ouverte, c'est déclarer le rôle qu'elle exige vraiment.
239
+
185
240
  ## Prouver — trois identités, pas une
186
241
 
187
242
  Le refus d'un anonyme est gratuit : n'importe quelle zone le donne. Ce qui prouve, c'est le
@@ -195,6 +250,20 @@ npx nodefony security:user:add # un témoin SANS le rôle
195
250
  npx nodefony inspect routes --json # la garde est-elle sur la route qu'on croit ?
196
251
  ```
197
252
 
253
+ ## Utilisateurs et droits : tout existe, n'improvise RIEN
254
+
255
+ Chaque geste ci-dessus a sa référence INSTALLÉE — une recherche ordinaire ne la voit pas, `rg`
256
+ ne descendant pas dans `node_modules` :
257
+
258
+ - **Zones, authentificateurs, CSRF, CORS, clés d'API** — `node_modules/@nodefony/security/docs/firewall.md`
259
+ - **`@IsGranted`, voters, hiérarchie de rôles** — `node_modules/@nodefony/security/docs/authorization.md`
260
+ - **Contrat `IUser`, `UserService`, mot de passe** — `node_modules/@nodefony/user/docs/index.md`
261
+ - **Politique de contenu, nonce, HSTS** — `node_modules/@nodefony/security/docs/headers.md`
262
+
263
+ N'écris pas ton propre lecteur de session, ne teste pas l'appartenance à un rôle à la main, et
264
+ n'insère jamais un utilisateur directement en base — le mot de passe passe par l'encodeur du
265
+ framework.
266
+
198
267
  ## Voisins
199
268
 
200
269
  | Besoin | Skill |