@nodefony/devkit 10.0.0-alpha.6 → 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.
Files changed (25) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/nodefony/controllers/DevkitController.js +2 -2
  6. package/dist/nodefony/controllers/McpController.js +3 -3
  7. package/dist/nodefony/service/DevkitService.js +2 -2
  8. package/docs/index.md +2 -2
  9. package/package.json +8 -8
  10. package/skills/nodefony-add-crud/SKILL.md +11 -2
  11. package/skills/nodefony-add-realtime-channel/SKILL.md +9 -0
  12. package/skills/nodefony-add-service/SKILL.md +9 -0
  13. package/skills/nodefony-browser/SKILL.md +31 -23
  14. package/skills/nodefony-dev/SKILL.md +497 -0
  15. package/skills/nodefony-dev/scripts/docs.mjs +544 -0
  16. package/skills/nodefony-devops/SKILL.md +198 -0
  17. package/skills/nodefony-devops/references/compose.md +125 -0
  18. package/skills/nodefony-devops/references/frontal.md +119 -0
  19. package/skills/nodefony-devops/references/image.md +136 -0
  20. package/skills/nodefony-devops/references/kubernetes.md +189 -0
  21. package/skills/nodefony-devops/references/podman.md +103 -0
  22. package/skills/nodefony-devops/references/secrets.md +108 -0
  23. package/skills/nodefony-devops/references/variables.md +174 -0
  24. package/skills/nodefony-migrate-schema/SKILL.md +50 -17
  25. package/skills/nodefony-protect-route/SKILL.md +78 -0
@@ -9,20 +9,26 @@ description: >
9
9
  exemplaires. À charger AVANT de modifier une entité déjà en base, ou avant de déployer un schéma
10
10
  changé.
11
11
  Déclencheurs : "j'ai ajouté un champ à une entité", "la colonne n'existe pas en base",
12
- "modifier une table existante", "migration", "migrer le schéma", "orm:migrate", "orm:generate",
13
- "la base est en retard", "appliquer les migrations", "déployer un changement de schéma",
14
- "comment passer ce modèle en production", "ma base ne correspond plus au code",
15
- "no such column", "column does not exist", "erreur SQL après avoir changé une entité",
16
- "adopter une base existante", "réparer une migration en échec", "le pod ne devient pas prêt",
17
- "comment tester ma migration", "éprouver une migration", "vérifier qu'une migration marche",
18
- "prouver que ma migration s'applique", "essayer sans casser ma base", "base d'essai",
19
- "rejouer les migrations depuis zéro", "repartir d'une base propre".
12
+ "modifier une table existante", "migrer le schéma", "orm:migrate", "orm:generate",
13
+ "appliquer les migrations", "déployer un changement de schéma", "no such column",
14
+ "column does not exist", "ma base ne correspond plus au code", "adopter une base existante",
15
+ "réparer une migration en échec", "éprouver une migration sans casser ma base",
16
+ "rejouer les migrations depuis zéro".
20
17
  metadata:
21
18
  version: 2
22
19
  ---
23
20
 
24
21
  # Faire évoluer un schéma, et le porter en production
25
22
 
