@nodefony/devkit 10.0.0-alpha.7 → 10.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/controllers/DevkitController.js +2 -2
- package/dist/nodefony/controllers/McpController.js +3 -3
- package/dist/nodefony/service/DevkitService.js +2 -2
- package/docs/index.md +28 -20
- package/package.json +7 -7
- package/skills/nodefony-add-crud/SKILL.md +72 -32
- package/skills/nodefony-browser/SKILL.md +12 -7
- package/skills/nodefony-dev/SKILL.md +239 -15
- package/skills/nodefony-migrate-schema/SKILL.md +34 -12
- package/skills/nodefony-migrate-schema/references/verdicts.md +19 -18
- package/skills/nodefony-protect-route/SKILL.md +69 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
|
|
2
2
|
import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
3
3
|
import { buildCard } from "./nodefony/src/card.js";
|
|
4
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
5
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
5
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
6
6
|
import DevkitService_default from "./nodefony/service/DevkitService.js";
|
|
7
7
|
import DevkitController_default from "./nodefony/controllers/DevkitController.js";
|
|
8
8
|
import McpController_default from "./nodefony/controllers/McpController.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { Controller, controller, route } from "@nodefony/framework";
|
|
4
4
|
//#region nodefony/controllers/DevkitController.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
|
|
4
4
|
import { ACCESS_TOKEN_VERIFIER, JsonRpcError, Nodefony, authorizeProtectedResource, checkMcpAccess, collectMcpTools, handleMcpMessage, jsonRpcFailure, mcpCallerRoles, protectedResourceMetadataUrl, readBearerHeader } from "nodefony";
|
|
5
5
|
import { Body, Controller, Headers, controller, route } from "@nodefony/framework";
|
|
6
6
|
//#region nodefony/controllers/McpController.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import defaults from "../config/config.js";
|
|
2
2
|
import { buildCard } from "../src/card.js";
|
|
3
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
4
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
5
5
|
import { Module, Nodefony, Service, extend, injectable, mcpDeclaredScopes } from "nodefony";
|
|
6
6
|
//#region nodefony/service/DevkitService.ts
|
|
7
7
|
/**
|
package/docs/index.md
CHANGED
|
@@ -106,9 +106,13 @@ la rend éprouvable sans serveur, et ce qui l'empêche d'inventer quoi que ce so
|
|
|
106
106
|
npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
Sept outils. Quatre sont les commandes que vous connaissez déjà :
|
|
110
110
|
`nodefony_inspect` (ce qui est monté), `nodefony_check` (ce qui manque),
|
|
111
111
|
`nodefony_symbols` (ce qu'une API signifie), `nodefony_card` (par où commencer).
|
|
112
|
+
`nodefony_docs` sert la documentation des modules. `nodefony_admin_list` et
|
|
113
|
+
`nodefony_admin_call` ouvrent les lectures du data plane d'administration — le
|
|
114
|
+
catalogue, puis l'appel — et exigent un jeton porteur du scope `admin:read` :
|
|
115
|
+
sans lui, ils n'apparaissent pas dans la liste.
|
|
112
116
|
|
|
113
117
|
**Il n'y a pas de process à lancer.** Depuis la révision `2026-07-28` du
|
|
114
118
|
transport, un serveur MCP est un endpoint `POST` sans session : c'est donc une
|
|
@@ -264,8 +268,8 @@ est absent de `tools/list` **et** inappelable en le nommant, et le refus dit
|
|
|
264
268
|
|
|
265
269
|
> 🔴 **Tant que la porte n'authentifie personne, un outil à scopes ne sortira
|
|
266
270
|
> jamais.** C'est le comportement voulu — fermé par défaut — mais il faut le
|
|
267
|
-
> savoir avant de chercher une panne : `caller` vaut
|
|
268
|
-
scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
|
|
271
|
+
> savoir avant de chercher une panne : `caller` vaut
|
|
272
|
+
> `{ authenticated: false, scopes: [] }` tant que le rôle _resource server_ décrit plus haut n'est pas
|
|
269
273
|
> branché. Le jour où il le sera, ces déclarations prendront effet sans qu'une
|
|
270
274
|
> ligne d'outil change.
|
|
271
275
|
|
|
@@ -280,16 +284,20 @@ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
|
|
|
280
284
|
service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
|
|
281
285
|
dans le corps de l'action.
|
|
282
286
|
|
|
283
|
-
Le paquet livre donc
|
|
284
|
-
un
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
|
288
|
-
|
|
|
289
|
-
| `nodefony-
|
|
290
|
-
| `nodefony-
|
|
291
|
-
| `nodefony-add-
|
|
292
|
-
| `nodefony-
|
|
287
|
+
Le paquet livre donc huit **skills** au format [Agent Skills](https://agentskills.io) :
|
|
288
|
+
un qui porte la conduite commune de toute tâche, et un par geste où l'invention
|
|
289
|
+
coûte cher :
|
|
290
|
+
|
|
291
|
+
| Skill | Le besoin qu'il couvre |
|
|
292
|
+
| ------------------------------- | --------------------------------------------------------------- |
|
|
293
|
+
| `nodefony-dev` | conduire une tâche de bout en bout, avant toute modification |
|
|
294
|
+
| `nodefony-add-crud` | exposer une ressource REST complète, entité comprise |
|
|
295
|
+
| `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur |
|
|
296
|
+
| `nodefony-protect-route` | réserver une route à qui est habilité |
|
|
297
|
+
| `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse |
|
|
298
|
+
| `nodefony-browser` | voir **et mesurer** un écran, sans navigateur sur le poste |
|
|
299
|
+
| `nodefony-migrate-schema` | faire évoluer le schéma d'une base et le porter en production |
|
|
300
|
+
| `nodefony-devops` | exploiter l'application en conteneur, sans réécrire son contrat |
|
|
293
301
|
|
|
294
302
|
Tous portent le préfixe `nodefony-` : leurs pointeurs arrivent dans le dossier où
|
|
295
303
|
vous écrivez aussi les vôtres, et sans namespace un skill maison du même nom
|
|
@@ -330,13 +338,13 @@ absente sans `NODE_ENV`) ; les skills, eux, doivent se mettre à jour par npm.
|
|
|
330
338
|
|
|
331
339
|
## Configuration
|
|
332
340
|
|
|
333
|
-
| Clé | Type | Défaut
|
|
334
|
-
| -------------------- | ---------- |
|
|
335
|
-
| `enabled` | `boolean` | `true`
|
|
336
|
-
| `mcp.enabled` | `boolean` | `true`
|
|
337
|
-
| `mcp.allowedOrigins` | `string[]` | `[]`
|
|
338
|
-
| `mcp.allowRemote` | `boolean` | `false`
|
|
339
|
-
| `mcp.tools` | `string[]` | `
|
|
341
|
+
| Clé | Type | Défaut | Rôle |
|
|
342
|
+
| -------------------- | ---------- | -------------------------------------------------- | ---------------------------------------------- |
|
|
343
|
+
| `enabled` | `boolean` | `true` | Interrupteur du module |
|
|
344
|
+
| `mcp.enabled` | `boolean` | `true` | Répond-on aux requêtes MCP ? Coupé → `404` |
|
|
345
|
+
| `mcp.allowedOrigins` | `string[]` | `[]` | Origines de navigateur admises ; vide = aucune |
|
|
346
|
+
| `mcp.allowRemote` | `boolean` | `false` | Accepter un appel d'une adresse non locale |
|
|
347
|
+
| `mcp.tools` | `string[]` | les sept outils intégrés (`BUILTIN_MCP_TOOL_KEYS`) | Allowlist des outils — lecture seule |
|
|
340
348
|
|
|
341
349
|
Les clés, leurs types et leurs défauts viennent du schéma Zod
|
|
342
350
|
(`nodefony/config/config.ts`) — **source unique** dont dérivent la
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/devkit",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
|
|
6
6
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"coverage": "vitest run --coverage"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
29
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
30
|
-
"nodefony": "^10.0.0-alpha.
|
|
28
|
+
"@nodefony/framework": "^10.0.0-alpha.9",
|
|
29
|
+
"@nodefony/http": "^10.0.0-alpha.9",
|
|
30
|
+
"nodefony": "^10.0.0-alpha.9",
|
|
31
31
|
"zod": "^4.6.5",
|
|
32
32
|
"playwright": "^1.50.0",
|
|
33
33
|
"lighthouse": "^13.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
37
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
38
|
-
"nodefony": "^10.0.0-alpha.
|
|
36
|
+
"@nodefony/framework": "^10.0.0-alpha.9",
|
|
37
|
+
"@nodefony/http": "^10.0.0-alpha.9",
|
|
38
|
+
"nodefony": "^10.0.0-alpha.9"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"axe-core": "4.13.0",
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodefony-add-crud
|
|
3
3
|
description: >
|
|
4
|
-
Crée une ressource complète dans une application Nodefony —
|
|
4
|
+
Crée une ressource complète dans une application Nodefony — entité, schémas de validation,
|
|
5
5
|
service CRUD, controller REST+WebSocket et tests — par le générateur `nodefony create entity`,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
controller de ressource.
|
|
6
|
+
sur SQL comme sur MongoDB. Porte la grammaire de champs, la
|
|
7
|
+
table des types moteur par moteur, les relations et clés étrangères,
|
|
8
|
+
les réglages pour épouser une table SQL existante, et ce qu'on découvre autrement en
|
|
9
|
+
production : la table naît au démarrage, un champ ajouté n'est rattrapé que s'il accepte le
|
|
10
|
+
vide, et la production s'applique par des migrations (skill `nodefony-migrate-schema`). À
|
|
11
|
+
charger AVANT d'écrire une entité, un repository ou un controller de ressource.
|
|
12
12
|
Déclencheurs : "ajoute une entité", "crée un CRUD", "nouvelle table", "modèle de données",
|
|
13
|
-
"ressource REST", "
|
|
14
|
-
|
|
15
|
-
"
|
|
13
|
+
"ressource REST", "je veux stocker des articles/commandes", "comment définir un champ", "quels types de champ ?", "une relation entre
|
|
14
|
+
deux entités", "clé étrangère", "index composite", "épouser une table existante",
|
|
15
|
+
"entité MongoDB", "schéma Mongoose".
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# add-crud — une ressource complète, générée
|
|
@@ -44,29 +44,48 @@ se connecte qu'au démarrage), la pagination bornée **et son tri déclaré**, l
|
|
|
44
44
|
|
|
45
45
|
## La grammaire de champs
|
|
46
46
|
|
|
47
|
-
`nom:type[
|
|
48
|
-
d'unicité, `:index` un index simple.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
`nom:type[?][=défaut][:index|:unique]` — **non-null par défaut**, `?` rend facultatif, `=valeur`
|
|
48
|
+
fixe un défaut LITTÉRAL, `:unique` pose une contrainte d'unicité, `:index` un index simple.
|
|
49
|
+
🔴 **`!` est REFUSÉ** : il ne veut pas dire « obligatoire » ici — un champ l'est déjà.
|
|
50
|
+
|
|
51
|
+
**Casse** : l'entité et la cible d'une relation en PascalCase (`Post`, `ref:User`), le champ en
|
|
52
|
+
camelCase (`publishedAt`). Une faute de casse ou un type d'un autre outil (`boolean`, `integer`)
|
|
53
|
+
est refusé avec la forme juste proposée.
|
|
54
|
+
|
|
55
|
+
| Type | Ce que ça produit | SQLite | PostgreSQL | MySQL / MariaDB | MongoDB |
|
|
56
|
+
| -------------- | ----------------------------- | --------------- | ---------------- | --------------- | ---------------------- |
|
|
57
|
+
| `string(n)` | texte court, borné (255) | `text` ¹ | `varchar(n)` | `varchar(n)` | `String` + `maxlength` |
|
|
58
|
+
| `text` | texte long | `text` | `text` | `text` | `String` |
|
|
59
|
+
| `int` | entier | `integer` | `integer` | `int` | `Number` |
|
|
60
|
+
| `float` | nombre à virgule | `real` | `double` | `double` | `Number` |
|
|
61
|
+
| `decimal(p,s)` | décimal EXACT (voyage chaîne) | `numeric` | `numeric(p,s)` | `decimal(p,s)` | `String` |
|
|
62
|
+
| `bool` | booléen | `integer` (0/1) | `boolean` | `boolean` | `Boolean` |
|
|
63
|
+
| `json` | document libre | `text` (JSON) | `jsonb` | `json` | `Mixed` |
|
|
64
|
+
| `date` | horodatage (ms) | `integer` (ms) | `timestamptz(3)` | `datetime(3)` | `Date` |
|
|
65
|
+
| `uuid` | identifiant | `text` | `uuid` | `varchar(36)` | `String` |
|
|
66
|
+
| `char(n)` | longueur EXACTE | `text` ¹ | `char(n)` | `char(n)` | `String`, `n` exact |
|
|
67
|
+
| `enum(a,b)` | valeurs admises ² | `text` | `varchar(255)` | `varchar(255)` | `String` + `enum` |
|
|
68
|
+
| `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
|
|
69
|
+
|
|
70
|
+
¹ SQLite n'applique aucune longueur : c'est le schéma d'entrée (Zod) qui borne, sur TOUS les
|
|
71
|
+
transports. ² Même colonne partout, sans type SQL nommé (qui exigerait une migration) : le type
|
|
72
|
+
TypeScript et le schéma Zod bornent les valeurs. ³ En SQL, la colonne prend le type de la clé
|
|
73
|
+
visée (`uuid`, entier pour `--id serial`, texte pour `User`) ; en MongoDB, aucune clé étrangère.
|
|
74
|
+
|
|
75
|
+
La table exacte, par moteur, telle que CETTE version l'écrit :
|
|
76
|
+
`npx nodefony create entity --describe-json` (champ `context.columnTypes`).
|
|
77
|
+
|
|
78
|
+
**Une relation** s'écrit `<champ>:ref:<Entité>` — le champ d'abord, toujours :
|
|
62
79
|
|
|
63
80
|
```bash
|
|
64
|
-
npx nodefony create entity Comment body:text ref:Article
|
|
81
|
+
npx nodefony create entity Comment body:text article:ref:Article
|
|
65
82
|
```
|
|
66
83
|
|
|
67
|
-
La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`.
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`. En SQL,
|
|
85
|
+
la **clé étrangère est émise** (`.references()`), du type de la clé visée, et son effacement se
|
|
86
|
+
déduit de la nullabilité : `article:ref:Article` (obligatoire) → `restrict`, le parent ne peut pas
|
|
87
|
+
partir ; `article:ref:Article?` → `set null`. `cascade` n'est jamais un défaut : il s'écrit à la
|
|
88
|
+
main dans la table générée.
|
|
70
89
|
|
|
71
90
|
**Un index de table** porte plusieurs colonnes, et c'est le seul à le pouvoir :
|
|
72
91
|
|
|
@@ -77,12 +96,32 @@ npx nodefony create entity Visit siteId:uuid path:string at:date --index "siteId
|
|
|
77
96
|
Les deux options sont **répétables** — un couple par index. Sur un schéma réel, la majorité des
|
|
78
97
|
index utiles sont composites : c'est ainsi qu'une table est réellement interrogée.
|
|
79
98
|
|
|
99
|
+
## Sur une application MongoDB
|
|
100
|
+
|
|
101
|
+
La même commande écrit une entité **document** : schéma Mongoose, service CRUD, controller et
|
|
102
|
+
tests — sans table ni migration (la collection naît à la première écriture). Une ligne, toutes
|
|
103
|
+
les options qui ont un sens ici :
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx nodefony create entity Post title:string(120) body:text? views:int=0 status:enum(draft,published)=draft slug:string:unique tags:json? author:ref:User --soft-delete --route /api/posts --dry-run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- La clé est l'`_id` natif, servie en `id` ; `ref:<Entité>` est un `ObjectId` indexé, chargé
|
|
110
|
+
par `?include=`, que le schéma d'entrée exige bien formé (24 caractères hexadécimaux → 422).
|
|
111
|
+
- **MongoDB ne tient aucune clé étrangère, l'ORM tient l'effacement** : supprimer un parent
|
|
112
|
+
encore désigné par une référence obligatoire est refusé (409), une facultative est remise à
|
|
113
|
+
`null` — la politique du SQL. Hors transaction, ce contrôle n'est pas atomique ; et
|
|
114
|
+
l'INSERTION n'est pas gardée (un identifiant inexistant est accepté, `?include=` rend `null`).
|
|
115
|
+
- Refusées en le disant (options SQL) : `--table`, `--column-case`, `--id-name`, `--dialect`,
|
|
116
|
+
`--id`, `--index`, `--unique`. Un index composite s'écrit à la main dans le schéma.
|
|
117
|
+
- `User` ne se régénère pas : il s'étend dans `nodefony/entity/User.ts` (son TSDoc donne le geste).
|
|
118
|
+
|
|
80
119
|
## Épouser une table qui existe déjà
|
|
81
120
|
|
|
82
121
|
Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
|
|
83
122
|
|
|
84
123
|
```bash
|
|
85
|
-
npx nodefony create entity Session token:string
|
|
124
|
+
npx nodefony create entity Session token:string:unique --table user_sessions --column-case snake --id-name session_id
|
|
86
125
|
```
|
|
87
126
|
|
|
88
127
|
Faire suivre le TypeScript aurait transformé un réglage de nommage en refonte : le service, le
|
|
@@ -149,7 +188,7 @@ Ce contrat vaut aussi quand tu écris une liste **à la main** (un endpoint d'ad
|
|
|
149
188
|
listing filtré) : le côté serveur déclare ce qu'il sait trier, le point d'entrée le demande, et le
|
|
150
189
|
refus tombe tout seul.
|
|
151
190
|
|
|
152
|
-
## Les trois vérités à savoir avant de livrer
|
|
191
|
+
## Les trois vérités à savoir avant de livrer (SQL)
|
|
153
192
|
|
|
154
193
|
1. **La table naît au prochain démarrage en développement** (`CREATE TABLE IF NOT EXISTS`).
|
|
155
194
|
2. **La modifier n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
|
|
@@ -164,7 +203,8 @@ Le générateur s'arrête plutôt que de produire un fichier bancal — lis le m
|
|
|
164
203
|
geste :
|
|
165
204
|
|
|
166
205
|
- **hors projet** (aucun `nodefony.config.ts` au-dessus) ;
|
|
167
|
-
-
|
|
206
|
+
- **aucun ORM** dans l'application (ni `@nodefony/drizzle`, ni `@nodefony/mongoose`) ;
|
|
207
|
+
- **une option SQL sur une application MongoDB** (liste ci-dessus) ;
|
|
168
208
|
- **entité déjà déclarée** ;
|
|
169
209
|
- **nom réservé par un module du framework** (`session`, `access_token`, `audit_event`…) — un
|
|
170
210
|
homonyme dépossède le module, et l'application ne démarre plus sur un message parlant d'une
|
|
@@ -32,12 +32,17 @@ description: >
|
|
|
32
32
|
## Le geste — sur ta machine
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
npx nodefony see / # dans une application : mesure l'écran
|
|
36
|
+
npx nodefony see / --install # la même, en posant d'abord `playwright` et `axe-core`
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
La commande résout elle-même la sonde et **dit ce qui manque** plutôt que d'échouer sur une trace de
|
|
40
|
+
résolution. Elle n'installe rien sans `--install` : l'outillage d'un navigateur piloté pèse des
|
|
41
|
+
centaines de mégaoctets une fois les navigateurs posés.
|
|
42
|
+
|
|
43
|
+
Hors d'une application Nodefony, le premier pas s'écrit en toutes lettres : `npm i -D playwright
|
|
44
|
+
axe-core`, puis `npx playwright install chromium` si aucun navigateur n'est déjà là (§ suivant), et
|
|
45
|
+
la sonde se lance par son chemin (`node node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/inspect.mjs /`).
|
|
41
46
|
|
|
42
47
|
C'est tout. Les sondes **constatent** où elles s'exécutent : sur ta machine elles visent
|
|
43
48
|
`https://127.0.0.1:5152` et déposent leurs captures dans `tmp/browser/`. Rien à configurer tant que
|
|
@@ -278,9 +283,9 @@ Lighthouse complet, y compris **derrière une authentification** — ce que l'ex
|
|
|
278
283
|
ne sait pas faire sur une application protégée.
|
|
279
284
|
|
|
280
285
|
```bash
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
npx nodefony see /tableau-de-bord --audit # audite
|
|
287
|
+
npx nodefony see /tableau-de-bord --audit --install # pose `lighthouse` (une vingtaine de
|
|
288
|
+
# mégaoctets, payés seulement si tu audites)
|
|
284
289
|
```
|
|
285
290
|
|
|
286
291
|
Hors d'une application générée, ou pour passer des identifiants :
|
|
@@ -14,7 +14,7 @@ description: >
|
|
|
14
14
|
est-il fini", "je ne trouve rien sur ce sujet", "ce n'est pas documenté", "je touche au
|
|
15
15
|
frontend", "mon composant charge des données", "mon écran n'affiche rien".
|
|
16
16
|
metadata:
|
|
17
|
-
version: 1.
|
|
17
|
+
version: 1.2.0
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
# nodefony-dev — développer dans cette application sans rien inventer
|
|
@@ -112,17 +112,160 @@ dans la doc chargée) et `nodefony_symbols` (rend la SIGNATURE réelle d'un symb
|
|
|
112
112
|
ne porte pas). Elles supposent un serveur démarré et la porte câblée (`npx nodefony ai:mcp`) ;
|
|
113
113
|
`docs.mjs`, lui, répond toujours.
|
|
114
114
|
|
|
115
|
-
## 3.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
## 3. Interroger l'application — elle répond mieux que ses sources
|
|
116
|
+
|
|
117
|
+
**Perdu ? La carte de visite dit qui répond, ce qui est chargé, où lire et quoi lancer :**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx nodefony card # -j pour du JSON
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Elle répond **toujours** — application pas encore construite, terminal sans aucune variable
|
|
124
|
+
d'environnement : elle ne lit que des fichiers. Elle le DIT alors (« modules installés », pas
|
|
125
|
+
« chargés ») et renvoie à `inspect modules` pour ce qui est vraiment monté.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx nodefony inspect routes --json # routes réelles (chemin MONTÉ, méthodes, controller)
|
|
129
|
+
npx nodefony inspect services --json # services enregistrés, et le module qui les porte
|
|
130
|
+
npx nodefony inspect config --json # config EFFECTIVE, et d'où vient chaque valeur
|
|
131
|
+
npx nodefony inspect schema http # ce qu'on a le DROIT d'écrire : clés, type, défaut, description
|
|
132
|
+
npx nodefony inspect modules --json # modules CHARGÉS — pas ceux que le manifeste déclare
|
|
133
|
+
npx nodefony inspect entities --json # entités déclarées à l'ORM
|
|
134
|
+
npx nodefony inspect stores --json # où sont RÉELLEMENT écrites les données (sessions, cache…)
|
|
135
|
+
npx nodefony inspect graph --json # graphe des entités et de leurs relations
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Ces commandes bootent l'application **sans ouvrir un seul port** et rendent exactement ce que sert
|
|
139
|
+
la console d'administration — même code, deux portes.
|
|
140
|
+
|
|
141
|
+
> **Ce que rend `inspect` ENGLOBE tes sources et les dépasse.** Les modules installés montent leurs
|
|
142
|
+
> propres routes : une application qui en définit une poignée en expose couramment plus d'une
|
|
143
|
+
> centaine. Un écart d'un ordre de grandeur entre tes fichiers et `inspect routes --json | jq
|
|
144
|
+
'length'` n'est PAS une anomalie de l'outil — c'est la différence entre ce que TU as écrit et ce
|
|
145
|
+
> que l'application MONTE.
|
|
146
|
+
|
|
147
|
+
**« Que fait cette classe, où est-elle définie, qu'étend-elle ? » — une commande, pas une fouille :**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx nodefony symbols AbstractCrudService # définition, TSDoc, parenté — en O(1)
|
|
151
|
+
npx nodefony symbols --module @nodefony/http # toute la surface exportée d'un paquet
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Le graphe symbolique de tout le framework est livré avec le paquet `nodefony` : la réponse ne
|
|
155
|
+
dépend ni d'un serveur, ni d'un build, ni de ta connexion. Va y chercher un symbole AVANT d'ouvrir
|
|
156
|
+
un `.d.ts` — et avant, surtout, d'inventer une signature.
|
|
157
|
+
|
|
158
|
+
**Si la commande te résiste, répare l'APPEL — ne te rabats pas sur les sources.** C'est le réflexe
|
|
159
|
+
le plus cher, parce qu'il produit une réponse d'allure normale : un shell qui manque un outil
|
|
160
|
+
(`timeout` n'existe pas sur macOS), un `jq` mal formé, et l'on se replie sur ce qu'on sait lire.
|
|
161
|
+
Les fichiers répondront toujours quelque chose — mais pas à la question posée. Relance sans le
|
|
162
|
+
tube, puis remets ton filtre.
|
|
163
|
+
|
|
164
|
+
**Tu préfères des OUTILS à des commandes ?** Cette application les expose en Model Context Protocol
|
|
165
|
+
(`npx nodefony ai:mcp` écrit `.mcp.json`) — mêmes réponses, et tes propres modules peuvent publier
|
|
166
|
+
les leurs. La porte est une ROUTE : elle n'existe que serveur démarré, et un client qui la trouve
|
|
167
|
+
éteinte la marque en échec pour toute sa session — démarre l'application D'ABORD, ta session
|
|
168
|
+
ENSUITE. Tout le reste (déclarer un outil métier, le réserver à des scopes, l'autorisation OAuth) :
|
|
169
|
+
`node_modules/@nodefony/devkit/docs/index.md`.
|
|
170
|
+
|
|
171
|
+
### L'environnement : ne devine JAMAIS, demande
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npx nodefony env # cascade des .env, valeur EFFECTIVE de chaque variable, sa PROVENANCE
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Encadre toute modification de configuration par cette commande** : une fois AVANT, pour savoir ce
|
|
178
|
+
qui s'applique et d'où ça vient ; une fois APRÈS, pour prouver que ta valeur est celle qui gagne.
|
|
179
|
+
Lire les `.env` toi-même donne des contenus ; la précédence est un mécanisme — tu ne peux que la
|
|
180
|
+
supposer, et une supposition fausse ne se voit qu'en production. La commande ne boote rien, donc
|
|
181
|
+
elle répond aussi quand l'application ne démarre plus.
|
|
182
|
+
|
|
183
|
+
**Précédence, du plus FORT au plus faible** — le premier qui pose une valeur gagne, les suivants
|
|
184
|
+
sont ignorés en silence :
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
process.env > .env.<déploiement>.local > .env.<mode>.local > .env.local
|
|
188
|
+
> .env.<déploiement> > .env.<mode> > .env
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`<mode>` = `NODE_ENV` · `<déploiement>` = `APP_ENV` (plus spécifique, donc plus fort). Les `*.local`
|
|
192
|
+
ne sont jamais committés : les secrets y vont, et nulle part ailleurs.
|
|
193
|
+
|
|
194
|
+
| Forme | Ce que c'est | Où c'est déclaré |
|
|
195
|
+
| ------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------ |
|
|
196
|
+
| `NF_PORT=5151` | variable de l'APP, typée et validée | `env.ts` (`defineEnv`) — non déclarée = **sans effet** |
|
|
197
|
+
| `NF__HTTP__SERVERS__HTTPS__PORT=8443` | surcharge DIRECTE d'une clé de config d'un module | rien à déclarer — double `__` = séparateur |
|
|
198
|
+
| `NF_TOTP_KEY_FILE=/run/secrets/x` | la même variable, lue depuis un fichier (secret K8s) | idem `NF_TOTP_KEY` |
|
|
199
|
+
|
|
200
|
+
Une variable `NF_` mal orthographiée n'échoue pas : elle est **ignorée**, et le défaut s'applique en
|
|
201
|
+
silence. `npx nodefony env` est le seul endroit qui la montre, avec la correction probable.
|
|
202
|
+
|
|
203
|
+
## 4. Les commandes — demande la liste, ne la devine pas
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx nodefony --help # TOUTES les commandes, celles des modules installés comprises
|
|
207
|
+
npx nodefony <commande> --help # les options exactes de l'une d'elles
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
La liste **dépend des modules installés** : elle n'est pas la même d'une application à l'autre, et
|
|
211
|
+
elle s'allonge dès que tu en ajoutes un. C'est pour ça qu'elle se demande au lieu de se retenir.
|
|
212
|
+
|
|
213
|
+
**Toujours `npx`, jamais `nodefony` nu.** Le binaire vit dans les `node_modules` de CETTE
|
|
214
|
+
application : la forme nue rend un code 127 tant que rien n'est installé globalement. Une
|
|
215
|
+
installation globale existe (elle sert à créer une application HORS projet) et, dans un projet, elle
|
|
216
|
+
passe la main au binaire local — mais elle peut être plus ANCIENNE. `npx` prend la version que cette
|
|
217
|
+
application a choisie, sans dépendre de ce qui traîne sur la machine.
|
|
218
|
+
|
|
219
|
+
Celles qu'on n'invente pas, faute de savoir qu'elles existent :
|
|
220
|
+
|
|
221
|
+
- Mettre l'application derrière **nginx ou haproxy** — `npx nodefony proxy:generate <nginx|haproxy>`
|
|
222
|
+
- **Servir les fichiers statiques depuis un CDN** — `npx nodefony assets:publish [--clean]`
|
|
223
|
+
- **Certificat TLS de développement** — `npx nodefony http:certificates`
|
|
224
|
+
- **Dépendances en retard**, agrégées et non le brut de npm — `npx nodefony outdated`
|
|
225
|
+
- **Cohérence du projet** (classe non câblée, route qui répondra 404) — `npx nodefony doctor`
|
|
226
|
+
- **Plusieurs processus, un cœur chacun** — `npx nodefony production -w <n|auto>` · `NF_WORKERS`
|
|
227
|
+
- **Complétion au TAB** — `source <(nodefony completion zsh)`
|
|
228
|
+
|
|
229
|
+
Ce tableau ne remplace pas `--help` : lui seul connaît les modules de CETTE application, et il fait
|
|
230
|
+
foi le jour où les deux divergent.
|
|
231
|
+
|
|
232
|
+
## 5. Piloter le serveur — et l'ARRÊTER
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm run dev # développement : rechargement auto, Ctrl+C pour arrêter
|
|
236
|
+
npx nodefony development --no-watch # développement SANS rechargement : un process, stable
|
|
237
|
+
npx nodefony status # que tourne-t-il ? ports, PID — ne boote rien
|
|
238
|
+
npx nodefony stop # arrêt PROPRE de tout runtime de cette application
|
|
239
|
+
npx nodefony stop <nom|chemin> # arrêter un AUTRE projet, sans changer de dossier
|
|
240
|
+
npx nodefony production --detach --wait # boot réel en arrière-plan ; rend la main ports OUVERTS
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Arrête ce que tu démarres.** Un serveur laissé derrière garde les ports : le run suivant échoue
|
|
244
|
+
sur une erreur qui ne parle jamais de lui (`EADDRINUSE`) — ou pire, un test interroge l'ANCIENNE
|
|
245
|
+
version du code. Et **jamais `… &`** : le processus reçoit SIGHUP et meurt ; tuer le PID du port ne
|
|
246
|
+
tue pas le superviseur, qui respawne.
|
|
247
|
+
|
|
248
|
+
**Ces commandes ne voient QUE cette application.** Plusieurs projets Nodefony peuvent tourner sur la
|
|
249
|
+
même machine ; `status` ne compte jamais les processus du voisin comme les tiens, il les NOMME dans
|
|
250
|
+
une table à part — et ce nom est ce que `stop` accepte. Donc « aucune instance » veut dire « aucune
|
|
251
|
+
À MOI », pas « rien ne tourne » ; et une cible que `stop` ne peut pas désigner sans ambiguïté est
|
|
252
|
+
REFUSÉE, avec un code de sortie non nul et rien d'arrêté — **lis ce code**, un refus ressemble
|
|
253
|
+
sinon à un succès.
|
|
254
|
+
|
|
255
|
+
**Pour faire tourner une suite contre un serveur, prends `--no-watch`.** Le mode développement
|
|
256
|
+
relance le serveur dès qu'un fichier bouge : pendant un run, le redémarrage coupe les connexions
|
|
257
|
+
sous les tests, et le rouge qui en sort accuse le code alors que le fautif est le décor.
|
|
258
|
+
|
|
259
|
+
**N'invente pas d'attente** : `--wait` ne rend la main qu'une fois les ports en écoute — un `sleep`
|
|
260
|
+
arbitraire est soit trop court (test rouge sans raison), soit du temps perdu à chaque exécution.
|
|
261
|
+
|
|
262
|
+
## 6. Conduire une tâche — la séquence, et ses points d'arrêt
|
|
263
|
+
|
|
264
|
+
1. **Demande à l'application, ne déduis pas du code** (§3). Une route lue dans un fichier peut
|
|
265
|
+
n'être montée nulle part ; l'inverse aussi. **Un argument RESTREINT la réponse** — `inspect
|
|
266
|
+
routes auth` ne rend que les routes dont le chemin, le nom, le contrôleur, l'action, le module
|
|
267
|
+
ou les méthodes portent `auth`. Ne tronque JAMAIS une sortie d'inspection (`| head`) pour la
|
|
268
|
+
faire tenir : ce qu'on cherche est presque toujours dans la partie coupée — c'est ainsi qu'on
|
|
126
269
|
conclut « le framework ne fournit pas ça » et qu'on le réécrit à la main.
|
|
127
270
|
2. **Cherche la référence** (§2) avant de choisir une façade. Le framework en a presque toujours
|
|
128
271
|
une, et la contourner compile — c'est tout le piège.
|
|
@@ -137,7 +280,20 @@ ne porte pas). Elles supposent un serveur démarré et la porte câblée (`npx n
|
|
|
137
280
|
**Le point d'arrêt qu'on rate** : `npm test` seul ne prouve pas que ça compile — vitest
|
|
138
281
|
n'inspecte aucun type. Une application peut être verte et ne pas compiler.
|
|
139
282
|
|
|
140
|
-
|
|
283
|
+
### Le poids du modèle est un CHOIX, et il est mesuré
|
|
284
|
+
|
|
285
|
+
Si ton outil sait déléguer à des sous-agents : une tâche couverte par un **générateur** ne demande
|
|
286
|
+
pas un gros modèle — c'est le générateur qui porte le savoir. Mesuré sur ce framework, « ajoute une
|
|
287
|
+
ressource REST » rend le MÊME résultat en modèle léger et en modèle fort (mêmes contrôles verts,
|
|
288
|
+
écart d'étapes dans le bruit) pour **~3× moins cher**. À l'inverse, le socle SANS générateur (flux,
|
|
289
|
+
session, cycle de vie) fait échouer le modèle léger environ une fois sur deux.
|
|
290
|
+
|
|
291
|
+
Donc : **léger** pour appeler un générateur, inventorier, lire, vérifier un fait, appliquer un
|
|
292
|
+
patron ; **fort** pour écrire du socle sans générateur et pour arbitrer une architecture. Le test
|
|
293
|
+
qui tranche en une seconde : _la tâche a-t-elle une bonne réponse vérifiable ?_ Aucun nom de modèle
|
|
294
|
+
ici — ils changent tous les trimestres ; raisonne en poids.
|
|
295
|
+
|
|
296
|
+
## 7. Les pièges du serveur
|
|
141
297
|
|
|
142
298
|
Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers sont les plus fréquents.
|
|
143
299
|
|
|
@@ -154,6 +310,33 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
|
|
|
154
310
|
indolore sur les quelques lignes du poste de développement, fatal sur les dizaines de milliers
|
|
155
311
|
de la production. Le service d'une entité hérite `findPage({ limit: 25 })`.
|
|
156
312
|
|
|
313
|
+
- **Des dizaines de tests d'intégration rouges d'un coup** (`ECONNREFUSED`) : ils FRAPPENT un
|
|
314
|
+
serveur, ils ne le lancent pas — il est éteint. `npx nodefony status` d'abord ; en e2e, laisse
|
|
315
|
+
la commande gérer le cycle.
|
|
316
|
+
- **Ta route NEUVE répond 404 et le `dist/` est à jour** : elle n'est pas montée où tu crois. Le
|
|
317
|
+
chemin réel est le PRÉFIXE de son controller suivi du `path` de la route — une action
|
|
318
|
+
`path: "/widget"` dans un controller `@controller("/api")` répond sur `/api/widget`.
|
|
319
|
+
`npx nodefony inspect routes --json` donne le chemin MONTÉ.
|
|
320
|
+
- **TOUT répond 404, même les routes du gabarit** : un AUTRE serveur tient les ports — ou LE TIEN
|
|
321
|
+
a glissé, le port voulu étant pris. `npx nodefony status` montre les ports RÉELS, pas ceux que
|
|
322
|
+
tu as configurés, et NOMME le projet voisin.
|
|
323
|
+
- 🔴 **`nodefony <commande>` échoue là où `npm run dev` réussit, sur la MÊME application** : DEUX
|
|
324
|
+
paquets `nodefony` tournent dans le processus — un binaire lié ailleurs (installation globale,
|
|
325
|
+
`npm link`) exécute un noyau pendant que l'application importe le sien. Chaque copie a ses
|
|
326
|
+
propres classes, son contexte de requête et ses registres d'injection : un module construit à la
|
|
327
|
+
frontière perd son container SANS la moindre erreur — il figure dans la liste des modules et ne
|
|
328
|
+
fait rien. ⚠️ Les deux copies peuvent être la MÊME version ; deux fichiers distincts suffisent,
|
|
329
|
+
il n'y a aucune incohérence à repérer. `npm ls nodefony` liste les copies (« deduped » partout =
|
|
330
|
+
une seule) ; `NF_CLI_DEBUG=1 nodefony --version` dit quel CLI s'exécute.
|
|
331
|
+
- **`localhost` et `127.0.0.1` te jouent des tours** : ce sont deux ORIGINES distinctes — cookies,
|
|
332
|
+
cache et passkeys ne les partagent pas. Une seule origine en développement, partout, URL ouverte
|
|
333
|
+
comme callbacks.
|
|
334
|
+
|
|
335
|
+
- **Les routes authentifiées plafonnent quand le reste tient la charge** : le stockage de session
|
|
336
|
+
par défaut est SYNCHRONE — chaque reprise bloque la boucle d'événements. Compare une route
|
|
337
|
+
anonyme et une route authentifiée AVANT d'accuser TLS ou le pare-feu ; passe le stockage sur
|
|
338
|
+
redis pour la charge.
|
|
339
|
+
|
|
157
340
|
### Ce qui tue le processus, pas la requête
|
|
158
341
|
|
|
159
342
|
- 🔴 **Une promesse lancée sans `await` porte TOUJOURS un `.catch()`.** Audit, courriel,
|
|
@@ -185,7 +368,34 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
|
|
|
185
368
|
- **L'application ne démarre plus et le superviseur avale la sortie** :
|
|
186
369
|
`NF_DEV_CHILD=1 npx nodefony development` lance l'enfant seul et montre le crash brut.
|
|
187
370
|
|
|
188
|
-
##
|
|
371
|
+
## 8. Données et fichiers — les façades qu'on ne recompose pas
|
|
372
|
+
|
|
373
|
+
- **Un adaptateur de données ne remplace pas l'autre : ils se COMPLÈTENT.** Chacun déclare les
|
|
374
|
+
_stores_ qu'il sait tenir (`nodefony.stores` de son `package.json`) — `drizzle` les huit,
|
|
375
|
+
`mongoose` cinq (ni `totp`, ni `audit`, ni `idempotency`), `redis` quatre. Ce n'est pas un retard
|
|
376
|
+
de développement mais un CHOIX : un journal d'audit n'a rien à faire dans un moteur documentaire.
|
|
377
|
+
Ne promets jamais une parité qui n'existe pas — `npx nodefony inspect stores` dit où atterrit
|
|
378
|
+
chaque donnée.
|
|
379
|
+
- **Les violations de contrainte sont DÉJÀ traduites en HTTP — ne les rattrape pas.** Un doublon
|
|
380
|
+
sur une colonne unique ressort en **409**, une donnée qui viole le schéma Zod en **422**, chacun
|
|
381
|
+
avec son corps JSON : le rendu d'erreur lit le code du pilote (`23505`, `ER_DUP_ENTRY`,
|
|
382
|
+
`SQLITE_CONSTRAINT_UNIQUE`, `11000`) et le mappe, quel que soit le moteur. N'écris donc JAMAIS un
|
|
383
|
+
`throw … 409` pour un identifiant déjà pris. Le vérifier toi-même d'abord (« existe-t-il ? » puis
|
|
384
|
+
insertion) est plus lent ET **faux sous concurrence** : deux requêtes simultanées passent toutes
|
|
385
|
+
les deux le test avant que l'une n'écrive. La contrainte de la base est le seul arbitre exact.
|
|
386
|
+
- **Un fichier ne se sert pas à la main.** Trois façades, choisies sur l'usage :
|
|
387
|
+
`this.renderMediaStream(file)` implémente les **requêtes par plage** (`Range` → 206 +
|
|
388
|
+
`Content-Range`, 416 hors plage) — ce qu'exige un lecteur vidéo ou audio pour se déplacer ;
|
|
389
|
+
`this.streamFile(file)` envoie le fichier ENTIER en flux ; `this.renderFileDownload(file)` force
|
|
390
|
+
le téléchargement. Recomposer ça avec `createReadStream` et `response.write` compile, passe les
|
|
391
|
+
tests — et rend une réponse **incohérente** : un statut posé à la main n'atteint jamais la socket
|
|
392
|
+
(le pipeline écrit statut et en-têtes à SON tour), donc le client reçoit **200 avec un corps
|
|
393
|
+
partiel** et croit tenir le fichier complet. Mesuré au banc, pas supposé.
|
|
394
|
+
- **Un pod dont la base est en retard répond 503 sur `/readyz`** (jamais sur `/livez`) et reste
|
|
395
|
+
hors du répartiteur : c'est voulu, ce n'est pas une panne. `npx nodefony orm:migrate:status` dit
|
|
396
|
+
qui est en retard ; applique les migrations, les pods se mettent en service SEULS.
|
|
397
|
+
|
|
398
|
+
## 9. Les pièges du front
|
|
189
399
|
|
|
190
400
|
Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les quatre moteurs**
|
|
191
401
|
(React, Vue, Angular, Svelte) : le framework ne t'en impose aucun.
|
|
@@ -233,6 +443,20 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
|
|
|
233
443
|
seules, `role="img"` + `aria-label` sur un graphe SVG, jamais une information portée par la
|
|
234
444
|
**couleur seule**, `rel="noopener noreferrer"` sur les liens externes.
|
|
235
445
|
|
|
446
|
+
- **Une route d'API répond du HTML** : un repli SPA générique avale les routes voisines — le
|
|
447
|
+
premier motif qui correspond gagne. Repli en préfixe LITTÉRAL ; `npx nodefony inspect routes
|
|
448
|
+
--json` montre l'ordre réel.
|
|
449
|
+
- **Des utilisateurs « déconnectés au hasard »** : le traitement global « 401 = session expirée »
|
|
450
|
+
frappe aussi les sondes d'authentification, où 401 est NORMAL — et détruit une session valide.
|
|
451
|
+
Exempte les sondes du traitement global.
|
|
452
|
+
- **Ta page répond 200 et son script ne s'exécute pas** : la politique de contenu exige un `nonce`
|
|
453
|
+
sur les scripts, et le navigateur refuse un `<script>` en ligne qui n'en porte pas (« Refused to
|
|
454
|
+
execute inline script »). Un `curl` ne le voit JAMAIS, il ne lit que le corps. Signe le script
|
|
455
|
+
(valeur `this.context?.cspNonce`) ou sors-le dans un fichier servi ; ne desserre PAS la politique.
|
|
456
|
+
- **En production, la modification front n'apparaît jamais** : hors développement il n'y a PAS de
|
|
457
|
+
rechargement à chaud, et le manifeste est lu AU BOOT. `npm run build`, puis **redémarre le
|
|
458
|
+
serveur**, puis rechargement forcé.
|
|
459
|
+
|
|
236
460
|
### Le piège qui fait croire à un bug de code
|
|
237
461
|
|
|
238
462
|
- **Vite affirme qu'un export n'existe pas** (`does not provide an export named …`) alors qu'il
|
|
@@ -240,7 +464,7 @@ Ne concerne qu'une application qui a un frontend. Sauf mention, **vaut pour les
|
|
|
240
464
|
ajoutée, ou un `git pull`. C'est son cache : `rm -rf node_modules/.vite`, puis relance. Ne
|
|
241
465
|
purge pas sans raison, ça coûte 5 à 20 s de ré-optimisation.
|
|
242
466
|
|
|
243
|
-
##
|
|
467
|
+
## 10. Avant de dire « fait »
|
|
244
468
|
|
|
245
469
|
```bash
|
|
246
470
|
npm run verify # types + style + tests + câblage
|
|
@@ -94,6 +94,27 @@ reprendre après une coupure sans lire d'état préalable.
|
|
|
94
94
|
> dépose un fichier vide et son entrée de journal. Le gabarit déposé explique comment séparer les
|
|
95
95
|
> instructions ; suis-le à la lettre.
|
|
96
96
|
|
|
97
|
+
**En développement, les deux premiers gestes se disent en un seul** :
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx nodefony orm:generate --name ajout_slug --apply
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`--apply` ENCHAÎNE `orm:migrate` — il ne rejoue pas l'application pour son compte, donc toutes ses
|
|
104
|
+
gardes valent (refus du destructif sans `--allow-destructive`, verrou, historique). En `--json`, un
|
|
105
|
+
seul document sort : celui de l'application, qui porte le verdict. Le drapeau **n'existe qu'en
|
|
106
|
+
développement** et rien ne le déverrouille ailleurs : hors développement, la migration a été écrite,
|
|
107
|
+
relue et versionnée en amont, et c'est `orm:migrate` seul qui l'applique.
|
|
108
|
+
|
|
109
|
+
> **Le démarrage bascule tout seul, et il faut le savoir avant d'être surpris.** Tant que
|
|
110
|
+
> l'application ne versionne AUCUNE migration, le démarrage en développement dérive les tables
|
|
111
|
+
> depuis le code (`ddl: "auto"`) : on n'a rien à taper. Dès la première migration écrite, il
|
|
112
|
+
> applique les migrations (`ddl: "migrate"`) et cesse de dériver. C'est ce qui empêche deux
|
|
113
|
+
> fabricants du même schéma de se contredire — le DDL dérivé pose une unicité collée à la colonne,
|
|
114
|
+
> le générateur de migrations raisonne sur un index nommé, et la migration qui tente de retirer le
|
|
115
|
+
> second échoue sur un objet qui n'a jamais existé. Un `ddl` écrit dans la configuration gagne
|
|
116
|
+
> toujours ; sous `NODE_ENV=test`, rien ne change.
|
|
117
|
+
|
|
97
118
|
### 🔴 Un champ OBLIGATOIRE sur une table PEUPLÉE — ton moteur ne fait pas ce que tu crois
|
|
98
119
|
|
|
99
120
|
Ajouter une colonne `NOT NULL` **sans valeur par défaut** à une table qui porte déjà des lignes n'a
|
|
@@ -286,18 +307,19 @@ nodefony orm:migrate:status
|
|
|
286
307
|
Un refus n'est pas une panne : c'est le produit qui s'arrête devant une décision qui t'appartient.
|
|
287
308
|
Le `code` est stable — **lis-le, il désigne le geste**.
|
|
288
309
|
|
|
289
|
-
| Code | Ce qui s'est passé
|
|
290
|
-
| ---------------------------------- |
|
|
291
|
-
| `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique
|
|
292
|
-
| `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables
|
|
293
|
-
| `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `
|
|
310
|
+
| Code | Ce qui s'est passé | Le geste |
|
|
311
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
312
|
+
| `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique | `orm:migrate:baseline` (l'adopter) |
|
|
313
|
+
| `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables | `orm:migrate:baseline --from-database`, PUIS regénérer |
|
|
314
|
+
| `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent | `orm:migrate:baseline` sans option |
|
|
315
|
+
| `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>/` |
|
|
316
|
+
| `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | LIRE l'erreur, puis `orm:migrate:repair` |
|
|
317
|
+
| `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 |
|
|
318
|
+
| `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 |
|
|
319
|
+
| `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
|
|
320
|
+
| `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` |
|
|
321
|
+
| `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 |
|
|
322
|
+
| `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
|
|
301
323
|
|
|
302
324
|
Les codes exhaustifs, avec un exemple de charge utile pour chacun :
|
|
303
325
|
[`references/verdicts.md`](references/verdicts.md).
|
|
@@ -113,24 +113,25 @@ 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
|
|
117
|
-
|
|
|
118
|
-
| `NF_MIGRATE_UNKNOWN_CONNECTOR`
|
|
119
|
-
| `NF_MIGRATE_UNKNOWN_TAG`
|
|
120
|
-
| `NF_MIGRATE_UNKNOWN_SOURCE`
|
|
121
|
-
| `NF_MIGRATE_URL_MISMATCH`
|
|
122
|
-
| `NF_MIGRATE_NOT_CONFIGURED`
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
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_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
|
|
132
|
+
| `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 |
|
|
133
|
+
| `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 |
|
|
134
|
+
| `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
|
|
134
135
|
|
|
135
136
|
## Un contrat qui ne bougera pas
|
|
136
137
|
|
|
@@ -10,6 +10,7 @@ description: >
|
|
|
10
10
|
Déclencheurs : "protège cette route", "réserver aux administrateurs", "@IsGranted", "firewall",
|
|
11
11
|
"zone protégée", "403", "401", "un rôle qui en implique un autre", "roleHierarchy",
|
|
12
12
|
"un partenaire doit pouvoir poster", "erreur CSRF", "origine refusée", "@CsrfExempt",
|
|
13
|
+
"@CsrfProtect", "mon POST passe sans jeton", "faut-il protéger une écriture",
|
|
13
14
|
"API pour un programme", "clé d'API", "désactiver la sécurité pour tester".
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -42,6 +43,36 @@ Les deux se combinent : la zone décide **qui entre**, la garde décide **qui fa
|
|
|
42
43
|
async list() { … }
|
|
43
44
|
```
|
|
44
45
|
|
|
46
|
+
### Une zone peut exiger un rôle PAR DÉFAUT
|
|
47
|
+
|
|
48
|
+
Une zone qui ne liste que des authentificateurs exige une **identité**, rien de plus : toute route
|
|
49
|
+
qu'elle couvre est ouverte à **n'importe quel compte connecté**. Pour un espace d'administration,
|
|
50
|
+
c'est un défaut ouvert — l'oubli d'une garde s'y solde par une fuite.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
areas: {
|
|
54
|
+
backoffice: {
|
|
55
|
+
pattern: "^/back-office",
|
|
56
|
+
authenticators: ["session"],
|
|
57
|
+
roles: ["ROLE_ADMIN"], // ← exigé par défaut dans toute la zone
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Les rôles sont en **OU** — un seul suffit — et la hiérarchie s'applique. Un refus est un **403**
|
|
63
|
+
(connecté, mais pas autorisé), jamais un 401. Omettre la clé ne change rien au comportement
|
|
64
|
+
existant.
|
|
65
|
+
|
|
66
|
+
C'est un **défaut**, pas une exclusivité : une route qui porte sa propre garde (`@IsGranted`)
|
|
67
|
+
décide seule, ce qui laisse exister une page réservée à un autre rôle dans une zone par ailleurs
|
|
68
|
+
fermée. Écris donc le rôle **une fois sur la zone**, et ne le répète sur une route que pour en
|
|
69
|
+
exiger un autre.
|
|
70
|
+
|
|
71
|
+
🔴 **`@RequireScope` seul ne remplace PAS un rôle.** Un scope dit ce qu'une **clé déléguée** peut
|
|
72
|
+
faire en ton nom ; sur une session humaine il n'a aucune prise. Une route gardée par un scope seul
|
|
73
|
+
n'a donc rien décidé de ton identité — et le rôle de sa zone continue de s'appliquer, ce qui est
|
|
74
|
+
voulu. Les deux axes se cumulent, ils ne se substituent pas.
|
|
75
|
+
|
|
45
76
|
🔴 **Le `pattern` d'une zone est un PRÉFIXE, jamais la liste des routes du jour.**
|
|
46
77
|
|
|
47
78
|
```ts
|
|
@@ -54,6 +85,19 @@ pattern: "^/api/account/(profile|invoices)"; // ❌ les routes d'aujourd'hui
|
|
|
54
85
|
elle a l'air de couvrir l'espace, et l'introspection montre bien une route protégée à côté. Quand
|
|
55
86
|
des routes partagent un préfixe, ne les protège pas une par une.
|
|
56
87
|
|
|
88
|
+
## 🔴 La PROVENANCE n'est pas une PREUVE D'INTENTION — une mutation exige `@CsrfProtect`
|
|
89
|
+
|
|
90
|
+
Le raisonnement qui vient, et qui est faux : « le pare-feu vérifie déjà `Sec-Fetch-Site`, donc une
|
|
91
|
+
écriture est protégée ». Ces en-têtes sont posés par un NAVIGATEUR ; un programme qui parle en HTTP
|
|
92
|
+
n'en envoie aucun, et la défense de provenance le laisse alors passer — c'est son rôle, elle
|
|
93
|
+
distingue les sites, pas les intentions. Résultat mesuré : un `POST /api/cart/items` sans jeton rend
|
|
94
|
+
`201`, et l'application croit avoir une défense.
|
|
95
|
+
|
|
96
|
+
Toute action qui ÉCRIT porte donc `@CsrfProtect` explicitement. Le jeton ne se demande à AUCUN
|
|
97
|
+
endpoint : une requête sûre (`GET`) vers la route protégée sème le cookie lisible `csrf-token`, et
|
|
98
|
+
la mutation le rejoue dans l'en-tête `x-csrf-token` — c'est le double-submit, sinon `403`. La
|
|
99
|
+
provenance et le jeton se **cumulent** ; l'une ne remplace jamais l'autre.
|
|
100
|
+
|
|
57
101
|
## 🔴 Ce qu'il ne faut jamais écrire
|
|
58
102
|
|
|
59
103
|
```ts
|
|
@@ -92,10 +136,14 @@ travail. **Le geste juste est de DÉCLARER l'origine**, jamais de retirer la dé
|
|
|
92
136
|
|
|
93
137
|
```ts
|
|
94
138
|
csrf: {
|
|
139
|
+
secret: ctx.env.NF_CSRF_SECRET, // la clé que la config pose DÉJÀ — ne la perds pas
|
|
95
140
|
trustedOrigins: ["https://partenaire.example"],
|
|
96
141
|
}
|
|
97
142
|
```
|
|
98
143
|
|
|
144
|
+
⚠️ Ce bloc est fait pour être recopié dans `nodefony/config/security.ts`. Recopié **sans** son
|
|
145
|
+
`secret`, il coupe le jeton anti-falsification — en silence, tests verts.
|
|
146
|
+
|
|
99
147
|
🔴 `@CsrfExempt`, `csrf.enabled: false`, ou couper le contrôle de provenance **résolvent le
|
|
100
148
|
symptôme et ouvrent l'application** : n'importe quel site peut alors faire poster le navigateur
|
|
101
149
|
d'une personne connectée, à son insu et avec ses droits.
|
|
@@ -179,9 +227,16 @@ marche, les tests passent, et le diff ne contient aucune faute visible — il co
|
|
|
179
227
|
| `@BypassFirewall`, `@Anonymous` | la route sort de sa zone |
|
|
180
228
|
| `anonymous` ajouté aux authentificateurs d'une zone | toute la zone devient publique |
|
|
181
229
|
| `rateLimit: { enabled: false }` | le bourrage de mots de passe redevient gratuit |
|
|
230
|
+
| `areaRoleExempt: true` sur une route | elle perd le rôle par défaut de sa zone |
|
|
182
231
|
|
|
183
232
|
Relever un **seuil** est un réglage légitime. L'**éteindre** ne l'est pas.
|
|
184
233
|
|
|
234
|
+
Le dernier de la liste est le plus trompeur : son nom dit bien ce qu'il fait — il **retire** le rôle
|
|
235
|
+
exigé par la zone —, mais il n'installe rien à la place. Posé sur une route qui ne décide pas
|
|
236
|
+
elle-même de son autorisation, il la rend accessible à tout compte connecté, sans erreur ni trace.
|
|
237
|
+
Il est **réservé aux mécanismes internes du framework** ; ce que tu veux, quand une route de ta zone
|
|
238
|
+
doit être plus ouverte, c'est déclarer le rôle qu'elle exige vraiment.
|
|
239
|
+
|
|
185
240
|
## Prouver — trois identités, pas une
|
|
186
241
|
|
|
187
242
|
Le refus d'un anonyme est gratuit : n'importe quelle zone le donne. Ce qui prouve, c'est le
|
|
@@ -195,6 +250,20 @@ npx nodefony security:user:add # un témoin SANS le rôle
|
|
|
195
250
|
npx nodefony inspect routes --json # la garde est-elle sur la route qu'on croit ?
|
|
196
251
|
```
|
|
197
252
|
|
|
253
|
+
## Utilisateurs et droits : tout existe, n'improvise RIEN
|
|
254
|
+
|
|
255
|
+
Chaque geste ci-dessus a sa référence INSTALLÉE — une recherche ordinaire ne la voit pas, `rg`
|
|
256
|
+
ne descendant pas dans `node_modules` :
|
|
257
|
+
|
|
258
|
+
- **Zones, authentificateurs, CSRF, CORS, clés d'API** — `node_modules/@nodefony/security/docs/firewall.md`
|
|
259
|
+
- **`@IsGranted`, voters, hiérarchie de rôles** — `node_modules/@nodefony/security/docs/authorization.md`
|
|
260
|
+
- **Contrat `IUser`, `UserService`, mot de passe** — `node_modules/@nodefony/user/docs/index.md`
|
|
261
|
+
- **Politique de contenu, nonce, HSTS** — `node_modules/@nodefony/security/docs/headers.md`
|
|
262
|
+
|
|
263
|
+
N'écris pas ton propre lecteur de session, ne teste pas l'appartenance à un rôle à la main, et
|
|
264
|
+
n'insère jamais un utilisateur directement en base — le mot de passe passe par l'encodeur du
|
|
265
|
+
framework.
|
|
266
|
+
|
|
198
267
|
## Voisins
|
|
199
268
|
|
|
200
269
|
| Besoin | Skill |
|