@nodefony/devkit 10.0.0-beta.5 → 10.0.0-beta.7

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 (38) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +3 -3
  4. package/dist/nodefony/controllers/DevkitController.js +1 -1
  5. package/dist/nodefony/controllers/McpController.js +2 -2
  6. package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
  7. package/dist/nodefony/mcp/profileRoute.js +28 -9
  8. package/dist/nodefony/service/DevkitService.js +6 -6
  9. package/docs/index.md +1 -1
  10. package/package.json +13 -13
  11. package/skills/nodefony-add-crud/SKILL.md +38 -23
  12. package/skills/nodefony-add-grpc-service/SKILL.md +179 -0
  13. package/skills/nodefony-add-grpc-service/scripts/call.mjs +218 -0
  14. package/skills/nodefony-add-realtime-binary/SKILL.md +301 -0
  15. package/skills/nodefony-add-realtime-binary/references/messages.md +20 -0
  16. package/skills/nodefony-add-realtime-binary/references/mesures.md +59 -0
  17. package/skills/nodefony-add-realtime-channel/SKILL.md +452 -105
  18. package/skills/nodefony-add-realtime-channel/references/contrat.md +22 -0
  19. package/skills/nodefony-add-realtime-channel/references/production.md +140 -0
  20. package/skills/nodefony-add-realtime-presence/SKILL.md +328 -0
  21. package/skills/nodefony-authentication/SKILL.md +2 -2
  22. package/skills/nodefony-browser/SKILL.md +37 -2
  23. package/skills/nodefony-browser/references/sondes.md +19 -0
  24. package/skills/nodefony-browser/scripts/audit.mjs +17 -2
  25. package/skills/nodefony-browser/scripts/inspect.mjs +108 -87
  26. package/skills/nodefony-browser/scripts/lib/actions.mjs +164 -0
  27. package/skills/nodefony-browser/scripts/lib/browser.mjs +67 -11
  28. package/skills/nodefony-browser/scripts/lib/probes.mjs +108 -3
  29. package/skills/nodefony-browser/scripts/watch.mjs +27 -11
  30. package/skills/nodefony-dev/SKILL.md +31 -15
  31. package/skills/nodefony-dev/references/rapport-de-build.md +19 -0
  32. package/skills/nodefony-devops/SKILL.md +28 -21
  33. package/skills/nodefony-devops/references/dimensionnement.md +1 -1
  34. package/skills/nodefony-devops/references/frontal.md +5 -2
  35. package/skills/nodefony-devops/references/image.md +23 -15
  36. package/skills/nodefony-migrate-schema/SKILL.md +110 -20
  37. package/skills/nodefony-migrate-schema/references/verdicts.md +23 -20
  38. package/skills/nodefony-protect-route/SKILL.md +27 -32
@@ -1,21 +1,21 @@
1
1
  ---
2
2
  name: nodefony-migrate-schema
3
3
  description: >
