@nodefony/devkit 10.0.0-alpha.6 → 10.0.0-alpha.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 (24) 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/package.json +8 -8
  9. package/skills/nodefony-add-crud/SKILL.md +9 -0
  10. package/skills/nodefony-add-realtime-channel/SKILL.md +9 -0
  11. package/skills/nodefony-add-service/SKILL.md +9 -0
  12. package/skills/nodefony-browser/SKILL.md +19 -16
  13. package/skills/nodefony-dev/SKILL.md +273 -0
  14. package/skills/nodefony-dev/scripts/docs.mjs +544 -0
  15. package/skills/nodefony-devops/SKILL.md +198 -0
  16. package/skills/nodefony-devops/references/compose.md +125 -0
  17. package/skills/nodefony-devops/references/frontal.md +119 -0
  18. package/skills/nodefony-devops/references/image.md +136 -0
  19. package/skills/nodefony-devops/references/kubernetes.md +189 -0
  20. package/skills/nodefony-devops/references/podman.md +103 -0
  21. package/skills/nodefony-devops/references/secrets.md +108 -0
  22. package/skills/nodefony-devops/references/variables.md +174 -0
  23. package/skills/nodefony-migrate-schema/SKILL.md +29 -17
  24. package/skills/nodefony-protect-route/SKILL.md +9 -0
