@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 +2 -2
- package/package.json +7 -7
- package/skills/nodefony-add-crud/SKILL.md +2 -2
- package/skills/nodefony-browser/SKILL.md +12 -7
- package/skills/nodefony-dev/SKILL.md +239 -15
- package/skills/nodefony-migrate-schema/SKILL.md +21 -0
- package/skills/nodefony-protect-route/SKILL.md +69 -0
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
|
|
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.
|
|
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.
|
|
29
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
30
|
-
"nodefony": "^10.0.0-alpha.
|
|
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.
|
|
37
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
38
|
-
"nodefony": "^10.0.0-alpha.
|
|
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[
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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 |
|