23
+ > 🧭 **Tu es arrivé ici directement ? Charge aussi `nodefony-dev`** — il porte la conduite
24
+ > commune (par où commencer, comment prouver que c'est fait) et les pièges qui coûtent une heure,
25
+ > serveur comme front. Cette page-ci ne couvre QUE son geste.
26
+ >
27
+ > Et si une réponse te manque, elle est probablement INSTALLÉE : `rg` ne descend pas dans
28
+ > `node_modules`, donc 70 pages de documentation y paraissent absentes. Une commande les lit, avec
29
+ > la ligne exacte :
30
+ > `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs <termes>`.
31
+
26
32
  ## 1. La seule chose à savoir avant tout le reste
27
33
 
28
34
  **En développement, il n'y a rien à faire.** La base suit le code : la table naît au démarrage, et
@@ -88,6 +94,27 @@ reprendre après une coupure sans lire d'état préalable.
88
94
  > dépose un fichier vide et son entrée de journal. Le gabarit déposé explique comment séparer les
89
95
  > instructions ; suis-le à la lettre.
90
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
+
91
118
  ### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
92
119
 
93
120
  Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
@@ -139,15 +166,21 @@ npx nodefony orm:migrate
139
166
  d'appliquer ». Il ne refuse pas — il ne lit pas la base et ignore si ta table porte des lignes —,
140
167
  mais s'il le signale et que ta table n'est pas vide, la migration échouera.
141
168
 
142
- **À l'étape 3, sur sqlite, attends-toi à un refus `NF_GENERATE_DESTRUCTIVE`** — mesuré sur une
143
- table de deux lignes. Rendre une colonne obligatoire n'est pas un `ALTER` en sqlite : le moteur
144
- n'en a pas, alors l'outil RECONSTRUIT la table (`CREATE __new_billets` → `INSERT … SELECT` →
145
- `DROP TABLE` → `RENAME`). Le `DROP TABLE` est reconnu comme destructeur, et il l'est en général —
146
- ici il porte sur une table déjà recopiée, une ligne plus haut, dans la même migration. **Relis le
147
- fichier avant de décider** : si tu y vois l'`INSERT INTO __new_… SELECT … FROM …` juste avant le
148
- `DROP`, la reconstruction conserve les lignes, et `orm:migrate` l'applique sans broncher (les
149
- fichiers sont écrits, c'est leur mise en service qui était refusée). Éprouvé de bout en bout :
150
- deux lignes semées, trois étapes, deux lignes intactes et l'index unique en place.
169
+ **À l'étape 3, sur sqlite, attends-toi à un AVERTISSEMENT `table-rebuild`, pas à un refus.**
170
+ Rendre une colonne obligatoire n'est pas un `ALTER` en sqlite : le moteur n'en a pas, alors la
171
+ table est RECONSTRUITE (`CREATE` une table d'étape → `INSERT … SELECT` → `DROP TABLE` →
172
+ `RENAME`). Ce `DROP TABLE` porte sur une table recopiée une ligne plus haut, dans la même
173
+ migration : l'outil le reconnaît et te le signale — « recrée la table pour modifier une colonne
174
+ (SQLite ne sait pas faire autrement) » — sans rien refuser, à la génération comme à la mise en
175
+ service. Tu n'as donc **pas** besoin de `--allow-destructive` pour une reconstruction.
176
+
177
+ Ce qui reste à TA charge, et c'est le sens de l'avertissement : **relis l'`INSERT … SELECT`**.
178
+ Une colonne absente de cette recopie serait bel et bien perdue, et aucun automate ne peut le
179
+ deviner à ta place. Éprouvé de bout en bout : deux lignes semées, trois étapes, deux lignes
180
+ intactes et l'index unique en place.
181
+
182
+ > Un vrai `DROP TABLE` — une table supprimée sans recopie, ou une AUTRE table supprimée dans la
183
+ > même migration — reste refusé. Le verdict porte sur chaque table, pas sur le fichier.
151
184
 
152
185
  > **Ne jamais** répondre à un échec de migration en refaisant la base. Une migration qui n'est pas
153
186
  > passée n'a **rien** changé — sqlite et PostgreSQL l'annulent entière. C'est le fichier qu'il faut
@@ -10,11 +10,21 @@ 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
 
16
17
  # protect-route — la garde vient du framework, jamais de l'action
17
18
 
19
+ > 🧭 **Tu es arrivé ici directement ? Charge aussi `nodefony-dev`** — il porte la conduite
20
+ > commune (par où commencer, comment prouver que c'est fait) et les pièges qui coûtent une heure,
21
+ > serveur comme front. Cette page-ci ne couvre QUE son geste.
22
+ >
23
+ > Et si une réponse te manque, elle est probablement INSTALLÉE : `rg` ne descend pas dans
24
+ > `node_modules`, donc 70 pages de documentation y paraissent absentes. Une commande les lit, avec
25
+ > la ligne exacte :
26
+ > `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs <termes>`.
27
+
18
28
  > ⚖️ **La confiance n'exclut pas le contrôle.** Une route qui « répond 403 quand je teste » n'est
19
29
  > pas une route protégée. Le seul juge est un appel avec **trois identités**.
20
30
 
@@ -33,6 +43,36 @@ Les deux se combinent : la zone décide **qui entre**, la garde décide **qui fa
33
43
  async list() { … }
34
44
  ```
35
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
+
36
76
  🔴 **Le `pattern` d'une zone est un PRÉFIXE, jamais la liste des routes du jour.**
37
77
 
38
78
  ```ts
@@ -45,6 +85,19 @@ pattern: "^/api/account/(profile|invoices)"; // ❌ les routes d'aujourd'hui
45
85
  elle a l'air de couvrir l'espace, et l'introspection montre bien une route protégée à côté. Quand
46
86
  des routes partagent un préfixe, ne les protège pas une par une.
47
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
+
48
101
  ## 🔴 Ce qu'il ne faut jamais écrire
49
102
 
50
103
  ```ts
@@ -83,10 +136,14 @@ travail. **Le geste juste est de DÉCLARER l'origine**, jamais de retirer la dé
83
136
 
84
137
  ```ts
85
138
  csrf: {
139
+ secret: ctx.env.NF_CSRF_SECRET, // la clé que la config pose DÉJÀ — ne la perds pas
86
140
  trustedOrigins: ["https://partenaire.example"],
87
141
  }
88
142
  ```
89
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
+
90
147
  🔴 `@CsrfExempt`, `csrf.enabled: false`, ou couper le contrôle de provenance **résolvent le
91
148
  symptôme et ouvrent l'application** : n'importe quel site peut alors faire poster le navigateur
92
149
  d'une personne connectée, à son insu et avec ses droits.
@@ -170,9 +227,16 @@ marche, les tests passent, et le diff ne contient aucune faute visible — il co
170
227
  | `@BypassFirewall`, `@Anonymous` | la route sort de sa zone |
171
228
  | `anonymous` ajouté aux authentificateurs d'une zone | toute la zone devient publique |
172
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 |
173
231
 
174
232
  Relever un **seuil** est un réglage légitime. L'**éteindre** ne l'est pas.
175
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
+
176
240
  ## Prouver — trois identités, pas une
177
241
 
178
242
  Le refus d'un anonyme est gratuit : n'importe quelle zone le donne. Ce qui prouve, c'est le
@@ -186,6 +250,20 @@ npx nodefony security:user:add # un témoin SANS le rôle
186
250
  npx nodefony inspect routes --json # la garde est-elle sur la route qu'on croit ?
187
251
  ```
188
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
+
189
267
  ## Voisins
190
268
 
191
269
  | Besoin | Skill |