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

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/docs/index.md CHANGED
@@ -264,8 +264,8 @@ est absent de `tools/list` **et** inappelable en le nommant, et le refus dit
264
264
 
265
265
  > 🔴 **Tant que la porte n'authentifie personne, un outil à scopes ne sortira
266
266
  > 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
267
+ > savoir avant de chercher une panne : `caller` vaut
268
+ > `{ authenticated: false, scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
269
269
  > branché. Le jour où il le sera, ces déclarations prendront effet sans qu'une
270
270
  > ligne d'outil change.
271
271
 
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.8",
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.8",
29
+ "@nodefony/http": "^10.0.0-alpha.8",
30
+ "nodefony": "^10.0.0-alpha.8",
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.8",
37
+ "@nodefony/http": "^10.0.0-alpha.8",
38
+ "nodefony": "^10.0.0-alpha.8"
39
39
  },
40
40
  "dependencies": {
41
41
  "axe-core": "4.13.0",
@@ -44,7 +44,7 @@ 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
47
+ `nom:type[?][:index|:unique]` — **non-null par défaut**, `?` rend facultatif, `:unique` pose une contrainte
48
48
  d'unicité, `:index` un index simple.
49
49
 
50
50
  | Type | Ce que ça produit |
@@ -82,7 +82,7 @@ index utiles sont composites : c'est ainsi qu'une table est réellement interrog
82
82
  Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
83
83
 
84
84
  ```bash
85
- npx nodefony create entity Session token:string! --table user_sessions --column-case snake --id-name session_id
85
+ npx nodefony create entity Session token:string:unique --table user_sessions --column-case snake --id-name session_id
86
86
  ```
87
87
 
88
88
  Faire suivre le TypeScript aurait transformé un réglage de nommage en refonte : le service, le
@@ -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
@@ -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 |