@nodefony/devkit 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +318 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +47 -0
- package/dist/nodefony/command/CardCommand.js +70 -0
- package/dist/nodefony/config/config.js +200 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controllers/DevkitController.js +60 -0
- package/dist/nodefony/controllers/McpController.js +223 -0
- package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
- package/dist/nodefony/interfaces/IDevkitService.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DevkitService.js +198 -0
- package/dist/nodefony/src/card.js +2 -0
- package/dist/nodefony/src/errors/DevkitError.js +21 -0
- package/dist/nodefony/src/mcp/guard.js +51 -0
- package/dist/nodefony/src/mcp/protocol.js +127 -0
- package/dist/nodefony/src/mcp/server.js +133 -0
- package/dist/nodefony/src/mcp/tools.js +163 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
- package/dist/types/nodefony/config/config.d.ts +25 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
- package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
- package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
- package/dist/types/nodefony/src/card.d.ts +18 -0
- package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
- package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
- package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
- package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
- package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
- package/docs/index.md +358 -0
- package/package.json +77 -0
- package/skills/nodefony-add-crud/SKILL.md +199 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
- package/skills/nodefony-add-service/SKILL.md +90 -0
- package/skills/nodefony-browser/SKILL.md +416 -0
- package/skills/nodefony-browser/references/socket.md +115 -0
- package/skills/nodefony-browser/references/sondes.md +175 -0
- package/skills/nodefony-browser/scripts/audit.mjs +169 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
- package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
- package/skills/nodefony-browser/scripts/socket.mjs +354 -0
- package/skills/nodefony-browser/scripts/watch.mjs +125 -0
- package/skills/nodefony-migrate-schema/SKILL.md +359 -0
- package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
- package/skills/nodefony-protect-route/SKILL.md +195 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodefony-protect-route
|
|
3
|
+
description: >
|
|
4
|
+
Réserve une route d'une application Nodefony aux personnes habilitées, par les briques du
|
|
5
|
+
framework plutôt que par un contrôle écrit à la main dans l'action. Porte les deux étages
|
|
6
|
+
(zone du pare-feu et garde par route), la hiérarchie de rôles qui évite d'attribuer un rôle de
|
|
7
|
+
plus, la façon d'ouvrir une route à un partenaire sans démonter la défense anti-falsification,
|
|
8
|
+
et les gestes qui affaiblissent l'application en silence. À charger AVANT de poser une garde,
|
|
9
|
+
d'ouvrir une route à un tiers, ou de toucher à la configuration de sécurité.
|
|
10
|
+
Déclencheurs : "protège cette route", "réserver aux administrateurs", "@IsGranted", "firewall",
|
|
11
|
+
"zone protégée", "403", "401", "un rôle qui en implique un autre", "roleHierarchy",
|
|
12
|
+
"un partenaire doit pouvoir poster", "erreur CSRF", "origine refusée", "@CsrfExempt",
|
|
13
|
+
"API pour un programme", "clé d'API", "désactiver la sécurité pour tester".
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# protect-route — la garde vient du framework, jamais de l'action
|
|
17
|
+
|
|
18
|
+
> ⚖️ **La confiance n'exclut pas le contrôle.** Une route qui « répond 403 quand je teste » n'est
|
|
19
|
+
> pas une route protégée. Le seul juge est un appel avec **trois identités**.
|
|
20
|
+
|
|
21
|
+
## Deux étages, et ils ne font pas la même chose
|
|
22
|
+
|
|
23
|
+
| Étage | Où | Ce qu'il protège |
|
|
24
|
+
| -------------------- | --------------------------------------- | ----------------------------------------- |
|
|
25
|
+
| **zone du pare-feu** | `nodefony.config.ts`, `firewalls.areas` | un **espace** : `pattern: "^/api/secure"` |
|
|
26
|
+
| **garde par route** | `@IsGranted("ROLE_X")` sur l'action | **une** route précise |
|
|
27
|
+
|
|
28
|
+
Les deux se combinent : la zone décide **qui entre**, la garde décide **qui fait**.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
@Get("/api/reports")
|
|
32
|
+
@IsGranted("ROLE_REPORTS")
|
|
33
|
+
async list() { … }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
🔴 **Le `pattern` d'une zone est un PRÉFIXE, jamais la liste des routes du jour.**
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
pattern: "^/api/account"; // ✅ l'espace
|
|
40
|
+
pattern: "^/api/account/(profile|invoices)"; // ❌ les routes d'aujourd'hui
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Énumérer marche à l'essai et passe la revue. Puis quelqu'un ajoute
|
|
44
|
+
`/api/account/payment-methods` — et elle **naît publique**. Rien ne le signale : la zone existe,
|
|
45
|
+
elle a l'air de couvrir l'espace, et l'introspection montre bien une route protégée à côté. Quand
|
|
46
|
+
des routes partagent un préfixe, ne les protège pas une par une.
|
|
47
|
+
|
|
48
|
+
## 🔴 Ce qu'il ne faut jamais écrire
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// ❌ contrôle artisanal — invisible au pare-feu, à l'audit et à l'introspection
|
|
52
|
+
if (!this.context?.user?.roles.includes("ROLE_ADMIN")) {
|
|
53
|
+
return this.renderJson({ error: "forbidden" }, 403);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Le framework refuse **avant** d'entrer dans l'action. Un test écrit dans l'action s'oublie sur la
|
|
58
|
+
route suivante, ne se voit pas dans `inspect routes`, et ne protège rien qu'on n'ait pensé à
|
|
59
|
+
protéger.
|
|
60
|
+
|
|
61
|
+
## Un rôle qui en implique un autre
|
|
62
|
+
|
|
63
|
+
Un administrateur doit pouvoir consulter la facturation **sans** qu'on lui attribue un rôle de
|
|
64
|
+
plus. Ça se déclare une fois, dans le manifeste :
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
roleHierarchy: {
|
|
68
|
+
ROLE_ADMIN: ["ROLE_BILLING", "ROLE_REPORTS"],
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Deux gestes rendent la même réponse sur la route que tu mesures, et aucun des deux ne
|
|
73
|
+
généralise** : recopier le rôle sur le compte administrateur au moment du semis — ça marche pour
|
|
74
|
+
ce compte-là et pour aucun autre — et énumérer les rôles sur l'action,
|
|
75
|
+
`@IsGranted(["ROLE_BILLING", "ROLE_ADMIN"])`, où un attribut accordé suffit. Dans les deux cas la
|
|
76
|
+
relation entre les rôles n'existe nulle part : la route suivante devra répéter la liste, et
|
|
77
|
+
l'oubli ne se voit sur aucune route — c'est une ABSENCE, elle ne se relit pas dans un diff.
|
|
78
|
+
|
|
79
|
+
## Ouvrir à un partenaire sans démonter la défense
|
|
80
|
+
|
|
81
|
+
Une origine tierce qui poste reçoit un refus : c'est la défense anti-falsification qui fait son
|
|
82
|
+
travail. **Le geste juste est de DÉCLARER l'origine**, jamais de retirer la défense :
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
csrf: {
|
|
86
|
+
trustedOrigins: ["https://partenaire.example"],
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
🔴 `@CsrfExempt`, `csrf.enabled: false`, ou couper le contrôle de provenance **résolvent le
|
|
91
|
+
symptôme et ouvrent l'application** : n'importe quel site peut alors faire poster le navigateur
|
|
92
|
+
d'une personne connectée, à son insu et avec ses droits.
|
|
93
|
+
|
|
94
|
+
Deux précisions qui décident du résultat :
|
|
95
|
+
|
|
96
|
+
- la comparaison porte sur l'origine **ENTIÈRE** (`scheme://host[:port]`) — ni joker, ni
|
|
97
|
+
sous-domaine implicite : **une origine par entrée**, et `https://x.example` ne couvre pas
|
|
98
|
+
`https://api.x.example` ;
|
|
99
|
+
- `cors.origins` n'est **pas** la même clé et ne remplace pas celle-ci : elle autorise EN PLUS
|
|
100
|
+
le JS du tiers à **lire** tes réponses. Un partenaire qui POSTE n'en a pas besoin — et les deux
|
|
101
|
+
se traversent sans se suppléer.
|
|
102
|
+
|
|
103
|
+
Détail : `node_modules/@nodefony/security/docs/csrf.md`.
|
|
104
|
+
|
|
105
|
+
## Créer un compte
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx nodefony security:user:add <identifiant>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**N'insère jamais un utilisateur directement en base** : le mot de passe doit passer par
|
|
112
|
+
l'encodeur du framework. Une ligne posée à la main produit un compte qui ne pourra pas se
|
|
113
|
+
connecter — ou pire, un mot de passe stocké en clair.
|
|
114
|
+
|
|
115
|
+
## Lire l'utilisateur courant
|
|
116
|
+
|
|
117
|
+
Le paramètre décoré `@CurrentUser()` (typé `IUser` de `@nodefony/user`). L'identité est
|
|
118
|
+
**ré-résolue à chaque requête** : les rôles sont frais, et une révocation prend effet tout de
|
|
119
|
+
suite. N'écris pas ton propre lecteur de session — le tien lira un instantané.
|
|
120
|
+
|
|
121
|
+
## Un droit métier qui ne se réduit pas à un rôle
|
|
122
|
+
|
|
123
|
+
« L'auteur peut éditer SON document » ne s'exprime pas avec un rôle : la réponse dépend de
|
|
124
|
+
l'objet. Ça s'écrit en **voter**, enregistré par `registerVoterFactory`, et appelé par la garde
|
|
125
|
+
habituelle :
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
@IsGranted("doc.edit", { subject: "id" })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
C'est le point d'extension prévu — il n'y a **pas** de table de permissions à inventer, ni de test
|
|
132
|
+
d'appartenance à écrire dans l'action.
|
|
133
|
+
|
|
134
|
+
## Une API pour un PROGRAMME, pas pour un navigateur
|
|
135
|
+
|
|
136
|
+
Un service partenaire, un script, un agent ne stockent aucun cookie. **La zone est déjà posée**
|
|
137
|
+
dans le `nodefony.config.ts` généré :
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
machine: {
|
|
141
|
+
pattern: "^/api/machine",
|
|
142
|
+
authenticators: ["apikey"], // PAS "session" — ce client n'a pas de cookie
|
|
143
|
+
stateless: true, // false ⇒ un registre de sessions que ce client ne relit pas
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Fais donc **tomber ta route sous `/api/machine`** plutôt que d'ajouter une zone : celle-ci est
|
|
148
|
+
déjà réglée, et une seconde zone au pattern plus court la coifferait sans prévenir — le pare-feu
|
|
149
|
+
trie par longueur de pattern.
|
|
150
|
+
|
|
151
|
+
⚠️ `stateless: false` (le défaut) **ne fait pas échouer l'essai**, et c'est tout le piège : depuis
|
|
152
|
+
un navigateur ou un `curl -c`, le cookie posé revient aux requêtes suivantes et tout semble
|
|
153
|
+
marcher. Ce que ça coûte n'est pas un refus mais un **registre** — chaque appel portant un cookie
|
|
154
|
+
inconnu fait reprendre puis réécrire une session serveur, et renvoyer un `Set-Cookie`, pour un
|
|
155
|
+
appelant qui ne la relira jamais. `stateless: true` ferme cela : la zone n'ouvre ni ne reprend de
|
|
156
|
+
session, et le cookie entrant est ignoré. Lister `"session"` dans une zone stateless est une
|
|
157
|
+
contradiction, et l'application **refuse de démarrer** en nommant la zone. Règle : **un appelant
|
|
158
|
+
qui ne stocke pas de cookie ne doit rien recevoir qu'il faille stocker.**
|
|
159
|
+
|
|
160
|
+
Les clés s'émettent par `POST /nodefony/security/api/keys`.
|
|
161
|
+
|
|
162
|
+
## Les gestes qui affaiblissent en silence
|
|
163
|
+
|
|
164
|
+
Bloqué par une garde en résolvant autre chose, le réflexe est de la retirer. La fonctionnalité
|
|
165
|
+
marche, les tests passent, et le diff ne contient aucune faute visible — il contient un manque.
|
|
166
|
+
|
|
167
|
+
| À ne pas faire | Ce que ça ouvre |
|
|
168
|
+
| --------------------------------------------------- | ---------------------------------------------- |
|
|
169
|
+
| `'unsafe-inline'` dans `script-src` | l'exécution de n'importe quel script injecté |
|
|
170
|
+
| `@BypassFirewall`, `@Anonymous` | la route sort de sa zone |
|
|
171
|
+
| `anonymous` ajouté aux authentificateurs d'une zone | toute la zone devient publique |
|
|
172
|
+
| `rateLimit: { enabled: false }` | le bourrage de mots de passe redevient gratuit |
|
|
173
|
+
|
|
174
|
+
Relever un **seuil** est un réglage légitime. L'**éteindre** ne l'est pas.
|
|
175
|
+
|
|
176
|
+
## Prouver — trois identités, pas une
|
|
177
|
+
|
|
178
|
+
Le refus d'un anonyme est gratuit : n'importe quelle zone le donne. Ce qui prouve, c'est le
|
|
179
|
+
**deuxième** appel :
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npx nodefony security:user:add # un témoin SANS le rôle
|
|
183
|
+
# 1. anonyme → refusé (401 ou 403 : les deux sont justes)
|
|
184
|
+
# 2. connecté SANS rôle → refusé ← celui-ci porte l'information
|
|
185
|
+
# 3. administrateur → servi
|
|
186
|
+
npx nodefony inspect routes --json # la garde est-elle sur la route qu'on croit ?
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Voisins
|
|
190
|
+
|
|
191
|
+
| Besoin | Skill |
|
|
192
|
+
| ------------------------------ | ------------------------------- |
|
|
193
|
+
| Une ressource complète stockée | `nodefony-add-crud` |
|
|
194
|
+
| Un service métier injectable | `nodefony-add-service` |
|
|
195
|
+
| Un flux temps réel réservé | `nodefony-add-realtime-channel` |
|