@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.
Files changed (67) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +318 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
  9. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
  10. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
  11. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
  12. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
  15. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  16. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  17. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  18. package/dist/index.js +47 -0
  19. package/dist/nodefony/command/CardCommand.js +70 -0
  20. package/dist/nodefony/config/config.js +200 -0
  21. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  22. package/dist/nodefony/controllers/DevkitController.js +60 -0
  23. package/dist/nodefony/controllers/McpController.js +223 -0
  24. package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
  25. package/dist/nodefony/interfaces/IDevkitService.js +1 -0
  26. package/dist/nodefony/interfaces/index.js +1 -0
  27. package/dist/nodefony/service/DevkitService.js +198 -0
  28. package/dist/nodefony/src/card.js +2 -0
  29. package/dist/nodefony/src/errors/DevkitError.js +21 -0
  30. package/dist/nodefony/src/mcp/guard.js +51 -0
  31. package/dist/nodefony/src/mcp/protocol.js +127 -0
  32. package/dist/nodefony/src/mcp/server.js +133 -0
  33. package/dist/nodefony/src/mcp/tools.js +163 -0
  34. package/dist/types/index.d.ts +52 -0
  35. package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
  36. package/dist/types/nodefony/config/config.d.ts +25 -0
  37. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  38. package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
  39. package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
  40. package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
  41. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  43. package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
  44. package/dist/types/nodefony/src/card.d.ts +18 -0
  45. package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
  46. package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
  47. package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
  48. package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
  49. package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
  50. package/docs/index.md +358 -0
  51. package/package.json +77 -0
  52. package/skills/nodefony-add-crud/SKILL.md +199 -0
  53. package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
  54. package/skills/nodefony-add-service/SKILL.md +90 -0
  55. package/skills/nodefony-browser/SKILL.md +416 -0
  56. package/skills/nodefony-browser/references/socket.md +115 -0
  57. package/skills/nodefony-browser/references/sondes.md +175 -0
  58. package/skills/nodefony-browser/scripts/audit.mjs +169 -0
  59. package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
  60. package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
  61. package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
  62. package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
  63. package/skills/nodefony-browser/scripts/socket.mjs +354 -0
  64. package/skills/nodefony-browser/scripts/watch.mjs +125 -0
  65. package/skills/nodefony-migrate-schema/SKILL.md +359 -0
  66. package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
  67. 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` |