4
- Fait évoluer le schéma d'une base Nodefony et le porte en production, par les commandes
5
- `orm:generate` et `orm:migrate` du framework — jamais par un `ALTER` écrit à la main ni par la
6
- suppression d'une base. Porte la lecture de l'état (que l'application tourne ou non), le plan
7
- avant le geste, les codes de refus et le geste que chacun appelle, les trois interdits qui
8
- cassent un historique, et le patron de déploiement où les migrations passent AVANT les
9
- exemplaires. À charger AVANT de modifier une entité déjà en base, ou avant de déployer un schéma
10
- changé.
4
+ Fait évoluer le schéma d'une base Nodefony et le porte en production, par `orm:generate` et
5
+ `orm:migrate` — jamais par un `ALTER` à la main ni en supprimant la base. Porte la lecture de
6
+ l'état, le plan avant le geste, chaque code de refus et son geste, le renommage déclaré, les trois
7
+ interdits qui cassent un historique, et le déploiement où les migrations passent AVANT les
8
+ exemplaires. À charger AVANT de modifier une entité déjà en base, ou de déployer un schéma changé.
11
9
  Déclencheurs : "j'ai ajouté un champ à une entité", "la colonne n'existe pas en base",
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",
10
+ "migrer le schéma", "orm:migrate", "orm:generate", "déployer un changement de schéma",
11
+ "no such column",
14
12
  "column does not exist", "ma base ne correspond plus au code", "adopter une base existante",
15
13
  "réparer une migration en échec", "éprouver une migration sans casser ma base",
16
- "rejouer les migrations depuis zéro".
14
+ "rejouer les migrations depuis zéro", "renommer une colonne ou une table",
15
+ "changer le type ou la longueur d'une colonne", "changer de base", "passer à PostgreSQL ou
16
+ MySQL".
17
17
  metadata:
18
- version: 2
18
+ version: 4
19
19
  ---
20
20
 
21
21
  # Faire évoluer un schéma, et le porter en production
@@ -29,18 +29,27 @@ metadata:
29
29
  > la ligne exacte :
30
30
  > `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs <termes>`.
31
31
 
32
- ## 1. La seule chose à savoir avant tout le reste
32
+ ## 1. La seule chose à savoir avant tout le reste — QUI fabrique le schéma ici ?
33
33
 
34
- **En développement, il n'y a rien à faire.** La base suit le code : la table naît au démarrage, et
35
- un champ ajouté **qui accepte le vide** est posé au démarrage suivant.
34
+ La clé `ddl` d'un connecteur répond à une seule question : qui fabrique les tables. Le mode n'est
35
+ presque jamais écrit, il se DÉDUIT — et c'est lui qui décide du geste.
36
36
 
37
- **Deux cas seulement sortent de là**, et ce sont eux qui amènent ici :
37
+ | Situation | Mode | Qui fabrique les tables |
38
+ | -------------------------------------------------------- | --------- | ----------------------------------------------------------- |
39
+ | `ddl` écrit dans la configuration | lui | ce que dit la configuration — il gagne toujours |
40
+ | développement, l'application versionne **≥ 1** migration | `migrate` | les migrations, appliquées au démarrage |
41
+ | développement, **aucune** migration ; ou `NODE_ENV=test` | `auto` | le démarrage, d'après les entités (création + ajouts vides) |
42
+ | **tout le reste**, production comprise | `none` | personne au démarrage : un job lance `orm:migrate` AVANT |
38
43
 
39
- - un champ **obligatoire** ajouté à une table qui existe déjà — il n'est jamais rattrapé ;
40
- - **la production**, où le démarrage ne fabrique JAMAIS le schéma.
44
+ 🔴 **Une application créée par `nodefony create app` avec un ORM SQL est en `migrate` dès
45
+ l'installation** : le générateur écrit ET applique sa migration initiale. Le cas courant, en
46
+ développement, est donc : **une entité change → une migration s'écrit** (§ 3). Ne crois pas
47
+ qu'« en développement la base suit le code » sans avoir vérifié qu'aucune migration n'existe
48
+ (`npx nodefony orm:migrate:status --json`).
41
49
 
42
- Si l'application est en développement et que le champ ajouté accepte le vide, il suffit de
43
- redémarrer. Ne produis pas une migration pour ça.
50
+ En `auto` seulement, un champ ajouté **qui accepte le vide** est posé au démarrage suivant — rien à
51
+ taper. Tout le reste (renommage, type, longueur, suppression, champ obligatoire) n'y est JAMAIS
52
+ rattrapé : l'ancienne colonne reste en base, la nouvelle naît vide.
44
53
 
45
54
  ## 2. Lire l'état — deux voies, selon que l'application tourne
46
55
 
@@ -115,6 +124,62 @@ relue et versionnée en amont, et c'est `orm:migrate` seul qui l'applique.
115
124
  > second échoue sur un objet qui n'a jamais existé. Un `ddl` écrit dans la configuration gagne
116
125
  > toujours ; sous `NODE_ENV=test`, rien ne change.
117
126
 
127
+ ### 🔴 Un RENOMMAGE — le DÉCLARER, jamais le laisser deviner
128
+
129
+ Une colonne qui disparaît pendant qu'une autre apparaît, c'est un renommage (les données suivent)
130
+ OU une suppression puis un ajout (les données sont perdues). Aucun diff ne voit une intention. Ne
131
+ lance donc JAMAIS `orm:generate` nu après avoir renommé un champ : il s'arrête
132
+ (`NF_GENERATE_NEEDS_ANSWER`), et **il n'y a pas de question à laquelle répondre** — l'outil tourne
133
+ sans terminal, humain devant l'écran compris. La réponse se DÉCLARE :
134
+
135
+ ```bash
136
+ # une colonne : la table sous le nom que porte son entité
137
+ npx nodefony orm:generate --name titre_en_anglais --rename article.titre=title
138
+ # une table, puis une colonne DANS la table renommée (son NOUVEAU nom)
139
+ npx nodefony orm:generate --name lignes --rename invoice_line=invoice_det --rename invoice_det.fk_parent_line=fk_parent
140
+ ```
141
+
142
+ - La migration porte un `RENAME`, **changement de type compris** (`varchar(8)` → `varchar(16)`
143
+ renommé ET élargi s'écrit en entier).
144
+ - Le rapport `--json` le confirme : `renamed: ["article.titre → title"]`. Relis le fichier quand même.
145
+ - Un nom mal écrit, une ancienne colonne absente du dernier instantané, un nouveau nom qu'aucune
146
+ entité ne déclare : refus `NF_GENERATE_RENAME`, **rien n'est écrit**.
147
+ - Une VRAIE suppression ET un ajout sans rapport dans la même table : **deux générations** — d'abord
148
+ l'entité sans la colonne retirée (la perte est nommée par le garde destructif), puis avec
149
+ l'ajout. Ne déclare pas `--rename` pour faire taire l'outil : ce serait mentir sur les données.
150
+
151
+ Allonger une colonne (`varchar(16)` → `varchar(32)`) ou monter un entier de rang (`integer` →
152
+ `bigint`) n'est PAS destructif : le générateur le constate sur l'état de départ et n'exige rien. Le
153
+ raccourcir, ou rendre la colonne obligatoire, reste refusé sans `--allow-destructive`.
154
+
155
+ ### Les pièges d'un schéma qui ÉVOLUE — constatés en faisant migrer un vrai progiciel
156
+
157
+ Chacun a été rencontré en portant un schéma réel d'une version à la suivante, sur SQLite,
158
+ PostgreSQL, MariaDB et MySQL. Le geste est le seul qui ait tenu sur les quatre.
159
+
160
+ 1. **Renommer la PROPRIÉTÉ sans renommer la colonne ne demande AUCUNE migration.** Le modificateur
161
+ `column` garde le nom en base : `title: { kind: "string", length: 180, column: "titre" }`. Le
162
+ code lit `title`, la base garde `titre`, `orm:generate` répond « rien à écrire ». Choisis-le
163
+ quand seul le nom vu par le code doit changer — c'est le renommage qui ne coûte rien.
164
+ 2. **Une table renommée fait apparaître des `DROP CONSTRAINT` / `DROP INDEX` puis leurs `ADD`** :
165
+ les noms de contraintes que le kit dérive du nom de table changent, la base garde les anciens.
166
+ En SQLite, la table est même RECRÉÉE (`__new_<table>`, recopie, `DROP TABLE`, `RENAME`). C'est
167
+ la mécanique, pas une perte, et le garde ne la signale pas. Ce qui se vérifie : la ligne
168
+ `renamed` du rapport, et **aucun `DROP COLUMN`** dans le fichier.
169
+ 3. **Une référence posée sur des lignes DÉJÀ remplies échoue si une ligne pointe vers rien** —
170
+ sur les quatre moteurs : PostgreSQL et MySQL refusent l'`ADD CONSTRAINT`, SQLite refuse la
171
+ validation (« référence(s) orpheline(s) », table et ligne nommées). La migration est annulée et
172
+ un marqueur d'échec posé. Geste : corriger d'ABORD les lignes dans une migration libre
173
+ (`--custom` : `UPDATE … SET fk = NULL WHERE …`), PUIS déclarer la référence ; après un échec,
174
+ `orm:migrate:repair` lève le marqueur une fois la cause traitée.
175
+ 4. **Retirer un champ de l'entité, c'est `DROP COLUMN`** : refusé sans `--allow-destructive`.
176
+ Un progiciel qui abandonne une colonne la laisse souvent en base sur les installations
177
+ existantes (le script de passage réel ne la supprime pas). Si ses données comptent encore,
178
+ garde le champ, facultatif, plutôt que de le retirer.
179
+ 5. **Une évolution se prouve sur une base PEUPLÉE, jamais vide** (§ 4 b). Une table vide ne
180
+ déclenche ni la recréation SQLite d'une table référencée, ni une référence orpheline : un défaut
181
+ de l'applicateur SQLite n'apparaissait QUE sur des lignes réelles. Une ligne par table suffit.
182
+
118
183
  ### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
119
184
 
120
185
  Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
@@ -218,6 +283,29 @@ Deux choses qu'il rapporte et qu'il faut lire :
218
283
  > Cela ne concerne QUE cette commande de reprise : la création des tables, leur migration et le
219
284
  > fonctionnement de l'application sont inchangés sur MariaDB.
220
285
 
286
+ ### Changer de BASE — de SQLite à PostgreSQL, MySQL ou MariaDB
287
+
288
+ Une entité écrite par `create entity` est une description PORTABLE (`defineTable`) : elle suit la
289
+ base du connecteur, **sans être réécrite**. Changer de moteur tient en quatre gestes :
290
+
291
+ ```bash
292
+ npm install pg # ou mysql2 — le pilote de la NOUVELLE base (absent : le démarrage le nomme)
293
+ # .env : NF_DATABASE_URL=postgres://user:secret@hote:5432/app (le dialecte se déduit de l'URL)
294
+ npx nodefony orm:generate --name initiale # migration INITIALE de ce dialecte
295
+ npx nodefony orm:migrate
296
+ ```
297
+
298
+ - **Les migrations se rangent par dialecte** (`migrations/<dialecte>/`) : celles de SQLite ne
299
+ s'appliquent jamais ailleurs, d'où la migration initiale du nouveau moteur.
300
+ - 🔴 **Les DONNÉES ne suivent pas.** Le schéma change de base, les lignes restent dans l'ancienne :
301
+ aucune commande ne les transfère — export puis import, à ta charge.
302
+ - **Une table NATIVE ne suit pas** (`sqliteTable`, ou `create entity --dialect <moteur>`) : pointée
303
+ vers un autre moteur, l'application refuse de démarrer en nommant l'entité
304
+ (`EntityNotPortedError`). Régénère-la sans `--dialect` (`create entity <Nom> … --force`).
305
+ - **Certains écarts appartiennent au moteur**, pas au framework : l'ordre de tri des chaînes et
306
+ l'égalité sans casse suivent la collation de la base, la place des `NULL` dans un tri diffère, le
307
+ type `vector` n'existe qu'en PostgreSQL. Le SQL natif que tu écris reste propre à ton dialecte.
308
+
221
309
  ## 4. Éprouver une migration — sur une base d'ESSAI, jamais sur la tienne
222
310
 
223
311
  Quand il faut **prouver** qu'une migration fait ce qu'elle annonce, la réponse n'est jamais de
@@ -320,9 +408,11 @@ Le `code` est stable — **lis-le, il désigne le geste**.
320
408
  | `NF_MIGRATE_HASH_MISMATCH` | un fichier déjà appliqué a été modifié | RÉTABLIR le fichier (1er geste) ; `--update-hashes` seulement si la modification était sans effet |
321
409
  | `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière posée | renommer la nouvelle après la dernière appliquée |
322
410
  | `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
323
- | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
411
+ | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, RIEN n'attend, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
324
412
  | `NF_MIGRATE_LOCK_TIMEOUT` | un autre travail de migration tient le verrou | ATTENDRE puis rejouer — ce n'est pas une panne, et le verbe est idempotent |
325
413
  | `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
414
+ | `NF_GENERATE_NEEDS_ANSWER` | une colonne disparaît et une autre apparaît : renommage ou perte ? | renommage → `--rename table.ancienne=nouvelle` ; vraie suppression + ajout → deux générations |
415
+ | `NF_GENERATE_RENAME` | un `--rename` mal écrit, ou que le dernier instantané ne permet pas | corriger la déclaration — la table sous le nom de son entité ; rien n'a été écrit |
326
416
 
327
417
  Les codes exhaustifs, avec un exemple de charge utile pour chacun :
328
418
  [`references/verdicts.md`](references/verdicts.md).
@@ -113,26 +113,29 @@ n'a jamais les deux — c'est le discriminant à tester.
113
113
 
114
114
  ### Refus d'usage — la demande elle-même
115
115
 
116
- | Code | Ce qui s'est passé | Le geste |
117
- | -------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
118
- | `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
119
- | `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
120
- | `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
121
- | `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
122
- | `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
123
- | `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
124
- | `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
125
- | `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
126
- | `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
127
- | `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
128
- | `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
129
- | `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
130
- | `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
131
- | `NF_GENERATE_UNREADABLE_ENTITY` | rien d'écrit, et un fichier d'entités ne s'importe pas : sa table n'est vue par personne | corriger la cause nommée (souvent un paquet absent des dépendances), ou sortir le fichier de `nodefony/entity/` |
132
- | `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
133
- | `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
134
- | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
135
- | `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
116
+ | Code | Ce qui s'est passé | Le geste |
117
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
118
+ | `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
119
+ | `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
120
+ | `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
121
+ | `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
122
+ | `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
123
+ | `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
124
+ | `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
125
+ | `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
126
+ | `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
127
+ | `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
128
+ | `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
129
+ | `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
130
+ | `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
131
+ | `NF_GENERATE_UNREADABLE_ENTITY` | rien d'écrit, et un fichier d'entités ne s'importe pas : sa table n'est vue par personne | corriger la cause nommée (souvent un paquet absent des dépendances), ou sortir le fichier de `nodefony/entity/` |
132
+ | `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
133
+ | `NF_GENERATE_DEV_MODULE_TABLE` | un module `policy: "dev"` lie ses tables au connecteur généré : elles iraient en base de production | lui donner un connecteur à lui, sans migration ; ou retirer sa politique `dev` |
134
+ | `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
135
+ | `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, RIEN n'attend, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
136
+ | `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
137
+ | `NF_GENERATE_NEEDS_ANSWER` | une colonne disparaît et une autre apparaît — l'outil demande « renommage ? », sans terminal possible | renommage → `--rename table.ancienne=nouvelle` ; vraie suppression + ajout → deux générations |
138
+ | `NF_GENERATE_RENAME` | `--rename` mal écrit, ancien nom absent du dernier instantané, ou nouveau nom qu'aucune entité ne porte | corriger la déclaration (la table sous le nom de son entité) — rien n'a été écrit |
136
139
 
137
140
  ## Un contrat qui ne bougera pas
138
141
 
@@ -245,10 +245,10 @@ Un service partenaire, un script, un agent ne stockent aucun cookie. Le
245
245
  `nodefony/config/security.ts` généré (preset `complete`) pose **deux zones, une par sorte de
246
246
  preuve** — choisis selon ce que l'appelant détient, n'en ajoute pas une troisième :
247
247
 
248
- | L'appelant présente | Zone | Préfixe | Obtenir la preuve |
249
- | ---------------------------------------------- | --------- | -------------- | --------------------------------------- |
250
- | une **clé d'API** opaque (`nf_…`) | `machine` | `/api/machine` | `POST /nodefony/security/api/keys` |
251
- | un **jeton d'accès** (de l'app ou de Keycloak) | `partner` | `/api/partner` | `npx nodefony app:partner:call` le fait |
248
+ | L'appelant présente | Zone | Préfixe | Obtenir la preuve |
249
+ | ------------------------------------ | --------- | -------------- | ---------------------------------- |
250
+ | une **clé d'API** opaque (`nf_…`) | `machine` | `/api/machine` | `POST /nodefony/security/api/keys` |
251
+ | un **jeton d'accès** signé par l'app | `service` | `/api/service` | émis par l'app (voir la démo) |
252
252
 
253
253
  ### Clé d'API : la zone `machine`
254
254
 
@@ -275,27 +275,30 @@ qui ne stocke pas de cookie ne doit rien recevoir qu'il faille stocker.**
275
275
 
276
276
  Les clés s'émettent par `POST /nodefony/security/api/keys`.
277
277
 
278
- ### Jeton d'accès : la zone `partner`, et la commande qui l'éprouve
278
+ ### Jeton d'accès : la zone `service`
279
279
 
280
- La zone accepte un jeton signé par l'application (`jwt`) **et** un jeton de Keycloak
281
- (`external-jwt`) ; les deux s'aiguillent sur l'émetteur, leur ordre ne décide de rien. Une route
282
- qui tombe sous `/api/partner` est protégée sans rien ajouter. L'exemple qui marche est déjà dans
283
- l'application — **lis-le avant d'écrire un appelant** :
284
-
285
- ```bash
286
- npx nodefony app:partner:call # jeton signé par l'app pour `admin` (--as <compte>)
287
- npx nodefony app:partner:call --issuer keycloak # client <app>-machine, flux client_credentials
280
+ ```ts
281
+ service: {
282
+ pattern: "^/api/service",
283
+ authenticators: ["jwt"], // jeton signé par CETTE application
284
+ stateless: true,
285
+ }
288
286
  ```
289
287
 
290
- Source : `nodefony/command/PartnerCallCommand.ts` (la commande) et
291
- `nodefony/controllers/PartnerController.ts` (`GET /api/partner/whoami`, qui rend l'identité ET
292
- l'émetteur). Un jeton Keycloak s'obtient par `requestClientCredentialsToken` de
293
- `@nodefony/security` — découverte du point de jeton comprise —, jamais par un `fetch` recopié.
288
+ Une route qui tombe sous `/api/service` est protégée sans rien ajouter. L'application générée
289
+ n'y met AUCUN controller : l'exemple complet — émission du jeton, appel, et la variante Keycloak
290
+ (`client_credentials`) — vit dans la démo, qu'on copie dans l'application pour la lire.
291
+
292
+ **Accepter aussi les jetons d'un fournisseur** (Keycloak) : ajouter `"external-jwt"` à la zone ;
293
+ `jwt` et `external-jwt` s'aiguillent sur l'émetteur du jeton, leur ordre ne décide de rien. Un jeton
294
+ Keycloak s'obtient par `requestClientCredentialsToken` de `@nodefony/security` — découverte du point
295
+ de jeton comprise —, jamais par un `fetch` recopié.
294
296
 
295
297
  🔴 **L'audience vit en UN endroit.** Un jeton n'ouvre que la zone dont il porte la ressource
296
- (RFC 8707). `partnerResource(ctx.env)` est lue par la zone (`resource`), par `jwt.audiences` et par
297
- la commande. Une ressource recopiée à la main finit par différer, et le seul symptôme est un refus
298
- sans explication. Ouvrir une AUTRE ressource à des jetons, c'est trois gestes, pas un :
298
+ (RFC 8707). Écris-la UNE fois (une fonction de `nodefony/config/security.ts`, sur le modèle de
299
+ `publicBaseUrl`), lue par la zone (`resource`) et par `jwt.audiences`. Une ressource recopiée à la
300
+ main finit par différer, et le seul symptôme est un refus sans explication. Ouvrir une ressource
301
+ à des jetons, c'est trois gestes, pas un :
299
302
 
300
303
  | Oublié | Symptôme |
301
304
  | ----------------------------------------- | ------------------------------------------------------------- |
@@ -303,19 +306,11 @@ sans explication. Ouvrir une AUTRE ressource à des jetons, c'est trois gestes,
303
306
  | `security:keycloak:realm --write` relancé | **401** du jeton Keycloak : le realm n'inscrit pas l'audience |
304
307
  | la `resource` sur la zone | l'application **refuse de démarrer** en nommant la zone |
305
308
 
306
- 🔴 **`subjectPolicy: "ephemeral"` s'écrit SUR LA ZONE, jamais dans `resourceServer`.** Le client
309
+ 🔴 **`subjectPolicy: "ephemeral"` s'écrit SUR LA ZONE, jamais dans `resourceServer`.** Un client
307
310
  machine de Keycloak n'a pas de compte ici : `ephemeral` l'accepte le temps de la requête, avec les
308
- seuls `ephemeralRoles` de la zone (`ROLE_PARTNER`). Posé dans `resourceServer.subjectPolicy`
309
- (global), il ferait accepter un appelant sans compte par **toutes** les zones `external-jwt`. Et
310
- un rôle fort dans `ephemeralRoles` est accordé à **quiconque** détient un jeton valide du realm.
311
-
312
- `whoami` n'exige pas `ROLE_PARTNER` — un compte de l'app ne l'a pas. Pour réserver une action aux
313
- partenaires : `@IsGranted("ROLE_PARTNER")`.
314
-
315
- **Prouver** : `--issuer nodefony` rend 200 sans Keycloak ; `--issuer keycloak` rend 200 avec le
316
- profil `keycloak` du compose allumé ; et le MÊME jeton machine est refusé (401) sur une zone qui
317
- garde `subjectPolicy` par défaut (`require`). Chaque échec de la commande sort en code 1 avec le
318
- geste qui répare — lis-le avant de chercher ailleurs.
311
+ seuls `ephemeralRoles` de la zone. Posé dans `resourceServer.subjectPolicy` (global), il ferait
312
+ accepter un appelant sans compte par **toutes** les zones `external-jwt`. Et un rôle fort dans
313
+ `ephemeralRoles` est accordé à **quiconque** détient un jeton valide du realm.
319
314
 
320
315
  ## Les gestes qui affaiblissent en silence
321
316