@nodefony/devkit 10.0.0-beta.2 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +27 -163
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/card.js +1 -2
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +217 -20
- package/package.json +17 -12
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
- package/skills/nodefony-add-service/SKILL.md +14 -3
- package/skills/nodefony-authentication/SKILL.md +234 -0
- package/skills/nodefony-browser/SKILL.md +65 -21
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/references/sondes.md +13 -0
- package/skills/nodefony-browser/scripts/audit.mjs +59 -10
- package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
- package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
- package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +78 -82
- package/skills/nodefony-dev/scripts/docs.mjs +24 -8
- package/skills/nodefony-devops/SKILL.md +98 -74
- package/skills/nodefony-devops/references/compose.md +8 -0
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +282 -48
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +30 -5
- package/skills/nodefony-devops/references/variables.md +66 -31
- package/skills/nodefony-migrate-schema/SKILL.md +5 -2
- package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
- package/skills/nodefony-protect-route/SKILL.md +124 -21
- package/skills/nodefony-security-review/SKILL.md +127 -0
- package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
- package/skills/nodefony-troubleshoot/SKILL.md +225 -0
- package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
- package/skills/nodefony-upgrade/SKILL.md +130 -0
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/README.md
CHANGED
|
@@ -75,13 +75,64 @@ navigateur — d'où la commande, qui reste la porte utile.
|
|
|
75
75
|
npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Neuf outils : **`nodefony_inspect`** (ce qui est monté), **`nodefony_check`**
|
|
79
79
|
(ce qui manque — la commande s'appelle `doctor`), **`nodefony_symbols`** (ce qu'une
|
|
80
80
|
API du framework signifie), **`nodefony_card`** (par où commencer),
|
|
81
81
|
**`nodefony_docs`** (la documentation des modules installés), **`nodefony_admin_list`**
|
|
82
82
|
et **`nodefony_admin_call`** (lectures du data plane d'administration, jeton porteur
|
|
83
|
-
du scope `admin:read` exigé)
|
|
84
|
-
source, plusieurs portes.
|
|
83
|
+
du scope `admin:read` exigé), **`nodefony_observe`** (ce que l'application fait réellement de ses requêtes, mesuré pendant qu'elle tourne) et **`nodefony_profile_route`** (une route mesurée par l'outil lui-même, au nom d'un compte s'il le faut). Ce sont les mêmes
|
|
84
|
+
briques que les commandes — une source, plusieurs portes.
|
|
85
|
+
|
|
86
|
+
### Mesurer l'application qui tourne — `nodefony_observe`
|
|
87
|
+
|
|
88
|
+
L'outil observe l'application pendant une fenêtre de 1 à 50 secondes (10 par
|
|
89
|
+
défaut) et rend ce que son profileur de développement a mesuré : où part le
|
|
90
|
+
temps de chaque route, les requêtes SQL répétées dans une boucle (N+1), les
|
|
91
|
+
erreurs `5xx` avec leurs lignes de journal, et les blocages de la boucle
|
|
92
|
+
d'événements. **Il ne génère aucun trafic** : on ouvre la page, on joue les
|
|
93
|
+
tests ou on envoie des requêtes pendant que la fenêtre court.
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// tools/call
|
|
97
|
+
{ "name": "nodefony_observe", "arguments": { "durationSec": 10 } }
|
|
98
|
+
// optionnel : "route": "<nom de la route>", "includeTooling": true
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Extrait d'un rapport réel (une route qui plante, sur une application démarrée
|
|
102
|
+
depuis dix secondes) :
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
Verdict : anomalies (2) — observation complète
|
|
106
|
+
Fenêtre : 6.0 s sur 6.0 s · 3 relevé(s) · 39 requête(s) (39 HTTP, 0 WebSocket) · 0 perdue(s) · 0 de l'outillage écartée(s)
|
|
107
|
+
Process : pid 46402, démarré il y a 10 s · boucle d'événements : retard moyen 4.0 ms, pire blocage 665 ms · tas -10.0 Mo (indicatif, sans GC)
|
|
108
|
+
|
|
109
|
+
Anomalies (les plus coûteuses d'abord) :
|
|
110
|
+
1. Erreurs — route `crash-sync` (DefaultController.crashSync) : 13 sur 13 — « simulated sync crash ». requestId : d706bfef-…, 577adac2-…, 207f8121-… — journal ci-dessous.
|
|
111
|
+
2. Boucle d'événements bloquée — jusqu'à 665 ms d'un tenant (retard moyen 4.0 ms) : […] Le process a démarré il y a 10 s : ce blocage peut venir du démarrage […]
|
|
112
|
+
|
|
113
|
+
Journal des requêtes en erreur (syslog de l'application, lu au relevé qui les a vues) :
|
|
114
|
+
- requestId d706bfef-… — route `crash-sync`, statut 500 :
|
|
115
|
+
ERROR [req] GET 500 http://127.0.0.1:5151/nodefony/test/crash/sync 1.4ms 127.0.0.1 [d706bfef] simulated sync crash
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
C'est un outil **long** : un instantané part en progression toutes les deux
|
|
119
|
+
secondes, un client qui l'annule l'arrête au relevé suivant — qu'il ferme le flux de la réponse (révision courante de MCP) ou envoie une notification d'annulation (révision précédente, celle du SDK officiel) —, et un
|
|
120
|
+
rechargement du serveur pendant la fenêtre rend un rapport partiel qui le dit.
|
|
121
|
+
Ce qu'il faut savoir pour s'en servir sans conclure à tort :
|
|
122
|
+
|
|
123
|
+
- les durées et le temps « par requête » portent sur les requêtes **servies** ;
|
|
124
|
+
les refus `401`/`403` sont comptés à part ;
|
|
125
|
+
- `action.self` est le temps de l'action et des services qu'elle appelle, une
|
|
126
|
+
fois retirés le rendu, l'initialisation et le SQL ; « SQL (cumul) » additionne
|
|
127
|
+
les requêtes, même lancées en parallèle ;
|
|
128
|
+
- les valeurs SQL sont masquées (`?`) : le rapport montre la forme d'une requête,
|
|
129
|
+
jamais une donnée ; le journal n'en garde que les lignes WARNING et plus graves ;
|
|
130
|
+
- le journal des erreurs n'est lu que pour un appel **authentifié** (scope
|
|
131
|
+
`admin:read`) ; les routes `/nodefony/…` (outillage) sont écartées, sauf
|
|
132
|
+
`includeTooling`.
|
|
133
|
+
|
|
134
|
+
La façon de lire chaque anomalie et le geste qui la corrige sont dans le skill
|
|
135
|
+
d'agent `nodefony-troubleshoot` (§ « Mesurer l'application qui tourne »).
|
|
85
136
|
|
|
86
137
|
**Ce n'est pas un process de plus.** Depuis la révision `2026-07-28` du transport
|
|
87
138
|
« Streamable HTTP », un serveur MCP est un simple endpoint `POST` sans session :
|
|
@@ -89,6 +140,15 @@ c'est donc une **route de votre application**. Elle n'existe que pendant qu'elle
|
|
|
89
140
|
tourne, suit chaque rechargement du serveur de développement, et n'a aucun cache
|
|
90
141
|
à invalider.
|
|
91
142
|
|
|
143
|
+
**Et sans serveur ?** Un agent qui travaille là où personne n'a lancé
|
|
144
|
+
`nodefony dev` — en CI, dans le runner d'un agent cloud — ne joint pas cette
|
|
145
|
+
route. Il lance lui-même `npx nodefony ai:stdio` (commande de `@nodefony/ai`) et
|
|
146
|
+
lui parle sur l'entrée et la sortie standard : mêmes outils, mais le code du
|
|
147
|
+
démarrage, sans rechargement à chaud, et aucun trafic à mesurer —
|
|
148
|
+
`nodefony_observe` et `nodefony_profile_route` le disent et renvoient ici. En
|
|
149
|
+
local, la route reste le bon choix. Les deux transports, côte à côte :
|
|
150
|
+
[`@nodefony/ai` — Deux transports](https://github.com/nodefony/nodefony-core/blob/main/src/packages/@nodefony/ai/docs/mcp.md#-deux-transports--http-ou-stdio).
|
|
151
|
+
|
|
92
152
|
**Ce qui la protège**, et il faut le savoir avant de s'étonner d'un `403` :
|
|
93
153
|
|
|
94
154
|
- toute **adresse non locale** est refusée (`mcp.allowRemote`) ;
|
|
@@ -100,6 +160,76 @@ tourne, suit chaque rechargement du serveur de développement, et n'a aucun cach
|
|
|
100
160
|
- les outils sont en **lecture seule**, et la liste est une allowlist
|
|
101
161
|
(`mcp.tools`).
|
|
102
162
|
|
|
163
|
+
### Mesurer une route — `nodefony_profile_route`
|
|
164
|
+
|
|
165
|
+
Là où `nodefony_observe` attend le trafic, `nodefony_profile_route` le **produit** :
|
|
166
|
+
il envoie lui-même N requêtes `GET` (ou `HEAD`) sur un chemin de l'application,
|
|
167
|
+
retrouve leurs profils par l'identifiant qu'il pose (`x-request-id`) et rend le
|
|
168
|
+
même rapport. C'est la boucle « je modifie → je mesure » en un appel.
|
|
169
|
+
|
|
170
|
+
```jsonc
|
|
171
|
+
// tools/call
|
|
172
|
+
{
|
|
173
|
+
"name": "nodefony_profile_route",
|
|
174
|
+
"arguments": { "path": "/orders?page=2", "count": 50 },
|
|
175
|
+
}
|
|
176
|
+
// optionnel : "concurrency" (défaut 4, max 8), "warmup" (défaut 3, hors mesures),
|
|
177
|
+
// "method": "HEAD", "as": "<identifiant>", "anonymous": true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`path` est un **chemin**, jamais une URL : hôte, schéma et `//` sont refusés,
|
|
181
|
+
l'outil ne vise que l'application elle-même. Au plus 200 requêtes par appel —
|
|
182
|
+
sous les 500 profils que garde le profileur — et 45 secondes de tir.
|
|
183
|
+
|
|
184
|
+
**Au nom de qui.** Une page protégée vue sans compte ne rend que des refus.
|
|
185
|
+
Quand le jeton MCP porte le scope `admin:write`, l'outil joue son
|
|
186
|
+
**propriétaire** : il ouvre une vraie session à son nom par le flux de connexion
|
|
187
|
+
de l'application (un ticket à usage unique, présenté en boucle locale à
|
|
188
|
+
`POST /nodefony/devkit/profile-session`), tire avec, puis la détruit — l'audit
|
|
189
|
+
l'écrit sous la raison `devkit`. `as` joue un autre compte, à condition que ses
|
|
190
|
+
rôles soient inclus dans ceux du propriétaire, hiérarchie comprise : jouer un
|
|
191
|
+
compte ne monte jamais en droits ; sans `admin:write`, `as` reçoit sur une porte
|
|
192
|
+
protégée un défi `403 insufficient_scope` qui nomme le scope à demander. Sans ce
|
|
193
|
+
scope, l'outil joue l'anonyme et le rapport donne la commande :
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npx nodefony security:mcp:token --scope "admin:read admin:write" --write
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Extrait de deux appels réels, avant puis après la correction d'un N+1 (la même
|
|
200
|
+
lecture, une requête par compte, puis une seule requête `$in`) :
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
Verdict : anomalies (1) — observation complète
|
|
204
|
+
Tir : 30/30 requête(s) GET /nodefony/test/db/n-plus-one · concurrence 4 · 106 ms · chauffe de 3 requête(s) hors mesures (p50 1.5 ms, max 2.5 ms)
|
|
205
|
+
Joué en tant que : `admin` (ROLE_ADMIN, ROLE_NODEFONY_ADMIN, ROLE_DEV, ROLE_SUPERVISOR)
|
|
206
|
+
Reçu par le client : 2xx 30 · 3xx 0 · 401 0 · 403 0 · autres 4xx 0 · 5xx 0 · sans réponse 0
|
|
207
|
+
|
|
208
|
+
Anomalies (les plus coûteuses d'abord) :
|
|
209
|
+
1. N+1 — route `DbController::nPlusOne` (DbController.nPlusOne) : `select … from "User" where "User"."id" = ? …` répétée ×16 dans une requête (30 requête(s) touchée(s), 14 ms cumulés). […] Remède habituel : charger la relation en une requête (`relations`, `$in`).
|
|
210
|
+
|
|
211
|
+
route | type | n | p50 | p95 | max | 2xx/3xx/4xx/5xx | SQL par requête moy/max | temps par requête
|
|
212
|
+
`DbController::nPlusOne` (DbController.nPlusOne) | http | 30 | 1.2 ms | 1.7 ms | 1.7 ms | 30/0/0/0 | 18.0/18 | SQL (cumul) 0.7 ms · action.self 0.2 ms · send 0.1 ms
|
|
213
|
+
|
|
214
|
+
— après correction —
|
|
215
|
+
Verdict : ok — observation complète
|
|
216
|
+
`DbController::nPlusOneFixed` (DbController.nPlusOneFixed) | http | 30 | 0.7 ms | 0.9 ms | 2.1 ms | 30/0/0/0 | 3.0/3 | SQL (cumul) 0.4 ms · send 0.1 ms · action.self 0.1 ms
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Ce que le rapport dit **en tête**, avant tout chiffre : les requêtes refusées
|
|
220
|
+
(`401`/`403` — verdict « rien à juger »), redirigées (vers la page de
|
|
221
|
+
connexion), restées sans réponse, et les profils non retrouvés. Une mesure qui ne
|
|
222
|
+
porte pas sur ce qui était demandé le dit d'abord.
|
|
223
|
+
|
|
224
|
+
À savoir :
|
|
225
|
+
|
|
226
|
+
- ce sont de **vraies requêtes** : une page qui écrit en `GET` écrit N fois — un
|
|
227
|
+
compteur, ou une session créée pour chaque visiteur anonyme ;
|
|
228
|
+
- les zones à jeton (`jwt`, `external-jwt`) ne sont pas jouées : leurs requêtes
|
|
229
|
+
rendent `401`, et le rapport le dit ;
|
|
230
|
+
- un tir plus court que cinq périodes de la sonde laisse la boucle d'événements
|
|
231
|
+
« non mesurée » plutôt que d'afficher un zéro trompeur.
|
|
232
|
+
|
|
103
233
|
### Protéger la porte par OAuth 2.1
|
|
104
234
|
|
|
105
235
|
Par défaut la porte est anonyme, et ce qui la borne est le périmètre ci-dessus.
|
|
@@ -170,7 +300,8 @@ module de votre application peut publier les siens en implémentant
|
|
|
170
300
|
`getMcpTools(): IMcpTool[]` :
|
|
171
301
|
|
|
172
302
|
```ts
|
|
173
|
-
import { Module,
|
|
303
|
+
import { Module, type IMcpTool } from "nodefony";
|
|
304
|
+
import { mcpText } from "@nodefony/ai";
|
|
174
305
|
|
|
175
306
|
class Shop extends Module {
|
|
176
307
|
getMcpTools(): IMcpTool[] {
|
|
@@ -185,6 +316,8 @@ class Shop extends Module {
|
|
|
185
316
|
properties: { sku: { type: "string" } },
|
|
186
317
|
required: ["sku"],
|
|
187
318
|
},
|
|
319
|
+
// Lecture dans la base de l'application : rien n'est modifié.
|
|
320
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
188
321
|
handler: async (args) => mcpText(await this.stock(String(args.sku))),
|
|
189
322
|
},
|
|
190
323
|
];
|
|
@@ -197,6 +330,15 @@ Rien ne s'enregistre au démarrage : la liste est relue à chaque requête, et
|
|
|
197
330
|
est déclaré. Un outil écarté (nom hors forme, nom déjà pris, handler absent) le
|
|
198
331
|
dit en `WARNING`, jamais en silence.
|
|
199
332
|
|
|
333
|
+
Chaque outil **dit ce qu'il fait** par ses `annotations` : `readOnlyHint: true`
|
|
334
|
+
pour une lecture, et un outil qui modifie quoi que ce soit DOIT se déclarer
|
|
335
|
+
`readOnlyHint: false`, avec `destructiveHint` (`true` s'il efface ou écrase).
|
|
336
|
+
Sans annotation, la spécification MCP tient l'outil pour une écriture
|
|
337
|
+
destructive : l'agent de code demande alors confirmation pour la moindre
|
|
338
|
+
lecture. Les sept outils intégrés se publient `readOnlyHint: true`,
|
|
339
|
+
`openWorldHint: false`. Grille de choix et erreurs à éviter :
|
|
340
|
+
`node_modules/@nodefony/ai/docs/mcp.md`, « Dire ce que fait l'outil ».
|
|
341
|
+
|
|
200
342
|
Un outil peut aussi se **réserver** — `scopes: ["shop:read"]` (tous exigés) ou
|
|
201
343
|
`requiresAuth: true` — et son handler reçoit alors l'appelant en second
|
|
202
344
|
paramètre. Il est alors absent de `tools/list` **et** inappelable en le nommant.
|
|
@@ -228,13 +370,17 @@ npx nodefony ai:sync # --dry-run pour voir sans écrire, --json pour
|
|
|
228
370
|
= nodefony-add-crud @nodefony/devkit
|
|
229
371
|
= nodefony-add-realtime-channel @nodefony/devkit
|
|
230
372
|
= nodefony-add-service @nodefony/devkit
|
|
373
|
+
= nodefony-authentication @nodefony/devkit
|
|
231
374
|
= nodefony-browser @nodefony/devkit
|
|
232
375
|
= nodefony-dev @nodefony/devkit
|
|
233
376
|
= nodefony-devops @nodefony/devkit
|
|
234
377
|
= nodefony-migrate-schema @nodefony/devkit
|
|
235
378
|
= nodefony-protect-route @nodefony/devkit
|
|
379
|
+
= nodefony-security-review @nodefony/devkit
|
|
380
|
+
= nodefony-troubleshoot @nodefony/devkit
|
|
381
|
+
= nodefony-upgrade @nodefony/devkit
|
|
236
382
|
|
|
237
|
-
0 posé(s) · 0 mis à jour ·
|
|
383
|
+
0 posé(s) · 0 mis à jour · 12 inchangé(s)
|
|
238
384
|
```
|
|
239
385
|
|
|
240
386
|
**Le préfixe `nodefony-` vous laisse la place.** Ces pointeurs arrivent dans
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// \0@oxc-project+runtime@0.155.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);
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
}
|
|
8
|
-
//#endregion
|
|
9
8
|
export { __decorate as default };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// \0@oxc-project+runtime@0.155.0/helpers/esm/decorateParam.js
|
|
2
2
|
function __decorateParam(paramIndex, decorator) {
|
|
3
3
|
return function(target, key) {
|
|
4
4
|
decorator(target, key, paramIndex);
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
|
-
//#endregion
|
|
8
7
|
export { __decorateParam as default };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { BUILTIN_MCP_TOOL_KEYS, builtinMcpTools, selectBuiltinMcpTools } from "./nodefony/mcp/tools.js";
|
|
1
2
|
import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
|
|
2
3
|
import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
3
4
|
import { buildCard } from "./nodefony/src/card.js";
|
|
4
|
-
import
|
|
5
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
5
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.155.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";
|
|
9
|
-
import
|
|
9
|
+
import ProfileSessionController_default from "./nodefony/controllers/ProfileSessionController.js";
|
|
10
|
+
import { Module, services } from "nodefony";
|
|
10
11
|
import { controllers } from "@nodefony/framework";
|
|
11
|
-
|
|
12
|
+
// index.ts
|
|
12
13
|
let DevkitModule = class DevkitModule extends Module {
|
|
13
14
|
/**
|
|
14
15
|
* ⚠️ Ce module ne pose PLUS de commande CLI.
|
|
@@ -37,12 +38,24 @@ let DevkitModule = class DevkitModule extends Module {
|
|
|
37
38
|
this.options = defineDevkitConfig(input ?? {});
|
|
38
39
|
return this;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Déclare les outils `nodefony_*` — comme n'importe quel module.
|
|
43
|
+
*
|
|
44
|
+
* Aucun cas particulier côté collecte : `@nodefony/ai` les ramasse avec ceux
|
|
45
|
+
* de l'application, et seul le préfixe `nodefony_`, réservé aux modules
|
|
46
|
+
* `@nodefony/*`, les distingue. Un service absent (module en cours de
|
|
47
|
+
* démontage) ne déclare rien plutôt que de lever.
|
|
48
|
+
*
|
|
49
|
+
* @returns les outils retenus par l'allowlist `mcp.tools`
|
|
50
|
+
*/
|
|
51
|
+
getMcpTools() {
|
|
52
|
+
return this.get("devkit")?.mcpTools() ?? [];
|
|
53
|
+
}
|
|
40
54
|
};
|
|
41
|
-
DevkitModule = __decorate([
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
], DevkitModule);
|
|
55
|
+
DevkitModule = __decorate([controllers([
|
|
56
|
+
DevkitController_default,
|
|
57
|
+
McpController_default,
|
|
58
|
+
ProfileSessionController_default
|
|
59
|
+
]), services([DevkitService_default])], DevkitModule);
|
|
46
60
|
var devkit_default = DevkitModule;
|
|
47
|
-
|
|
48
|
-
export { DevkitController_default as DevkitController, DevkitService_default as DevkitService, McpController_default as McpController, buildCard, devkit_default as default, defineDevkitConfig, devkitConfigSchema };
|
|
61
|
+
export { BUILTIN_MCP_TOOL_KEYS, DevkitController_default as DevkitController, DevkitService_default as DevkitService, McpController_default as McpController, ProfileSessionController_default as ProfileSessionController, buildCard, builtinMcpTools, devkit_default as default, defineDevkitConfig, devkitConfigSchema, selectBuiltinMcpTools };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { BUILTIN_MCP_TOOL_KEYS
|
|
1
|
+
import { BUILTIN_MCP_TOOL_KEYS } from "../mcp/tools.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
|
|
3
|
+
import { mcpConfigSchema } from "@nodefony/ai";
|
|
4
|
+
// nodefony/config/config.ts
|
|
4
5
|
/**
|
|
5
6
|
* @nodefony/devkit — CONFIGURATION DU MODULE (schéma Zod = source unique).
|
|
6
7
|
*
|
|
@@ -26,146 +27,25 @@ import { z } from "zod";
|
|
|
26
27
|
* la métadonnée. Ces flags ressortent dans le JSON Schema lu par Studio.
|
|
27
28
|
*/
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* se comporte exactement comme avant — anonyme, aucune métadonnée publiée,
|
|
33
|
-
* aucun `401`. Non vide, le rôle s'allume ENTIÈREMENT. Un interrupteur séparé
|
|
34
|
-
* aurait permis deux états qui se contredisent (« protégé mais sans serveur
|
|
35
|
-
* d'autorisation », « métadonnées publiées mais jetons ignorés »), et c'est
|
|
36
|
-
* précisément ce genre d'état que personne ne diagnostique.
|
|
37
|
-
*
|
|
38
|
-
* ⚠️ **Allumer ce rôle ne suffit pas à protéger quoi que ce soit** : il faut
|
|
39
|
-
* aussi qu'un service du conteneur sache VÉRIFIER un jeton. S'il n'y en a pas,
|
|
40
|
-
* la porte refuse de servir (et le dit fort) plutôt que d'accepter n'importe
|
|
41
|
-
* quel porteur.
|
|
42
|
-
*/
|
|
43
|
-
const mcpAuthorizationSchema = z.strictObject({
|
|
44
|
-
/**
|
|
45
|
-
* Serveurs d'autorisation capables d'émettre un jeton pour cette porte.
|
|
46
|
-
*
|
|
47
|
-
* Vide (défaut) = rôle éteint. Non vide, la spécification MCP impose que le
|
|
48
|
-
* document de métadonnées les publie (« MUST include the
|
|
49
|
-
* `authorization_servers` field containing at least one authorization
|
|
50
|
-
* server ») : sans eux, un client apprendrait qu'un jeton est nécessaire
|
|
51
|
-
* sans jamais pouvoir en obtenir un.
|
|
52
|
-
*/
|
|
53
|
-
authorizationServers: z.array(z.string()).default([]).describe("Émetteurs de jetons acceptés (issuer OAuth) ; vide = aucune autorisation"),
|
|
54
|
-
/**
|
|
55
|
-
* URI canonique publique de cette porte — l'audience que les jetons doivent
|
|
56
|
-
* porter (RFC 8707).
|
|
57
|
-
*
|
|
58
|
-
* 🔴 **Elle s'écrit, elle ne se devine pas depuis l'en-tête `Host`.** Si
|
|
59
|
-
* l'URI publiée et l'audience attendue venaient toutes deux de la requête,
|
|
60
|
-
* un `Host` forgé obtiendrait un jeton d'audience arbitraire *et* passerait
|
|
61
|
-
* la vérification : la liaison d'audience, dont c'est l'unique raison
|
|
62
|
-
* d'être, ne protégerait plus rien.
|
|
63
|
-
*
|
|
64
|
-
* Ex. `https://mon-app.example/nodefony/mcp`.
|
|
65
|
-
*/
|
|
66
|
-
resource: z.string().default("").describe("URI publique de la porte MCP (audience attendue des jetons)"),
|
|
67
|
-
/**
|
|
68
|
-
* Autres adresses de CETTE MÊME porte, dont les jetons sont aussi acceptés.
|
|
69
|
-
*
|
|
70
|
-
* Le cas courant : l'application sert la porte en clair sur un port et en
|
|
71
|
-
* TLS sur un autre. Un jeton émis pour l'une était alors refusé sur l'autre
|
|
72
|
-
* — la liaison d'audience (RFC 8707) faisant son travail. Nommer la seconde
|
|
73
|
-
* adresse est la bonne réponse ; relâcher la liaison n'en serait pas une.
|
|
74
|
-
*
|
|
75
|
-
* 🔴 Ces valeurs s'ÉCRIVENT. Dérivées du `Host`, un en-tête forgé
|
|
76
|
-
* obtiendrait un jeton d'audience arbitraire ET passerait la vérification.
|
|
77
|
-
* Seule `resource` est publiée en RFC 9728 : le document n'admet qu'une
|
|
78
|
-
* valeur, et un client n'a besoin que d'une adresse.
|
|
79
|
-
*
|
|
80
|
-
* ⚠️ L'ÉMETTEUR doit servir ces audiences (`security.jwt.audiences`), sinon
|
|
81
|
-
* il refuse de les émettre — `invalid_target`, et à juste titre.
|
|
82
|
-
*/
|
|
83
|
-
additionalResources: z.array(z.string()).default([]).describe("Autres URI de la même porte dont les jetons sont acceptés (jamais dérivées du Host)"),
|
|
84
|
-
/** Nom lisible, affiché pendant le consentement. */
|
|
85
|
-
resourceName: z.string().optional().describe("Nom affiché au consentement"),
|
|
86
|
-
/** Page de documentation destinée à un humain. */
|
|
87
|
-
resourceDocumentation: z.string().optional().describe("URL de documentation de la ressource"),
|
|
88
|
-
/**
|
|
89
|
-
* Servir une requête SANS jeton comme anonyme, au lieu de la refuser.
|
|
90
|
-
*
|
|
91
|
-
* `false` (défaut) : sans jeton, `401` + `WWW-Authenticate` — c'est ce qui
|
|
92
|
-
* apprend au client qu'une autorisation existe et où l'obtenir.
|
|
93
|
-
* `true` : la porte reste ouverte aux outils publics, et les outils
|
|
94
|
-
* réservés restent retenus. Utile en développement ; c'est un choix qui
|
|
95
|
-
* s'écrit, jamais un comportement dont on hérite.
|
|
96
|
-
*/
|
|
97
|
-
anonymous: z.boolean().default(false).describe("Tolérer les requêtes sans jeton (outils publics seulement)")
|
|
98
|
-
}).superRefine((value, ctx) => {
|
|
99
|
-
if (value.authorizationServers.length === 0) return;
|
|
100
|
-
if (value.resource.length === 0) {
|
|
101
|
-
ctx.addIssue({
|
|
102
|
-
code: "custom",
|
|
103
|
-
path: ["resource"],
|
|
104
|
-
message: "`mcp.authorization.resource` est requis dès qu'un serveur d'autorisation est déclaré : c'est l'URI publique de la porte, et l'audience que les jetons doivent porter (RFC 8707)."
|
|
105
|
-
});
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
canonicalResourceUri(value.resource);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
ctx.addIssue({
|
|
112
|
-
code: "custom",
|
|
113
|
-
path: ["resource"],
|
|
114
|
-
message: error.message
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
/**
|
|
119
|
-
* Réglages du serveur MCP — extrait en constante pour une raison mécanique.
|
|
30
|
+
* Réglages du serveur MCP — le schéma de la porte vient de `@nodefony/ai`
|
|
31
|
+
* (source unique, {@link mcpConfigSchema}) ; devkit n'y ajoute que l'allowlist
|
|
32
|
+
* de ses outils intégrés.
|
|
120
33
|
*
|
|
121
34
|
* ⚠️ **Piège Zod 4** : un `.default({})` posé à plat sur un sous-objet NE
|
|
122
35
|
* ré-applique PAS les défauts de ses champs. Le pattern du dépôt est donc
|
|
123
36
|
* `sous.default(() => sous.parse({}))` — le callback force la ré-évaluation, et
|
|
124
37
|
* une application qui n'écrit rien obtient bien les valeurs d'usine.
|
|
125
38
|
*/
|
|
126
|
-
const mcpSchema =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
* C'est ce que la spec exige (`transports/streamable-http` §Security :
|
|
137
|
-
* « Servers MUST validate the `Origin` header […] MUST respond with HTTP 403
|
|
138
|
-
* Forbidden »), et c'est ce qui ferme le DNS rebinding.
|
|
139
|
-
*
|
|
140
|
-
* Vide (défaut) = aucune origine de navigateur n'est admise.
|
|
141
|
-
*/
|
|
142
|
-
allowedOrigins: z.array(z.string()).default([]).describe("Origines navigateur admises ; vide = aucune"),
|
|
143
|
-
/**
|
|
144
|
-
* Accepter un appel dont l'adresse distante n'est pas locale.
|
|
145
|
-
*
|
|
146
|
-
* La spec recommande de n'écouter que sur la boucle locale ; le serveur de
|
|
147
|
-
* l'application, lui, écoute pour le développement. La garde est donc portée
|
|
148
|
-
* par la route : toute adresse non locale reçoit `403`.
|
|
149
|
-
*/
|
|
150
|
-
allowRemote: z.boolean().default(false).describe("Autorise les appels depuis une adresse non locale"),
|
|
151
|
-
/**
|
|
152
|
-
* Outils exposés — **allowlist**, et lecture seule.
|
|
153
|
-
*
|
|
154
|
-
* Tant qu'aucun outil n'écrit, le pire qu'obtienne un appelant illégitime est
|
|
155
|
-
* la description de l'application. Le jour où un générateur sera exposé, ce
|
|
156
|
-
* sera un ajout explicite ici — et cette ligne sera l'endroit où quelqu'un
|
|
157
|
-
* décide de donner à un agent le droit d'écrire des fichiers.
|
|
158
|
-
*/
|
|
159
|
-
tools: z.array(z.string()).default([...BUILTIN_MCP_TOOL_KEYS]).describe("Outils MCP activés (allowlist, lecture seule)"),
|
|
160
|
-
/**
|
|
161
|
-
* Rôle serveur de ressource OAuth 2.1 — **éteint par défaut**.
|
|
162
|
-
*
|
|
163
|
-
* Tant qu'aucun serveur d'autorisation n'est déclaré, la porte se comporte
|
|
164
|
-
* comme elle l'a toujours fait : anonyme, protégée par son seul périmètre
|
|
165
|
-
* (`policy: "dev"` + gardes `Origin`/localité).
|
|
166
|
-
*/
|
|
167
|
-
authorization: mcpAuthorizationSchema.default(() => mcpAuthorizationSchema.parse({})).describe("Serveur de ressource OAuth 2.1 (RFC 9728/6750/8707)")
|
|
168
|
-
});
|
|
39
|
+
const mcpSchema = mcpConfigSchema.extend({
|
|
40
|
+
/**
|
|
41
|
+
* Outils exposés — **allowlist**, et lecture seule.
|
|
42
|
+
*
|
|
43
|
+
* Tant qu'aucun outil n'écrit, le pire qu'obtienne un appelant illégitime est
|
|
44
|
+
* la description de l'application. Le jour où un générateur sera exposé, ce
|
|
45
|
+
* sera un ajout explicite ici — et cette ligne sera l'endroit où quelqu'un
|
|
46
|
+
* décide de donner à un agent le droit d'écrire des fichiers.
|
|
47
|
+
*/
|
|
48
|
+
tools: z.array(z.string()).default([...BUILTIN_MCP_TOOL_KEYS]).describe("Outils MCP activés (allowlist, lecture seule)") });
|
|
169
49
|
const devkitConfigSchema = z.strictObject({
|
|
170
50
|
/** Interrupteur du module — l'app peut le charger sans l'activer. */
|
|
171
51
|
enabled: z.boolean().default(true).describe("Active les fonctionnalités du module devkit"),
|
|
@@ -196,5 +76,4 @@ const devkitConfigSchema = z.strictObject({
|
|
|
196
76
|
});
|
|
197
77
|
/** Défauts matérialisés (passés au `super()` du Module). */
|
|
198
78
|
const defaults = devkitConfigSchema.parse({});
|
|
199
|
-
//#endregion
|
|
200
79
|
export { defaults as default, devkitConfigSchema };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { devkitConfigSchema } from "./config.js";
|
|
2
2
|
import { parseModuleConfig } from "nodefony";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
|
|
4
|
+
// nodefony/config/defineModuleConfig.ts
|
|
5
5
|
/**
|
|
6
6
|
* @nodefony/devkit — LE COMMENT : builder PUR de la config.
|
|
7
7
|
*
|
|
@@ -32,5 +32,4 @@ function defineDevkitConfig(config = {}) {
|
|
|
32
32
|
function devkitConfigJsonSchema() {
|
|
33
33
|
return z.toJSONSchema(devkitConfigSchema);
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { defineDevkitConfig, devkitConfigJsonSchema };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
3
2
|
import { Controller, controller, route } from "@nodefony/framework";
|
|
4
|
-
|
|
3
|
+
// nodefony/controllers/DevkitController.ts
|
|
5
4
|
/**
|
|
6
5
|
* La carte de visite de l'application, servie en HTTP.
|
|
7
6
|
*
|
|
@@ -45,16 +44,10 @@ let DevkitController = class DevkitController extends Controller {
|
|
|
45
44
|
return this.renderJson(this.#service().getCard());
|
|
46
45
|
}
|
|
47
46
|
};
|
|
48
|
-
__decorate([
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
__decorateMetadata("design:type", Function),
|
|
54
|
-
__decorateMetadata("design:paramtypes", []),
|
|
55
|
-
__decorateMetadata("design:returntype", Promise)
|
|
56
|
-
], DevkitController.prototype, "card", null);
|
|
57
|
-
DevkitController = __decorate([controller("/nodefony/devkit/api"), __decorateMetadata("design:paramtypes", [Object])], DevkitController);
|
|
47
|
+
__decorate([route("devkit-card", {
|
|
48
|
+
path: "/card",
|
|
49
|
+
method: "GET"
|
|
50
|
+
})], DevkitController.prototype, "card", null);
|
|
51
|
+
DevkitController = __decorate([controller("/nodefony/devkit/api")], DevkitController);
|
|
58
52
|
var DevkitController_default = DevkitController;
|
|
59
|
-
//#endregion
|
|
60
53
|
export { DevkitController_default as default };
|