@nodefony/devkit 10.0.0-beta.2 → 10.0.0-beta.4
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/README.md +151 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +27 -163
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/card.js +1 -2
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +217 -20
- package/package.json +17 -12
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
- package/skills/nodefony-add-service/SKILL.md +14 -3
- package/skills/nodefony-authentication/SKILL.md +234 -0
- package/skills/nodefony-browser/SKILL.md +65 -21
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/references/sondes.md +13 -0
- package/skills/nodefony-browser/scripts/audit.mjs +59 -10
- package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
- package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
- package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +78 -82
- package/skills/nodefony-dev/scripts/docs.mjs +24 -8
- package/skills/nodefony-devops/SKILL.md +98 -74
- package/skills/nodefony-devops/references/compose.md +8 -0
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +282 -48
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +30 -5
- package/skills/nodefony-devops/references/variables.md +66 -31
- package/skills/nodefony-migrate-schema/SKILL.md +5 -2
- package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
- package/skills/nodefony-protect-route/SKILL.md +124 -21
- package/skills/nodefony-security-review/SKILL.md +127 -0
- package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
- package/skills/nodefony-troubleshoot/SKILL.md +225 -0
- package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
- package/skills/nodefony-upgrade/SKILL.md +130 -0
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 1. La commande qui répond, et à laquelle rien ne ment
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx nodefony env # la
|
|
8
|
+
npx nodefony env # la valeur EFFECTIVE de chaque variable, et sa PROVENANCE (.env ou process)
|
|
9
9
|
npx nodefony env --json # le même rapport, pour un script
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -19,8 +19,11 @@ npx nodefony inspect schema <module> # les clés qui EXISTENT, leur type,
|
|
|
19
19
|
npx nodefony doctor --env production # ce qui manquerait là-bas, depuis ici
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
> 🔴 **`inspect schema` avant d'écrire une clé.** Une clé inconnue
|
|
23
|
-
>
|
|
22
|
+
> 🔴 **`inspect schema` avant d'écrire une clé.** Une clé inconnue **arrête le démarrage**
|
|
23
|
+
> (code 78) en la nommant — y compris une clé qu'une version du framework a retirée, que tu
|
|
24
|
+
> découvres alors au premier déploiement après un `npm update`. Ce qui reste muet, c'est une
|
|
25
|
+
> **variable** mal orthographiée (`NF_PROT` au lieu de `NF_PORT`) : inconnue, donc ignorée, et
|
|
26
|
+
> seul `npx nodefony env` la montre.
|
|
24
27
|
|
|
25
28
|
## 2. Deux grammaires, et elles ne font pas la même chose
|
|
26
29
|
|
|
@@ -54,10 +57,33 @@ booléen, un nombre devient un nombre, `[…]` et `{…}` sont lus en JSON, une
|
|
|
54
57
|
devient un tableau, le reste reste une chaîne. C'est ce qui évite le piège classique où la chaîne
|
|
55
58
|
`"false"` est convertie en booléen **vrai**.
|
|
56
59
|
|
|
60
|
+
### `NF_<MODULE>_<RÉGLAGE>` — les noms courts des modules
|
|
61
|
+
|
|
62
|
+
Un module déclare sur un champ de son schéma le nom court qui le règle (`NF_REDIS_HOST`,
|
|
63
|
+
`NF_REALTIME_DRIVER`…). `npx nodefony env --example` les écrit dans `.env.example`, une section
|
|
64
|
+
par module. Trois règles, toutes au démarrage :
|
|
65
|
+
|
|
66
|
+
- La valeur est **validée par le schéma** : `NF_REDIS_PORT=abc` arrête le démarrage (78) en
|
|
67
|
+
nommant la VARIABLE, pas le seul champ.
|
|
68
|
+
- **Nom court ET `NF__…` sur le même champ** : refus du démarrage en production (78) ; en
|
|
69
|
+
développement, `NF__…` l'emporte avec un avertissement qui nomme les deux.
|
|
70
|
+
- Chaque nom court accepte `<NOM>_FILE` ; une variable vide vaut absente.
|
|
71
|
+
|
|
72
|
+
### Lire la notice `.env.example`
|
|
73
|
+
|
|
74
|
+
Chaque variable y porte ses métadonnées : `@required` / `@optional`, `@required=forEnv(production)`,
|
|
75
|
+
`@requiredWhen="…"` (exigée sous condition — `NF_JWT_KEYSET` : « en production, si l'application
|
|
76
|
+
émet des jetons »), `@sensitive`, `@default`, et **`@placement`**, qui dit où la poser en
|
|
77
|
+
production : `platform` (variables du déploiement), `secrets` (gestionnaire de secrets, ou
|
|
78
|
+
`<NOM>_FILE`), `workstation` (le poste seulement — ne la pose pas en production).
|
|
79
|
+
`npm run env:check` échoue (78) quand la notice a dérivé d'`env.ts`.
|
|
80
|
+
|
|
57
81
|
## 3. Le suffixe `_FILE` — les secrets montés
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
Les variables du catalogue acceptent un `_FILE`, **URL d'infrastructure comprises**
|
|
84
|
+
(`NF_DATABASE_URL_FILE`, `NF_REDIS_URL_FILE`, et leurs alias de plateforme). Le contenu du fichier
|
|
85
|
+
est lu, retour à la ligne final retiré. Une seule règle les tranche toutes : le tableau ci-dessous
|
|
86
|
+
vaut pour l'URL de la base comme pour un secret de session.
|
|
61
87
|
|
|
62
88
|
```yaml
|
|
63
89
|
env:
|
|
@@ -93,32 +119,41 @@ Les seules qui te concernent au déploiement. Le catalogue complet, c'est `npx n
|
|
|
93
119
|
|
|
94
120
|
### Derrière un frontal — **les plus importantes**
|
|
95
121
|
|
|
96
|
-
| Variable | Rôle | Absente ⇒
|
|
97
|
-
| ---------------------------- | ------------------------------------------------- |
|
|
98
|
-
| `NF__HTTP__TRUSTPROXY` | Quels intermédiaires sont crus sur leurs en-têtes | **Le schéma constaté est `http`** → cookie `__Host
|
|
99
|
-
| `NF__HTTP__TRUSTEDHOSTS` | Les hôtes servis | Un `Host` étranger n'est pas refusé
|
|
100
|
-
| `NF__APP__DOMAINCHECK` | Contrôle du domaine | Pas de contrôle
|
|
101
|
-
| `NF__HTTP__STATICS__ENABLED` | L'application sert-elle les fichiers statiques | Elle les sert — donc en double avec ton frontal
|
|
122
|
+
| Variable | Rôle | Absente ⇒ |
|
|
123
|
+
| ---------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `NF__HTTP__TRUSTPROXY` | Quels intermédiaires sont crus sur leurs en-têtes | **Le schéma constaté est `http`** → cookie de session sans `__Host-`, mot de passe refusé `403`, adresse client fausse |
|
|
125
|
+
| `NF__HTTP__TRUSTEDHOSTS` | Les hôtes servis | Un `Host` étranger n'est pas refusé |
|
|
126
|
+
| `NF__APP__DOMAINCHECK` | Contrôle du domaine | Pas de contrôle |
|
|
127
|
+
| `NF__HTTP__STATICS__ENABLED` | L'application sert-elle les fichiers statiques | Elle les sert — donc en double avec ton frontal |
|
|
102
128
|
|
|
103
129
|
### Données
|
|
104
130
|
|
|
105
|
-
| Variable | Rôle | Absente ⇒
|
|
106
|
-
| ----------------- | ---------------------- |
|
|
107
|
-
| `NF_DATABASE_URL` | L'URL de la base | **Repli sur SQLite local**, dans `var/` —
|
|
108
|
-
| `NF_REDIS_URL` | L'URL du cache partagé | Le cache n'est pas chargé
|
|
131
|
+
| Variable | Rôle | Absente ⇒ |
|
|
132
|
+
| ----------------- | ---------------------- | ------------------------------------------------- |
|
|
133
|
+
| `NF_DATABASE_URL` | L'URL de la base | **Repli sur SQLite local**, dans `var/` — annoncé |
|
|
134
|
+
| `NF_REDIS_URL` | L'URL du cache partagé | Le cache n'est pas chargé |
|
|
109
135
|
|
|
110
136
|
> Le repli SQLite est le piège d'exploitation classique : l'application **démarre**, sert des
|
|
111
|
-
> requêtes, écrit dans un fichier — et chaque exemplaire a sa propre base.
|
|
112
|
-
>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
137
|
+
> requêtes, écrit dans un fichier — et chaque exemplaire a sa propre base. Il est ANNONCÉ
|
|
138
|
+
> (avertissement au démarrage, point `DB_SQLITE_FALLBACK` dans `npx nodefony status --json`),
|
|
139
|
+
> mais rien n'échoue : lis le bilan du démarrage. Seule exception : si une entité est écrite pour
|
|
140
|
+
> un autre moteur (PostgreSQL, MySQL), le repli est **refusé** et le démarrage s'arrête.
|
|
141
|
+
|
|
142
|
+
### Secrets — ce qui se passe en production sans eux
|
|
143
|
+
|
|
144
|
+
| Variable | Ce qu'il protège | Absente en production ⇒ |
|
|
145
|
+
| ------------------- | ------------------------------------------------ | --------------------------------------------------- |
|
|
146
|
+
| `NF_CSRF_SECRET` | Les jetons anti-CSRF, partagés entre exemplaires | **Refus de démarrer** (78) |
|
|
147
|
+
| `NF_JWT_KEYSET` | La clé de signature des jetons, commune à tous | **Refus de démarrer** si l'app émet des jetons |
|
|
148
|
+
| `NF_TOTP_KEY` | Les secrets de double authentification au repos | Double authentification désactivée (journal CRITIC) |
|
|
149
|
+
| `NF_WEBHOOK_KEY` | Les signatures de webhooks | Webhooks désactivés (journal CRITIC) |
|
|
150
|
+
| `NF_ADMIN_PASSWORD` | Le compte d'administration, au PREMIER démarrage | Aucun compte créé ; inutile une fois le compte créé |
|
|
151
|
+
|
|
152
|
+
`NF_JWT_KEYSET` manque presque toujours au premier déploiement : sans elle, un exemplaire qui
|
|
153
|
+
émet des jetons **refuse de démarrer** en production (78) — et deux exemplaires à clés
|
|
154
|
+
différentes refuseraient chacun les jetons de l'autre. `npx nodefony security:secrets --jwt-keyset`
|
|
155
|
+
la produit (une ligne JSON) ; `--env` rend les autres secrets en lignes `CLÉ=valeur`, prêtes pour
|
|
156
|
+
`docker run --env-file` ou un gestionnaire de secrets.
|
|
122
157
|
|
|
123
158
|
Tous en `_FILE` en production. Détail et rotation → [`secrets.md`](secrets.md).
|
|
124
159
|
|
|
@@ -129,7 +164,7 @@ Tous en `_FILE` en production. Détail et rotation → [`secrets.md`](secrets.md
|
|
|
129
164
|
| `NF_POD_NAME` | Identité de l'exemplaire — **dérive l'identité d'origine du bus temps réel** |
|
|
130
165
|
| `NF_INSTANCE_ID` | Identifiant d'exemplaire |
|
|
131
166
|
| `NF_REALTIME_DRIVER` | Le transport du bus entre exemplaires |
|
|
132
|
-
| `NF_REALTIME_BACKPLANE_SECRET` | Scelle les enveloppes
|
|
167
|
+
| `NF_REALTIME_BACKPLANE_SECRET` | Scelle les enveloppes du bus — moins de 32 caractères arrête le démarrage |
|
|
133
168
|
| `NF_REALTIME_BACKPLANE_NAMESPACE` | Cloisonne deux applications qui partagent le même bus |
|
|
134
169
|
|
|
135
170
|
> 🔴 **Deux applications sur un même cache, sans espace de noms distinct, se parlent.** Le symptôme
|
|
@@ -137,10 +172,10 @@ Tous en `_FILE` en production. Détail et rotation → [`secrets.md`](secrets.md
|
|
|
137
172
|
|
|
138
173
|
### Multi-process sur une seule machine
|
|
139
174
|
|
|
140
|
-
| Variable | Rôle
|
|
141
|
-
| ------------ |
|
|
142
|
-
| `NF_CLUSTER` |
|
|
143
|
-
| `NF_WORKERS` | Nombre d'exécutants — sinon dérivé des limites du cgroup
|
|
175
|
+
| Variable | Rôle |
|
|
176
|
+
| ------------ | ------------------------------------------------------------------------------ |
|
|
177
|
+
| `NF_CLUSTER` | Posée par `nodefony cluster` dans chaque worker — **ne pas la poser soi-même** |
|
|
178
|
+
| `NF_WORKERS` | Nombre d'exécutants — sinon dérivé des limites du cgroup |
|
|
144
179
|
|
|
145
180
|
En Kubernetes, **laisse ça tranquille** : un exemplaire = un processus = un pod, et c'est
|
|
146
181
|
l'ordonnanceur qui multiplie. Le mode multi-process sert sur une machine nue.
|
|
@@ -259,10 +259,13 @@ neuve ne prouve RIEN de tout ça — elle est vide.
|
|
|
259
259
|
```bash
|
|
260
260
|
# sqlite : une copie du fichier. Le chemin par défaut du connecteur `default` est
|
|
261
261
|
# `var/databases/nodefony-drizzle.db` — `orm:migrate:status --json` le confirme.
|
|
262
|
-
|
|
262
|
+
# La copie va sous `tmp/scratch/` de l'application, pas dans le /tmp du système :
|
|
263
|
+
# c'est un résultat qu'on relit, et il doit disparaître avec le reste de `tmp/`.
|
|
264
|
+
mkdir -p tmp/scratch
|
|
265
|
+
cp var/databases/nodefony-drizzle.db tmp/scratch/essai.sqlite
|
|
263
266
|
# PostgreSQL : CREATE DATABASE app_essai TEMPLATE app; (ou une restauration de sauvegarde)
|
|
264
267
|
|
|
265
|
-
export NF_MIGRATE_DATABASE_URL="sqlite
|
|
268
|
+
export NF_MIGRATE_DATABASE_URL="sqlite:tmp/scratch/essai.sqlite"
|
|
266
269
|
npx nodefony orm:migrate --json
|
|
267
270
|
```
|
|
268
271
|
|
|
@@ -113,25 +113,26 @@ 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
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
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 |
|
|
135
136
|
|
|
136
137
|
## Un contrat qui ne bougera pas
|
|
137
138
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodefony-protect-route
|
|
3
3
|
description: >
|
|
4
|
-
Réserve une route d'une application Nodefony aux personnes habilitées, par les
|
|
5
|
-
framework plutôt que par un contrôle écrit
|
|
6
|
-
(zone du pare-feu et garde par route), la hiérarchie de rôles
|
|
7
|
-
|
|
8
|
-
et les gestes qui affaiblissent l'application en
|
|
9
|
-
d'ouvrir une route à un tiers, ou de toucher à la
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"un partenaire doit pouvoir poster", "erreur CSRF",
|
|
13
|
-
"@
|
|
14
|
-
"
|
|
4
|
+
Réserve une route ou une page d'une application Nodefony aux personnes habilitées, par les
|
|
5
|
+
briques du framework plutôt que par un contrôle écrit dans l'action. Porte les deux étages
|
|
6
|
+
(zone du pare-feu et garde par route), la hiérarchie de rôles, le renvoi de l'anonyme vers la
|
|
7
|
+
page de connexion, l'ouverture à un partenaire sans démonter la défense anti-falsification,
|
|
8
|
+
les zones des programmes (clé d'API, jeton), et les gestes qui affaiblissent l'application en
|
|
9
|
+
silence. À charger AVANT de poser une garde, d'ouvrir une route à un tiers, ou de toucher à la
|
|
10
|
+
sécurité. Se CONNECTER (page, second facteur, retour après connexion) → nodefony-authentication.
|
|
11
|
+
Déclencheurs : "protège cette route", "réserver aux administrateurs", "@IsGranted",
|
|
12
|
+
"firewall", "403", "401", "roleHierarchy", "un partenaire doit pouvoir poster", "erreur CSRF",
|
|
13
|
+
"@CsrfExempt", "mon POST passe sans jeton", "API pour un programme", "clé d'API",
|
|
14
|
+
"client_credentials", "désactiver la sécurité pour tester".
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
# protect-route — la garde vient du framework, jamais de l'action
|
|
@@ -85,6 +85,48 @@ pattern: "^/api/account/(profile|invoices)"; // ❌ les routes d'aujourd'hui
|
|
|
85
85
|
elle a l'air de couvrir l'espace, et l'introspection montre bien une route protégée à côté. Quand
|
|
86
86
|
des routes partagent un préfixe, ne les protège pas une par une.
|
|
87
87
|
|
|
88
|
+
## Protéger des PAGES — renvoyer l'anonyme vers la connexion
|
|
89
|
+
|
|
90
|
+
🔴 **La connexion elle-même — page `/login`, son habillage, le client `NodefonyLogin`, le second
|
|
91
|
+
facteur, le retour après connexion, les fournisseurs — est le skill `nodefony-authentication`.**
|
|
92
|
+
N'écris ni page de connexion ni controller de compte : tout est servi. Ici, seulement ce que la
|
|
93
|
+
ZONE décide.
|
|
94
|
+
|
|
95
|
+
**Conduire l'anonyme à la page** se déclare sur la ZONE, jamais dans l'action :
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
areas: {
|
|
99
|
+
pages: {
|
|
100
|
+
pattern: "^/compte(/|$)",
|
|
101
|
+
authenticators: ["session"],
|
|
102
|
+
entryPoint: "/login", // la page du framework (security.loginPage.path)
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| La requête refusée | Réponse |
|
|
108
|
+
| -------------------------------------------------------------------------- | ------------------------------- |
|
|
109
|
+
| navigation (`GET`/`HEAD`, `Accept: text/html`, `Sec-Fetch-Mode: navigate`) | `302` → `/login?from=%2Fcompte` |
|
|
110
|
+
| appel d'API (`Accept: application/json`, `fetch()`) | `401`, inchangé |
|
|
111
|
+
|
|
112
|
+
**L'éprouver** : la navigation se joue avec `navigate()` de `nodefony/testing`, JAMAIS avec
|
|
113
|
+
`fetch()` — Node y réécrit `Sec-Fetch-Mode` en `cors`, le pare-feu voit un appel d'API et répond
|
|
114
|
+
`401` sur un code juste. `fetch()` reste l'outil du côté API.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { navigate } from "nodefony/testing";
|
|
118
|
+
import { appBaseUrl } from "./e2e.setup"; // tests/e2e.setup.ts, généré
|
|
119
|
+
|
|
120
|
+
const res = await navigate(`${appBaseUrl()}/compte`); // 302 → /login?from=%2Fcompte
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Le `from` qui revient après la connexion ne se suit JAMAIS sans `safeRedirectPath` — skill
|
|
124
|
+
`nodefony-authentication`.
|
|
125
|
+
|
|
126
|
+
**Une zone `session` sans `roles`, avec un fournisseur qui crée les comptes à la première
|
|
127
|
+
connexion** (`allowSignup`), est ouverte à quiconque possède un compte chez ce fournisseur.
|
|
128
|
+
`npx nodefony doctor --live` la signale ; pose un rôle sur la zone.
|
|
129
|
+
|
|
88
130
|
## 🔴 La PROVENANCE n'est pas une PREUVE D'INTENTION — une mutation exige `@CsrfProtect`
|
|
89
131
|
|
|
90
132
|
Le raisonnement qui vient, et qui est faux : « le pare-feu vérifie déjà `Sec-Fetch-Site`, donc une
|
|
@@ -93,10 +135,12 @@ n'en envoie aucun, et la défense de provenance le laisse alors passer — c'est
|
|
|
93
135
|
distingue les sites, pas les intentions. Résultat mesuré : un `POST /api/cart/items` sans jeton rend
|
|
94
136
|
`201`, et l'application croit avoir une défense.
|
|
95
137
|
|
|
96
|
-
Toute action qui ÉCRIT porte donc `@CsrfProtect` explicitement.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
Toute action qui ÉCRIT porte donc `@CsrfProtect` explicitement. N'écris AUCUNE route pour distribuer
|
|
139
|
+
le jeton : `GET /nodefony/security/api/auth/me` le sème déjà (cookie lisible `csrf-token`), comme
|
|
140
|
+
toute requête sûre vers une route décorée — et il vaut pour TOUTES les routes protégées. La
|
|
141
|
+
mutation le rejoue dans l'en-tête `x-csrf-token` — c'est le double-submit, sinon `403`. Un test le
|
|
142
|
+
récupère par `csrfHeaders(base, cookie)` de `nodefony/testing`. La provenance et le jeton se
|
|
143
|
+
**cumulent** ; l'une ne remplace jamais l'autre.
|
|
100
144
|
|
|
101
145
|
## 🔴 Ce qu'il ne faut jamais écrire
|
|
102
146
|
|
|
@@ -171,9 +215,16 @@ connecter — ou pire, un mot de passe stocké en clair.
|
|
|
171
215
|
|
|
172
216
|
## Lire l'utilisateur courant
|
|
173
217
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
Dans un controller, **`this.user`** : l'utilisateur CONNECTÉ, ou `null` (anonyme, ou route hors
|
|
219
|
+
de toute zone). Le paramètre décoré `@CurrentUser()` rend le même, `undefined` pour l'anonyme.
|
|
220
|
+
Hors controller (un service), `RequestContext.getAuthenticatedUser()` — `null` hors requête ou
|
|
221
|
+
pour l'anonyme — et `RequestContext.getUserId()` (`string | null`). `RequestContext.getUser()`
|
|
222
|
+
rend l'utilisateur BRUT, anonyme compris : ne t'en sers pas pour décider « qui est connecté ».
|
|
223
|
+
|
|
224
|
+
L'identité est **ré-résolue à chaque requête** : les rôles sont frais, et une révocation prend
|
|
225
|
+
effet tout de suite. N'écris pas ton propre lecteur de session — le tien lira un instantané. Une
|
|
226
|
+
page PUBLIQUE qui veut savoir qui la visite se place sous une zone
|
|
227
|
+
`authenticators: ["session", "anonymous"]` : hors zone, `this.user` vaut toujours `null`.
|
|
177
228
|
|
|
178
229
|
## Un droit métier qui ne se réduit pas à un rôle
|
|
179
230
|
|
|
@@ -190,8 +241,16 @@ d'appartenance à écrire dans l'action.
|
|
|
190
241
|
|
|
191
242
|
## Une API pour un PROGRAMME, pas pour un navigateur
|
|
192
243
|
|
|
193
|
-
Un service partenaire, un script, un agent ne stockent aucun cookie.
|
|
194
|
-
|
|
244
|
+
Un service partenaire, un script, un agent ne stockent aucun cookie. Le
|
|
245
|
+
`nodefony/config/security.ts` généré (preset `complete`) pose **deux zones, une par sorte de
|
|
246
|
+
preuve** — choisis selon ce que l'appelant détient, n'en ajoute pas une troisième :
|
|
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 |
|
|
252
|
+
|
|
253
|
+
### Clé d'API : la zone `machine`
|
|
195
254
|
|
|
196
255
|
```ts
|
|
197
256
|
machine: {
|
|
@@ -216,6 +275,48 @@ qui ne stocke pas de cookie ne doit rien recevoir qu'il faille stocker.**
|
|
|
216
275
|
|
|
217
276
|
Les clés s'émettent par `POST /nodefony/security/api/keys`.
|
|
218
277
|
|
|
278
|
+
### Jeton d'accès : la zone `partner`, et la commande qui l'éprouve
|
|
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
|
|
288
|
+
```
|
|
289
|
+
|
|
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é.
|
|
294
|
+
|
|
295
|
+
🔴 **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 :
|
|
299
|
+
|
|
300
|
+
| Oublié | Symptôme |
|
|
301
|
+
| ----------------------------------------- | ------------------------------------------------------------- |
|
|
302
|
+
| la ressource dans `jwt.audiences` | **400 `invalid_target`** à l'émission du jeton de l'app |
|
|
303
|
+
| `security:keycloak:realm --write` relancé | **401** du jeton Keycloak : le realm n'inscrit pas l'audience |
|
|
304
|
+
| la `resource` sur la zone | l'application **refuse de démarrer** en nommant la zone |
|
|
305
|
+
|
|
306
|
+
🔴 **`subjectPolicy: "ephemeral"` s'écrit SUR LA ZONE, jamais dans `resourceServer`.** Le client
|
|
307
|
+
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.
|
|
319
|
+
|
|
219
320
|
## Les gestes qui affaiblissent en silence
|
|
220
321
|
|
|
221
322
|
Bloqué par une garde en résolvant autre chose, le réflexe est de la retirer. La fonctionnalité
|
|
@@ -244,7 +345,7 @@ Le refus d'un anonyme est gratuit : n'importe quelle zone le donne. Ce qui prouv
|
|
|
244
345
|
|
|
245
346
|
```bash
|
|
246
347
|
npx nodefony security:user:add # un témoin SANS le rôle
|
|
247
|
-
# 1. anonyme → refusé (401 ou 403
|
|
348
|
+
# 1. anonyme → refusé (401 ou 403 ; 302 vers /login si la zone a un entryPoint)
|
|
248
349
|
# 2. connecté SANS rôle → refusé ← celui-ci porte l'information
|
|
249
350
|
# 3. administrateur → servi
|
|
250
351
|
npx nodefony inspect routes --json # la garde est-elle sur la route qu'on croit ?
|
|
@@ -256,6 +357,7 @@ Chaque geste ci-dessus a sa référence INSTALLÉE — une recherche ordinaire n
|
|
|
256
357
|
ne descendant pas dans `node_modules` :
|
|
257
358
|
|
|
258
359
|
- **Zones, authentificateurs, CSRF, CORS, clés d'API** — `node_modules/@nodefony/security/docs/firewall.md`
|
|
360
|
+
- **Jeton d'un tiers, `subjectPolicy`, `client_credentials`** — `node_modules/@nodefony/security/docs/external-jwt.md`
|
|
259
361
|
- **`@IsGranted`, voters, hiérarchie de rôles** — `node_modules/@nodefony/security/docs/authorization.md`
|
|
260
362
|
- **Contrat `IUser`, `UserService`, mot de passe** — `node_modules/@nodefony/user/docs/index.md`
|
|
261
363
|
- **Politique de contenu, nonce, HSTS** — `node_modules/@nodefony/security/docs/headers.md`
|
|
@@ -271,3 +373,4 @@ framework.
|
|
|
271
373
|
| Une ressource complète stockée | `nodefony-add-crud` |
|
|
272
374
|
| Un service métier injectable | `nodefony-add-service` |
|
|
273
375
|
| Un flux temps réel réservé | `nodefony-add-realtime-channel` |
|
|
376
|
+
| Se connecter, second facteur | `nodefony-authentication` |
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodefony-security-review
|
|
3
|
+
description: >
|
|
4
|
+
Relit la sécurité du code d'une application Nodefony avant le commit : un script sur le diff,
|
|
5
|
+
puis une grille de jugement. Le script nomme, fichier et ligne, ce
|
|
6
|
+
qu'un motif suffit à voir — route ouverte à l'anonyme ou hors de toute
|
|
7
|
+
zone du pare-feu, mutation sans jeton anti-falsification, SQL interpolé, secret écrit ou
|
|
8
|
+
journalisé, identité brute qui inclut l'anonyme, garde éteinte, redirection ouverte — avec le
|
|
9
|
+
geste qui corrige et le skill qui le porte ; la grille couvre le droit sur l'objet, les champs
|
|
10
|
+
renvoyés, les entrées non bornées. À charger AVANT de commiter une route,
|
|
11
|
+
une requête, un service qui touche des données ou de la configuration de sécurité. Écrire la
|
|
12
|
+
garde elle-même → `nodefony-protect-route`.
|
|
13
|
+
Déclencheurs : "c'est sûr ?", "est-ce sécurisé ?", "relis la sécurité avant que je commite", "ma route est-elle
|
|
14
|
+
protégée ?", "revue de sécurité", "relecture de sécurité", "audit de sécurité de mon code",
|
|
15
|
+
"y a-t-il une faille", "injection SQL", "secret dans le code", "avant de commiter".
|
|
16
|
+
metadata:
|
|
17
|
+
version: 1.0.0
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# nodefony-security-review — relire avant de commiter, faute par faute
|
|
21
|
+
|
|
22
|
+
> **Maintenance** : vérité courante, jamais un journal. Éditer en place ; l'historique vit dans
|
|
23
|
+
> `git log`. Pas de `references/`, comme `nodefony-dev` et pour la même raison mesurée : une page
|
|
24
|
+
> de `references/` n'est presque jamais ouverte par un agent, un script cité comme commande l'est.
|
|
25
|
+
> Ce qui doit atteindre un agent s'EXÉCUTE ou vit dans ce corps.
|
|
26
|
+
|
|
27
|
+
## Passer la main
|
|
28
|
+
|
|
29
|
+
| Ce que tu fais en réalité | Le skill |
|
|
30
|
+
| ----------------------------------------------------------------------- | ------------------------- |
|
|
31
|
+
| Tu poses la garde : zone, rôle, jeton anti-falsification, voter | `nodefony-protect-route` |
|
|
32
|
+
| Tu touches à la connexion : identité, second facteur, jeton d'un script | `nodefony-authentication` |
|
|
33
|
+
| Tu codes la fonctionnalité elle-même | `nodefony-dev` |
|
|
34
|
+
| Tu sors un secret du source : `env.ts`, `.env`, `npx nodefony env` | `nodefony-dev` |
|
|
35
|
+
| Quelque chose qui marchait ne marche plus | `nodefony-troubleshoot` |
|
|
36
|
+
| Tu prépares l'image, les secrets du déploiement, le proxy TLS | `nodefony-devops` |
|
|
37
|
+
|
|
38
|
+
## 1. La règle — nommer, trancher, prouver
|
|
39
|
+
|
|
40
|
+
**Une relecture NOMME chaque point**, avec son fichier et sa ligne : « ça me paraît sûr » n'est
|
|
41
|
+
pas une relecture. Puis elle **tranche** chacun : corrigé, ou justifié par écrit (« cette route est
|
|
42
|
+
publique : c'est la page d'accueil »). Un point ni corrigé ni justifié reste ouvert, et se dit.
|
|
43
|
+
|
|
44
|
+
**Ne corrige jamais en affaiblissant.** Retirer une garde qui gêne, couper le jeton
|
|
45
|
+
anti-falsification, élargir une zone à l'anonyme, déplacer une URL sous une autre zone : le diff
|
|
46
|
+
redevient silencieux et la faille est plus grande qu'avant. Une URL déjà publiée ne se déplace pas
|
|
47
|
+
— on adapte la ZONE à l'URL (skill `nodefony-protect-route`).
|
|
48
|
+
|
|
49
|
+
## 2. Le script — ce qu'un motif suffit à voir
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node node_modules/@nodefony/devkit/skills/nodefony-security-review/scripts/review-diff.mjs
|
|
53
|
+
node node_modules/@nodefony/devkit/skills/nodefony-security-review/scripts/review-diff.mjs --base main
|
|
54
|
+
node node_modules/@nodefony/devkit/skills/nodefony-security-review/scripts/review-diff.mjs --staged
|
|
55
|
+
node node_modules/@nodefony/devkit/skills/nodefony-security-review/scripts/review-diff.mjs --json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Sans option, il relit le **travail en cours** (non commité, fichiers non suivis compris) ; s'il n'y
|
|
59
|
+
en a pas, le **dernier commit** — la première ligne de sa sortie dit toujours ce qu'il a relu.
|
|
60
|
+
`--base main` relit toute une branche. Sorties : `0` rien à signaler · `1` au moins un point ·
|
|
61
|
+
`64` usage · `66` aucun diff lisible (pas un dépôt git : passe un patch avec `--diff`).
|
|
62
|
+
|
|
63
|
+
Chaque point sort avec son niveau, `fichier:ligne`, la raison et le geste :
|
|
64
|
+
|
|
65
|
+
- **`FAUTE`** — à corriger. Une faute qu'on garde exige une raison écrite, et elle est rare.
|
|
66
|
+
- **`À JUSTIFIER`** — légitime parfois, jamais par accident : une route publique voulue, une
|
|
67
|
+
variable lue hors de `env.ts` pour une bonne raison. Corrige, ou écris la raison.
|
|
68
|
+
|
|
69
|
+
Un secret signalé est **masqué** dans l'extrait (quatre caractères, puis `…`). Ne le recopie pas
|
|
70
|
+
non plus dans ta relecture : nomme sa constante et sa ligne. Une valeur citée dans un compte rendu
|
|
71
|
+
versionné est publiée une fois de plus.
|
|
72
|
+
|
|
73
|
+
⚠️ **Il croise les routes NOUVELLES avec les zones du pare-feu** lues dans `nodefony/config/` — ce
|
|
74
|
+
que ni `doctor` ni `inspect` ne font. Dans une application générée, la zone `^/api` laisse passer
|
|
75
|
+
l'anonyme : une route ajoutée sous `/api` sans `@IsGranted` répond à n'importe qui, et rien
|
|
76
|
+
d'autre ne le dit. Il ne relit pas les tests (leurs secrets en dur sont des fixtures).
|
|
77
|
+
|
|
78
|
+
⚠️ **Un outil MCP échappe aux zones.** Ce qu'un module publie par `getMcpTools()` est servi par
|
|
79
|
+
`/nodefony/mcp`, qu'aucune zone du pare-feu ne couvre : tant que `mcp.authorization` n'est pas
|
|
80
|
+
déclaré, seules l'adresse locale et l'origine le gardent. Un outil qui rend des données de compte
|
|
81
|
+
ou qui ÉCRIT se relit comme une route ouverte.
|
|
82
|
+
|
|
83
|
+
## 3. La grille — ce qu'on cherche, ce qui le montre, ce qui le corrige
|
|
84
|
+
|
|
85
|
+
| Ce qu'on cherche | Ce qui le montre | Le geste juste |
|
|
86
|
+
| ------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
87
|
+
| Une route nouvelle que l'anonyme atteint (zone `anonymous`, ou hors zone) | le script (`route-open`) | `@IsGranted("ROLE_…")`, ou une zone qui exige une session → `nodefony-protect-route` |
|
|
88
|
+
| Une mutation sans `@CsrfProtect` sous une zone à session | le script | `@CsrfProtect()` ; le client sème le jeton par un `GET` → `nodefony-protect-route` |
|
|
89
|
+
| « Connecté ? » testé sur l'utilisateur brut | le script (`raw-identity`) | `this.user` dans un controller → `nodefony-authentication` |
|
|
90
|
+
| Une donnée reçue dans du SQL brut | le script | le gabarit `` sql`… ${valeur}` `` de drizzle lie la valeur ; jamais `sql.raw` sur une entrée |
|
|
91
|
+
| Un secret dans le source, un `.env` dans le diff | le script, puis `npx nodefony env` | variable `NF_…` déclarée dans `env.ts`, valeur dans le `.env` du poste, jamais commité ; `npx nodefony env` AVANT et APRÈS prouve qu'elle est lue → `nodefony-dev` |
|
|
92
|
+
| Un mot de passe, un jeton, un cookie dans le journal | le script | journaliser l'identifiant de l'objet, jamais la valeur |
|
|
93
|
+
| Une garde éteinte (`rateLimit`, `unsafe-inline`, secret en clair) | le script | régler un seuil, jamais éteindre → `nodefony-protect-route` |
|
|
94
|
+
| Une redirection vers une adresse reçue | le script | `safeRedirectPath(valeur)`, importé de `nodefony` |
|
|
95
|
+
| Un chemin de fichier, une commande composés avec une entrée | le script | vérifier que le chemin reste sous sa racine ; `execFile` avec un tableau d'arguments, sans shell |
|
|
96
|
+
| Un outil MCP publié (`getMcpTools()`), une porte MCP ouverte au réseau | le script | lecture seule et rien qu'on ne lirait sans compte ; sinon `scopes` + `mcp.authorization` (`node_modules/@nodefony/devkit/docs/index.md`) |
|
|
97
|
+
| Ce qui était DÉJÀ ouvert avant ce diff (`@Anonymous`, zone publique) | `npx nodefony doctor` | famille « Surface ouverte » : l'inventaire, à relire |
|
|
98
|
+
| Un secret suivi par git, une variable non déclarée | `npx nodefony doctor` | familles « Secrets hors de git » et « Variables déclarées » |
|
|
99
|
+
| Une dépendance à vulnérabilité connue | `npm audit --omit=dev` | monter la version corrigée ; jamais `--force` sans lire ce qu'il casse |
|
|
100
|
+
|
|
101
|
+
## 4. Ce que seul un jugement tranche
|
|
102
|
+
|
|
103
|
+
Le script ne voit pas l'intention. Relis ces trois points toi-même, sur chaque route touchée :
|
|
104
|
+
|
|
105
|
+
1. **Le droit sur l'OBJET.** Un rôle dit « peut éditer des documents », pas « peut éditer CE
|
|
106
|
+
document ». Une route qui lit un identifiant dans l'URL (`/api/documents/:id`) et renvoie
|
|
107
|
+
l'enregistrement sans vérifier qu'il appartient à l'appelant laisse chacun lire les données des
|
|
108
|
+
autres en changeant un nombre. Le geste : un voter, `@IsGranted("doc.edit", { subject: "id" })`
|
|
109
|
+
→ `nodefony-protect-route`.
|
|
110
|
+
2. **Les champs RENVOYÉS.** Rendre l'entité entière (`this.renderJson(user)`) envoie aussi ce
|
|
111
|
+
qu'on n'a pas choisi — l'empreinte du mot de passe (colonne `password` de `User`), les
|
|
112
|
+
métadonnées internes, les rôles d'un autre compte.
|
|
113
|
+
Rends un objet construit champ par champ.
|
|
114
|
+
3. **Les entrées NON BORNÉES.** Une liste sans plafond, une taille de page lue telle quelle, un
|
|
115
|
+
fichier reçu sans limite de taille : une seule requête suffit à épuiser le serveur. Plafonne
|
|
116
|
+
(`Math.min(limite, 100)`), et refuse ce qui dépasse plutôt que de le traiter.
|
|
117
|
+
|
|
118
|
+
## 5. Avant de dire « relu »
|
|
119
|
+
|
|
120
|
+
1. **Le script est relancé APRÈS les corrections** : il rend `0`, ou chaque point restant porte sa
|
|
121
|
+
justification écrite dans ta réponse.
|
|
122
|
+
2. **`npx nodefony doctor`** ne signale rien de neuf.
|
|
123
|
+
3. **Une garde ajoutée est PROUVÉE** par trois appels — anonyme refusé, compte sans le rôle refusé,
|
|
124
|
+
compte avec le rôle servi (skill `nodefony-protect-route`). Le refus de l'anonyme seul ne prouve
|
|
125
|
+
rien : n'importe quelle zone le donne.
|
|
126
|
+
4. **Nomme ce que tu n'as pas relu** : `npm audit` non lancé, une route dont tu ne sais pas si elle
|
|
127
|
+
doit être publique. Le dire coûte une ligne ; le taire, une faille.
|