@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/controllers/DevkitController.js +2 -2
- package/dist/nodefony/controllers/McpController.js +3 -3
- package/dist/nodefony/service/DevkitService.js +2 -2
- package/docs/index.md +2 -2
- package/package.json +8 -8
- package/skills/nodefony-add-crud/SKILL.md +11 -2
- package/skills/nodefony-add-realtime-channel/SKILL.md +9 -0
- package/skills/nodefony-add-service/SKILL.md +9 -0
- package/skills/nodefony-browser/SKILL.md +31 -23
- package/skills/nodefony-dev/SKILL.md +497 -0
- package/skills/nodefony-dev/scripts/docs.mjs +544 -0
- package/skills/nodefony-devops/SKILL.md +198 -0
- package/skills/nodefony-devops/references/compose.md +125 -0
- package/skills/nodefony-devops/references/frontal.md +119 -0
- package/skills/nodefony-devops/references/image.md +136 -0
- package/skills/nodefony-devops/references/kubernetes.md +189 -0
- package/skills/nodefony-devops/references/podman.md +103 -0
- package/skills/nodefony-devops/references/secrets.md +108 -0
- package/skills/nodefony-devops/references/variables.md +174 -0
- package/skills/nodefony-migrate-schema/SKILL.md +50 -17
- 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", "
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 |
|