@@ -0,0 +1,189 @@
1
+ # Kubernetes — ton Deployment
2
+
3
+ > **Maintenance** : vérité courante. Éditer en place.
4
+
5
+ ## 1. Ce qui est rendu, et ce qui ne l'est pas
6
+
7
+ **Rendu à ton nom** : `deploy/migrate-job.yaml`, le travail qui joue les migrations. Son mode
8
+ d'emploi est en tête du fichier.
9
+
10
+ **Pas rendu** : le Deployment, le Service, l'Ingress, les Secret et ConfigMap. Tu les écris. Cette
11
+ page dit quoi y mettre ; le guide `kubernetes.md` du framework en donne un exemple complet.
12
+
13
+ ```bash
14
+ kubectl apply -f deploy/migrate-job.yaml # les migrations AVANT les nouveaux exemplaires
15
+ kubectl wait --for=condition=complete job/<nom> --timeout=300s
16
+ kubectl apply -f <ton-deployment>.yaml
17
+ ```
18
+
19
+ L'ordre n'est pas négociable : un exemplaire neuf face à un schéma ancien écrit dans des colonnes
20
+ qui n'existent pas.
21
+
22
+ ## 2. Le contexte de sécurité — politique Restricted
23
+
24
+ **Commence par lire `deploy/migrate-job.yaml` : son contexte de sécurité est déjà conforme.** Il
25
+ est le seul modèle juste que tu aies sous la main, et il a été écrit pour être recopié.
26
+
27
+ Six exigences, toutes portées par le **manifeste** — aucune n'est « héritée de l'image ». Ce que
28
+ l'image apporte, c'est de les rendre satisfaisables sans contorsion : elle déclare
29
+ `USER 1000:1000`, donc l'identifiant numérique existe ; elle n'écoute qu'au-dessus de 1024, donc
30
+ aucune capacité n'est nécessaire. Cinq ne coûtent rien à poser. **La sixième —
31
+ `readOnlyRootFilesystem` — demande les deux volumes**, et c'est la seule qui se travaille.
32
+
33
+ ```yaml
34
+ spec:
35
+ securityContext:
36
+ runAsNonRoot: true
37
+ runAsUser: 1000
38
+ runAsGroup: 1000
39
+ fsGroup: 1000
40
+ fsGroupChangePolicy: OnRootMismatch # c'est LUI qui donne le volume au bon porteur
41
+ seccompProfile: { type: RuntimeDefault }
42
+ containers:
43
+ - name: <app>
44
+ securityContext:
45
+ allowPrivilegeEscalation: false
46
+ readOnlyRootFilesystem: true
47
+ capabilities: { drop: ["ALL"] }
48
+ volumeMounts:
49
+ - { name: tmp, mountPath: /app/tmp }
50
+ - { name: var, mountPath: /app/var }
51
+ volumes:
52
+ - { name: tmp, emptyDir: { sizeLimit: 256Mi } }
53
+ - { name: var, emptyDir: {} }
54
+ ```
55
+
56
+ - **`seccompProfile`** : la politique traite l'**absence** de profil comme une violation, pas comme
57
+ un défaut permissif. Aucun coût : rien ici n'appelle de fonction système exotique.
58
+ - **`readOnlyRootFilesystem: true`** : possible **uniquement** avec les deux volumes ci-dessus.
59
+ Sans eux, le conteneur ne démarre pas — l'application crée `tmp/` et `var/` au démarrage.
60
+
61
+ > ⚠️ **Un volume éphémère en mémoire compte dans la limite mémoire du conteneur.** Laisse
62
+ > `emptyDir: {}` sur disque, et pose une `sizeLimit` — un dossier temporaire sans borne est un
63
+ > moyen de faire évincer ton propre pod.
64
+
65
+ **Le rendre opposable, sinon ça ne prouve rien :**
66
+
67
+ ```bash
68
+ kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted
69
+ kubectl apply -f <ton-deployment>.yaml # doit passer SANS avertissement
70
+ ```
71
+
72
+ Puis **débranche** : retire un `emptyDir`, réapplique. Le pod ne doit plus démarrer. Une politique
73
+ qu'on n'a jamais vue refuser quelque chose n'est pas une politique.
74
+
75
+ ## 3. Les sondes
76
+
77
+ ```yaml
78
+ startupProbe:
79
+ {
80
+ httpGet: { path: /readyz, port: 5151 },
81
+ periodSeconds: 2,
82
+ failureThreshold: 30,
83
+ }
84
+ readinessProbe: { httpGet: { path: /readyz, port: 5151 }, periodSeconds: 5 }
85
+ livenessProbe: { httpGet: { path: /livez, port: 5151 }, periodSeconds: 10 }
86
+ ```
87
+
88
+ - **Pas de sonde de démarrage dédiée côté application** : `/readyz` en fait office. Dimensionne
89
+ `failureThreshold × periodSeconds` sur le **pire** temps de démarrage, pas le meilleur — c'est la
90
+ cause n° 1 des pods qui redémarrent en boucle avec des journaux muets.
91
+ - **`livenessProbe` sur `/livez`, jamais sur `/readyz`.** `/livez` reste à `200` pendant le drain :
92
+ un exemplaire qui draine est vivant. Sonder `/readyz` en vivacité le ferait **tuer** au milieu de
93
+ son arrêt propre — exactement ce qu'on cherche à éviter.
94
+ - Les deux répondent **avant la limitation de débit** : un kubelet limité recevrait `429` et
95
+ conclurait à un échec.
96
+
97
+ ## 4. L'arrêt — ne perdre aucune requête
98
+
99
+ ```yaml
100
+ terminationGracePeriodSeconds: 30
101
+ lifecycle: { preStop: { sleep: { seconds: 2 } } }
102
+ ```
103
+
104
+ La chronologie réelle :
105
+
106
+ ```
107
+ SIGTERM ──► /readyz passe à 503 ──► le répartiteur retire le pod ──► drain ≤ 15 s ──► sortie 0
108
+ │ │
109
+ └── preStop : le temps que les points d'accès se propagent ────────────────┘
110
+ fenêtre de grâce : 30 s
111
+ ```
112
+
113
+ **La fenêtre doit être strictement supérieure au drain** (15 s). À l'expiration, le kubelet tue —
114
+ code **137**, requêtes en vol perdues, aucun message. Le `preStop` court existe parce que le
115
+ retrait d'un point d'accès n'est pas instantané : sans lui, du trafic arrive encore après le début
116
+ du drain.
117
+
118
+ **Vérifier que ça marche** — sous charge constante, `kubectl rollout restart deploy/<app>` : zéro
119
+ `5xx`. C'est la seule preuve qui compte.
120
+
121
+ ## 5. Les ressources
122
+
123
+ ```yaml
124
+ resources:
125
+ requests: { cpu: 250m, memory: 256Mi }
126
+ limits: { memory: 512Mi } # PAS de limite CPU
127
+ ```
128
+
129
+ **Pas de limite processeur.** Une limite mémoire tue proprement et le pod redémarre ; une limite
130
+ processeur ne tue pas — elle **ralentit en permanence**, et ça ne ressemble pas à une limite, ça
131
+ ressemble à une application lente. Les demandes suffisent à garantir la part.
132
+
133
+ Si tu bornes le tas de la machine virtuelle JavaScript face à une limite mémoire de cgroup,
134
+ dimensionne-le avec une mesure, pas au jugé.
135
+
136
+ ## 6. La base de données — elle n'est pas dans le cluster
137
+
138
+ C'est la différence la plus structurante avec le compose de développement.
139
+
140
+ 1. **Aucun manifeste de base n'est fourni**, et c'est voulu : en production elle est administrée.
141
+ 2. **Ne reporte jamais l'hôte du compose.** `compose.yaml` pointe un **service du compose** ; le
142
+ même nom dans un cluster désigne un Service qui n'existe pas. Rien ne casse au déploiement, tout
143
+ casse au premier accès.
144
+ 3. **L'URL passe par un Secret, en `_FILE`** — elle porte un mot de passe, elle n'a rien à faire
145
+ dans un ConfigMap.
146
+ 4. **Le plafond de connexions.** Une base administrée les compte strictement. N exemplaires × le
147
+ pool de chacun peuvent la saturer, et l'erreur sort de **son** côté, pas du tien. Reste prudent
148
+ sur le nombre d'exemplaires.
149
+
150
+ | Ta base | Exemplaires | Volume pour `var/` | Stratégie |
151
+ | ---------------------- | ---------------- | ------------------------- | --------------- |
152
+ | SQLite (fichier local) | **1, impératif** | revendication persistante | **`Recreate`** |
153
+ | Serveur (administrée) | plusieurs | `emptyDir` | `RollingUpdate` |
154
+
155
+ > 🔴 **SQLite impose `strategy: Recreate`.** Un déploiement progressif sur un volume monté par un
156
+ > seul nœud attend un disque que l'exemplaire précédent tient encore : le nouveau pod reste en
157
+ > attente **pour toujours**, sans que rien ne nomme la cause.
158
+
159
+ ## 7. Les variables du manifeste
160
+
161
+ ```yaml
162
+ env:
163
+ - { name: NODE_ENV, value: production }
164
+ - { name: NF_POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } }
165
+ - { name: NF__HTTP__TRUSTPROXY, value: uniquelocal }
166
+ - { name: NF_CSRF_SECRET_FILE, value: /run/secrets/csrf }
167
+ - { name: NF_DATABASE_URL_FILE, value: /run/secrets/db-url }
168
+ ```
169
+
170
+ `NF_POD_NAME` vient de l'API descendante. Elle dérive l'identité d'origine du bus temps réel : sans
171
+ elle, plusieurs exemplaires se confondent et les messages s'écoutent eux-mêmes.
172
+
173
+ `NF__HTTP__TRUSTPROXY` est **obligatoire** dès qu'un Ingress termine le TLS —
174
+ [`frontal.md`](frontal.md) dit ce qui tombe sans lui, et comment le voir tomber.
175
+
176
+ ## 8. Éprouver sans cluster de production
177
+
178
+ ```bash
179
+ kind create cluster --name <app>
180
+ kubectl label ns default pod-security.kubernetes.io/enforce=restricted
181
+ kubectl apply -f deploy/migrate-job.yaml
182
+ kubectl apply -f <ton-deployment>.yaml
183
+ kubectl rollout status deploy/<app>
184
+ kind delete cluster --name <app>
185
+ ```
186
+
187
+ `podman kube play` joue les mêmes fichiers **sans cluster du tout** — mais il ne rejoue ni
188
+ l'admission de la politique, ni l'Ingress, ni un déploiement progressif. Ce qu'il ne prouve pas
189
+ doit être dit, pas supposé prouvé.
@@ -0,0 +1,103 @@
1
+ # Podman — les six écarts avec Docker
2
+
3
+ > **Maintenance** : vérité courante. Éditer en place.
4
+
5
+ Podman fait tourner cette image. Six choses changent, dont **une qui ne se répare pas** — et c'est
6
+ celle-là qu'il faut connaître avant de mettre une application derrière un frontal.
7
+
8
+ ## 1. Le contrôle de santé disparaît
9
+
10
+ Podman construit en format OCI par défaut, et **le format OCI ne porte pas de `HEALTHCHECK`**.
11
+ `podman inspect` rend alors une valeur vide, et toute dépendance qui attend « ce service est sain »
12
+ ne s'arme jamais — donc l'orchestration de ton compose démarre dans le désordre, sans erreur.
13
+
14
+ ```bash
15
+ podman build --format docker -t <app> . # le garde
16
+ podman run --health-cmd '…' --health-interval 10s … # ou le poser à l'exécution
17
+ ```
18
+
19
+ ## 2. L'arrêt coupe à 10 s — sous le drain
20
+
21
+ `podman stop` attend **10 secondes** par défaut. Le drain de l'application est borné à **15**.
22
+ Chaque arrêt tue donc l'application au milieu de son drain : code **137**, requêtes en vol perdues,
23
+ aucun message.
24
+
25
+ ```bash
26
+ podman stop --time 20 <conteneur>
27
+ # ou, dans le compose : stop_grace_period: 20s
28
+ ```
29
+
30
+ C'est l'écart le plus fréquent, et celui qui ressemble le moins à un problème de conteneur.
31
+
32
+ ## 3. Un montage lié n'est pas inscriptible, en mode sans privilèges
33
+
34
+ En mode sans privilèges, l'identifiant `1000` **du conteneur** est projeté sur un identifiant
35
+ subordonné **de l'hôte** — qui n'est pas ton `1000` à toi. Un dossier de ton disque monté dans le
36
+ conteneur ne lui appartient donc pas : le premier `mkdir` de l'application échoue en `EACCES`.
37
+
38
+ Deux remèdes :
39
+
40
+ - **un volume nommé** — il hérite du porteur du dossier sous-jacent, que l'image a déjà fixé ;
41
+ - **le suffixe `:U`** sur le montage, qui recale le porteur.
42
+
43
+ ## 4. Conserver l'identité de l'hôte écrase le `USER` de l'image
44
+
45
+ `--userns=keep-id` **nu** fait tourner le processus sous l'identifiant de l'hôte, et non sous le
46
+ `1000` que l'image déclare. Si ton identifiant d'hôte n'est pas `1000`, les dossiers `/app/tmp` et
47
+ `/app/var` — qui appartiennent à `1000` — lui refusent l'écriture, et le démarrage meurt.
48
+
49
+ ```bash
50
+ podman run --userns=keep-id:uid=1000,gid=1000 …
51
+ ```
52
+
53
+ ## 5. SELinux refuse les montages liés
54
+
55
+ Sur une distribution avec SELinux en vigueur, un montage lié est refusé (`permission denied`) tant
56
+ qu'il ne porte pas de suffixe de réétiquetage :
57
+
58
+ ```bash
59
+ -v ./docker/db:/docker-entrypoint-initdb.d:ro,z
60
+ ```
61
+
62
+ Le `z` partage l'étiquette entre conteneurs, le `Z` la rend exclusive. Sur un dossier partagé par
63
+ plusieurs services, c'est `z`.
64
+
65
+ ## 6. 🔴 L'adresse du client est perdue — et ça ne se répare pas
66
+
67
+ En mode sans privilèges, la redirection de ports passe par un composant qui **ne préserve pas
68
+ l'adresse source**. Le trafic arrive donc depuis la passerelle, pas depuis le client.
69
+
70
+ Conséquences, toutes **silencieuses** :
71
+
72
+ - l'en-tête d'adresse d'origine porte la passerelle → **l'audit est faux** ;
73
+ - toute limitation par adresse voit **une seule** adresse → elle protège tout le monde de personne,
74
+ ou bloque tout le monde ensemble ;
75
+ - aucune erreur, aucun journal : les valeurs sont plausibles, simplement fausses.
76
+
77
+ **Ce n'est pas un réglage à trouver.** Une autre pile réseau existe, mais son état varie selon les
78
+ versions. La conduite à tenir est de l'**énoncer** — dans le README de ton déploiement, dans le
79
+ ticket, dans la revue — et non de la masquer derrière une configuration qui en aurait l'air.
80
+
81
+ ## 7. Ce qui marche pareil
82
+
83
+ | Point | Verdict |
84
+ | --------------------------------------- | ----------------------------------------------------------------------------------------- |
85
+ | Un processus d'init dédié | Inutile, comme avec Docker : `node` est le processus n° 1 et reçoit `SIGTERM` directement |
86
+ | Les ports au-dessus de 1024 | `5151`, `8080`, `8443` : rien d'inaccessible en mode sans privilèges |
87
+ | Racine scellée + volumes `tmp` et `var` | Identique |
88
+ | `podman kube play` | Joue tes manifestes **sans cluster** — voir ci-dessous |
89
+
90
+ ## 8. `podman kube play` — ce qu'il prouve, et ce qu'il ne prouve pas
91
+
92
+ ```bash
93
+ podman kube play deploy/
94
+ ```
95
+
96
+ **Il prouve** : que le YAML est valide, que l'image démarre avec ce contexte de sécurité, que les
97
+ volumes déclarés suffisent, que les sondes répondent.
98
+
99
+ **Il ne prouve pas** : l'admission de la politique de sécurité (aucun contrôleur d'admission), les
100
+ Ingress, un déploiement progressif, la mise à l'échelle.
101
+
102
+ C'est une preuve locale utile et bon marché. Elle ne remplace pas un cluster jetable — elle évite
103
+ d'en démarrer un pour une faute de frappe.
@@ -0,0 +1,108 @@
1
+ # Secrets, mots de passe, matière cryptographique
2
+
3
+ > **Maintenance** : vérité courante. Éditer en place.
4
+
5
+ ## 1. Le fait qui gouverne tout
6
+
7
+ **Une couche d'image est lisible par quiconque télécharge l'image, et un fichier effacé par une
8
+ couche ultérieure y reste.** Un `COPY` d'un secret suivi d'un `RUN rm` ne supprime rien : le
9
+ secret est toujours dans l'archive, à une commande d'extraction. Un conteneur qui démarre ne le
10
+ montre plus — c'est précisément ce qui rend l'erreur indétectable à l'œil.
11
+
12
+ ```bash
13
+ npx nodefony image:check <app> # lit les COUCHES, pas l'image aplatie. Sort en échec.
14
+ ```
15
+
16
+ C'est pour ça que ce contrôle existe, et pour ça qu'il ne regarde pas l'image aplatie : un
17
+ contrôle qui la regarderait serait **vert dans le cas exactement le plus fautif**.
18
+
19
+ ## 2. Ce qui n'entre jamais dans l'image — le `.dockerignore`
20
+
21
+ Il est rendu avec l'application, et il exclut quatre familles. Ne les retire pas.
22
+
23
+ | Famille | Pourquoi |
24
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `**/node_modules`, `**/dist` | Reconstruits **dans** l'image. Entrés depuis ta machine, ils masqueraient la construction et l'image partirait avec le code de la veille. |
26
+ | `var`, `logs`, `**/*.log`, `**/*.jsonl` | Écritures propres à une machine, sans valeur dans une image |
27
+ | `*.local`, `**/*.local` | La convention des fichiers de secrets locaux — jamais commités, et pas davantage dans une image |
28
+ | **La matière cryptographique** | Clés privées, certificats |
29
+
30
+ > ⚠️ **Un motif de `.dockerignore` n'obéit pas aux règles du `.gitignore`.** Sans `**/`, il est
31
+ > **ancré à la racine du contexte** — alors qu'en `.gitignore` il vaut à toute profondeur. Un
32
+ > `*.log` seul ne voit donc aucun journal dans un sous-dossier. Quand tu ajoutes une exclusion,
33
+ > écris `**/motif` sauf si tu veux vraiment ne viser que la racine.
34
+
35
+ ## 3. Les certificats — ils se MONTENT, ils ne se gravent pas
36
+
37
+ L'image du frontal ne contient **aucune** clé privée : les certificats sont montés en lecture
38
+ seule à l'exécution. En Kubernetes, un Secret monté en volume ; avec compose, un montage lié.
39
+
40
+ L'étage de construction efface aussi les certificats de développement générés localement, pour
41
+ qu'aucun certificat auto-signé de ta machine ne descende en production.
42
+
43
+ ## 4. Les secrets de l'application
44
+
45
+ | Variable | Ce qu'il protège | Doit être partagé entre exemplaires ? |
46
+ | ------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
47
+ | `NF_CSRF_SECRET` | Les jetons anti-CSRF | **oui** — sinon un jeton émis par un exemplaire est refusé par l'autre |
48
+ | `NF_TOTP_KEY` | Le chiffrement au repos des secrets de double authentification | **oui** — sinon les seconds facteurs deviennent illisibles |
49
+ | `NF_WEBHOOK_KEY` | Le chiffrement des signatures de webhooks | **oui** |
50
+ | `NF_ADMIN_PASSWORD` | Le compte d'administration | — |
51
+
52
+ > 🔴 **Le partage entre exemplaires n'est pas un détail de confort.** Un secret différent par
53
+ > exemplaire produit des échecs **intermittents** — une requête sur deux, selon le répartiteur.
54
+ > C'est le symptôme le plus coûteux à diagnostiquer, parce qu'il ressemble à un problème réseau.
55
+
56
+ `NF_ADMIN_PASSWORD` a un défaut en développement. **En production il est obligatoire** : sans lui,
57
+ aucun compte d'administration n'est créé et le démarrage échoue — volontairement, plutôt que de
58
+ créer un compte avec un mot de passe connu de tout le monde.
59
+
60
+ ## 5. Les monter, pas les poser en clair
61
+
62
+ Le suffixe `_FILE` marche sur **toute** variable — détail complet dans
63
+ [`variables.md`](variables.md). Ce qu'il faut retenir ici :
64
+
65
+ ```yaml
66
+ # Kubernetes
67
+ env:
68
+ - name: NF_CSRF_SECRET_FILE
69
+ value: /run/secrets/csrf
70
+ volumeMounts:
71
+ - { name: secrets, mountPath: /run/secrets, readOnly: true }
72
+ ```
73
+
74
+ ```yaml
75
+ # compose
76
+ secrets: [csrf]
77
+ environment:
78
+ NF_CSRF_SECRET_FILE: /run/secrets/csrf
79
+ ```
80
+
81
+ **Pourquoi le fichier plutôt que la variable** : une variable d'environnement est lisible dans
82
+ `/proc/<pid>/environ`, ressort dans `docker inspect`, dans un vidage de processus, et souvent dans
83
+ un rapport d'erreur. Un fichier monté ne ressort d'aucun des quatre.
84
+
85
+ **Poser `KEY` et `KEY_FILE` ensemble fait échouer le démarrage.** L'ambiguïté n'est pas arbitrée en
86
+ silence — c'est voulu.
87
+
88
+ ## 6. Rotation
89
+
90
+ Les secrets de session et de CSRF ne peuvent pas changer d'un coup sans invalider ce qui est en
91
+ vol. L'ordre qui limite la casse :
92
+
93
+ 1. Poser le nouveau secret **à côté** de l'ancien dans le gestionnaire de secrets.
94
+ 2. Redéployer **progressivement** — les exemplaires reprennent le nouveau.
95
+ 3. Retirer l'ancien **après** que tous les exemplaires ont repris, pas avant.
96
+
97
+ Entre 2 et 3, les deux valeurs coexistent : c'est la fenêtre où rien ne casse. Sauter l'étape 3
98
+ est l'erreur la plus fréquente — un secret « remplacé » qui traîne encore six mois plus tard.
99
+
100
+ ## 7. Ce qu'il ne faut pas faire, et pourquoi c'est tentant
101
+
102
+ | Tentation | Ce qui arrive |
103
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
104
+ | Mettre le secret en `ARG` de construction | Les arguments de construction sont **dans les métadonnées de l'image**. `docker history` les montre. |
105
+ | Copier le fichier de secrets puis l'effacer | Il reste dans la couche. Voir §1. |
106
+ | Mettre les secrets dans un ConfigMap | Un ConfigMap n'est ni chiffré au repos ni traité comme sensible par les outils |
107
+ | Laisser le défaut de développement en production | Il est **connu** : il est dans le dépôt public du framework |
108
+ | Passer le secret en ligne de commande | La ligne de commande est visible dans la liste des processus du conteneur |
@@ -0,0 +1,174 @@
1
+ # Les variables d'environnement
2
+
3
+ > **Maintenance** : vérité courante. Éditer en place.
4
+
5
+ ## 1. La commande qui répond, et à laquelle rien ne ment
6
+
7
+ ```bash
8
+ npx nodefony env # la cascade des .env, la valeur EFFECTIVE de chaque variable, et sa PROVENANCE
9
+ npx nodefony env --json # le même rapport, pour un script
10
+ ```
11
+
12
+ C'est **la** commande d'exploitation. Elle ne dit pas ce que ton manifeste déclare : elle dit ce
13
+ que l'application a **réellement lu**, et d'où ça vient. Toutes les fois où « la variable ne fait
14
+ rien », la réponse est dans cette sortie.
15
+
16
+ ```bash
17
+ npx nodefony inspect config --json # la configuration effective, et la provenance de chaque valeur
18
+ npx nodefony inspect schema <module> # les clés qui EXISTENT, leur type, ce qu'elles font
19
+ npx nodefony doctor --env production # ce qui manquerait là-bas, depuis ici
20
+ ```
21
+
22
+ > 🔴 **`inspect schema` avant d'écrire une clé.** Une clé inconnue n'est pas refusée : elle est
23
+ > **retirée en silence** à la validation. Tu poses le réglage, rien ne change, rien ne le dit.
24
+
25
+ ## 2. Deux grammaires, et elles ne font pas la même chose
26
+
27
+ ### `NF_<NOM>` — les variables du catalogue
28
+
29
+ Simple underscore. Ce sont des variables **déclarées**, avec un rôle, un type et un défaut connus.
30
+
31
+ ### `NF__<MODULE>__<CHEMIN>` — la surcharge de configuration
32
+
33
+ **Double** underscore. Elle atteint **n'importe quelle clé de configuration de n'importe quel
34
+ module**, sans que cette clé ait eu besoin d'être prévue comme variable. Le séparateur en double
35
+ underscore est la convention .NET Core et Docker : explicite, et sans ambiguïté avec le camelCase.
36
+
37
+ ```bash
38
+ NF__SECURITY__JWT__ACCESSTTLS=300
39
+ NF__HTTP__SERVERS__HTTPS__PORT=8443
40
+ NF__SECURITY__CORS__ORIGINS=https://a.com,https://b.com # la virgule fait un tableau
41
+ ```
42
+
43
+ - Le **premier segment est le module** (`SECURITY` → `@nodefony/security`), les suivants sont le
44
+ chemin dans sa configuration.
45
+ - Les segments sont **insensibles à la casse** et résolus contre les clés réelles :
46
+ `ACCESSTTLS` trouve `accessTtlS`.
47
+ - **Précédence** : appliquée **après** la configuration de l'application, et **avant** la
48
+ validation du schéma. Donc une valeur surchargée est **validée comme les autres** — une valeur
49
+ aberrante fait échouer le démarrage au lieu de s'installer.
50
+ - Résolue **une fois au démarrage**. Aucun coût par requête.
51
+
52
+ **La conversion est explicite**, pas devinée par une bibliothèque : `true`/`false` deviennent un
53
+ booléen, un nombre devient un nombre, `[…]` et `{…}` sont lus en JSON, une chaîne à virgules
54
+ devient un tableau, le reste reste une chaîne. C'est ce qui évite le piège classique où la chaîne
55
+ `"false"` est convertie en booléen **vrai**.
56
+
57
+ ## 3. Le suffixe `_FILE` — les secrets montés
58
+
59
+ **Toute** variable du catalogue accepte un `_FILE`. Le contenu du fichier est lu, retour à la ligne
60
+ final retiré.
61
+
62
+ ```yaml
63
+ env:
64
+ - name: NF_CSRF_SECRET_FILE
65
+ value: /run/secrets/csrf
66
+ ```
67
+
68
+ Trois comportements à connaître, et les deux derniers sont ce qui distingue ce mécanisme d'un
69
+ mécanisme complaisant :
70
+
71
+ | Situation | Ce qui se passe |
72
+ | ---------------------------------------------- | ------------------------------------------------------------------- |
73
+ | `KEY` absente, `KEY_FILE` pointe un fichier | La valeur est lue dans le fichier |
74
+ | **`KEY` ET `KEY_FILE` posées toutes les deux** | **Le démarrage ÉCHOUE** — l'ambiguïté n'est pas arbitrée en silence |
75
+ | `KEY_FILE` pointe un fichier **illisible** | **Le démarrage ÉCHOUE** — pas de repli muet sur un défaut |
76
+
77
+ C'est délibéré : un secret qu'on croit monté et qui ne l'est pas est une application qui tourne
78
+ avec un secret de développement, en production, sans que personne le sache.
79
+
80
+ Marche avec les secrets Docker, les secrets Kubernetes, et tout agent qui dépose un fichier.
81
+
82
+ ## 4. Les variables de l'exploitation
83
+
84
+ Les seules qui te concernent au déploiement. Le catalogue complet, c'est `npx nodefony env`.
85
+
86
+ ### Environnement et réseau
87
+
88
+ | Variable | Rôle | Absente ⇒ |
89
+ | --------------- | -------------------------- | -------------------- |
90
+ | `NF_ENV` | L'environnement applicatif | Dérivé de `NODE_ENV` |
91
+ | `NF_PORT` | Port HTTP | `5151` |
92
+ | `NF_PORT_HTTPS` | Port HTTPS | `5152` |
93
+
94
+ ### Derrière un frontal — **les plus importantes**
95
+
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-` non émis, adresse client fausse |
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 |
102
+
103
+ ### Données
104
+
105
+ | Variable | Rôle | Absente ⇒ |
106
+ | ----------------- | ---------------------- | ---------------------------------------------------- |
107
+ | `NF_DATABASE_URL` | L'URL de la base | **Repli sur SQLite local**, dans `var/` — silencieux |
108
+ | `NF_REDIS_URL` | L'URL du cache partagé | Le cache n'est pas chargé |
109
+
110
+ > 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. Rien n'est en erreur.
112
+ > `npx nodefony env` le montre en une ligne.
113
+
114
+ ### Secrets — obligatoires en production
115
+
116
+ | Variable | Ce qu'il protège |
117
+ | ------------------- | ------------------------------------------------ |
118
+ | `NF_CSRF_SECRET` | Les jetons anti-CSRF, partagés entre exemplaires |
119
+ | `NF_TOTP_KEY` | Les secrets de double authentification au repos |
120
+ | `NF_WEBHOOK_KEY` | Les signatures de webhooks |
121
+ | `NF_ADMIN_PASSWORD` | Le compte d'administration |
122
+
123
+ Tous en `_FILE` en production. Détail et rotation → [`secrets.md`](secrets.md).
124
+
125
+ ### Plusieurs exemplaires
126
+
127
+ | Variable | Rôle |
128
+ | --------------------------------- | ---------------------------------------------------------------------------- |
129
+ | `NF_POD_NAME` | Identité de l'exemplaire — **dérive l'identité d'origine du bus temps réel** |
130
+ | `NF_INSTANCE_ID` | Identifiant d'exemplaire |
131
+ | `NF_REALTIME_DRIVER` | Le transport du bus entre exemplaires |
132
+ | `NF_REALTIME_BACKPLANE_SECRET` | Scelle les enveloppes échangées sur le bus |
133
+ | `NF_REALTIME_BACKPLANE_NAMESPACE` | Cloisonne deux applications qui partagent le même bus |
134
+
135
+ > 🔴 **Deux applications sur un même cache, sans espace de noms distinct, se parlent.** Le symptôme
136
+ > est un message qui arrive à des clients d'une autre application — jamais une erreur.
137
+
138
+ ### Multi-process sur une seule machine
139
+
140
+ | Variable | Rôle |
141
+ | ------------ | -------------------------------------------------------- |
142
+ | `NF_CLUSTER` | Active le mode multi-process |
143
+ | `NF_WORKERS` | Nombre d'exécutants — sinon dérivé des limites du cgroup |
144
+
145
+ En Kubernetes, **laisse ça tranquille** : un exemplaire = un processus = un pod, et c'est
146
+ l'ordonnanceur qui multiplie. Le mode multi-process sert sur une machine nue.
147
+
148
+ ### Démarrage et journalisation
149
+
150
+ | Variable | Rôle |
151
+ | -------------------- | ------------------------------------------------------- |
152
+ | `NF_BOOT_TIMEOUT_MS` | Au-delà, le démarrage abandonne — borne ta sonde dessus |
153
+ | `NF_BOOT_WARN_MS` | Au-delà, il avertit |
154
+ | `NF_LOG_DRIVER` | `stdout` (défaut), `file`, ou aucun |
155
+ | `NF__DEBUG` | Débogage ciblé, par module |
156
+
157
+ En conteneur, **laisse `NF_LOG_DRIVER` sur `stdout`** : c'est ce que ton collecteur ramasse. Écrire
158
+ dans un fichier à l'intérieur d'un conteneur, c'est écrire dans quelque chose qui disparaît.
159
+
160
+ ## 5. Le préfixe `NF_` — pourquoi, et ce que ça t'évite
161
+
162
+ **Tout ce que Nodefony lit est préfixé `NF_`.** Pas par coquetterie : ton environnement de
163
+ production a déjà des `REDIS_HOST`, des `COOKIE_SECRET`, des `POD_NAME` que d'autres outils
164
+ revendiquent. Une collision ne produit **jamais** une erreur — elle produit un comportement
165
+ inexplicable.
166
+
167
+ Deux exceptions, et deux seulement :
168
+
169
+ - Ce que Nodefony ne possède pas : `NODE_ENV`, `CI`, `NODE_DEBUG`… — lues telles quelles.
170
+ - Les alias qu'un hébergeur **pose lui-même** (`DATABASE_URL`, `REDIS_URL`, `APP_ENV`) : acceptés,
171
+ mais **en second rang** derrière la forme `NF_`.
172
+
173
+ > ⚠️ Corollaire d'exploitation : si tu poses `REDIS_URL` **et** `NF_REDIS_URL`, c'est la forme
174
+ > préfixée qui gagne. Purger l'une en croyant avoir purgé l'autre est une erreur déjà vue.
@@ -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
@@ -139,15 +145,21 @@ npx nodefony orm:migrate
139
145
  d'appliquer ». Il ne refuse pas — il ne lit pas la base et ignore si ta table porte des lignes —,
140
146
  mais s'il le signale et que ta table n'est pas vide, la migration échouera.
141
147
 
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.
148
+ **À l'étape 3, sur sqlite, attends-toi à un AVERTISSEMENT `table-rebuild`, pas à un refus.**
149
+ Rendre une colonne obligatoire n'est pas un `ALTER` en sqlite : le moteur n'en a pas, alors la
150
+ table est RECONSTRUITE (`CREATE` une table d'étape → `INSERT … SELECT` → `DROP TABLE` →
151
+ `RENAME`). Ce `DROP TABLE` porte sur une table recopiée une ligne plus haut, dans la même
152
+ migration : l'outil le reconnaît et te le signale « recrée la table pour modifier une colonne
153
+ (SQLite ne sait pas faire autrement) » sans rien refuser, à la génération comme à la mise en
154
+ service. Tu n'as donc **pas** besoin de `--allow-destructive` pour une reconstruction.
155
+
156
+ Ce qui reste à TA charge, et c'est le sens de l'avertissement : **relis l'`INSERT … SELECT`**.
157
+ Une colonne absente de cette recopie serait bel et bien perdue, et aucun automate ne peut le
158
+ deviner à ta place. Éprouvé de bout en bout : deux lignes semées, trois étapes, deux lignes
159
+ intactes et l'index unique en place.
160
+
161
+ > Un vrai `DROP TABLE` — une table supprimée sans recopie, ou une AUTRE table supprimée dans la
162
+ > même migration — reste refusé. Le verdict porte sur chaque table, pas sur le fichier.
151
163
 
152
164
  > **Ne jamais** répondre à un échec de migration en refaisant la base. Une migration qui n'est pas
153
165
  > passée n'a **rien** changé — sqlite et PostgreSQL l'annulent entière. C'est le fichier qu'il faut