@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.
Files changed (65) hide show
  1. package/README.md +151 -5
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  4. package/dist/index.js +24 -11
  5. package/dist/nodefony/config/config.js +16 -137
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  7. package/dist/nodefony/controllers/DevkitController.js +7 -14
  8. package/dist/nodefony/controllers/McpController.js +27 -163
  9. package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
  10. package/dist/nodefony/mcp/observe.js +235 -0
  11. package/dist/nodefony/mcp/profileReport.js +242 -0
  12. package/dist/nodefony/mcp/profileRoute.js +521 -0
  13. package/dist/nodefony/mcp/shotClient.js +84 -0
  14. package/dist/nodefony/mcp/shutdown.js +44 -0
  15. package/dist/nodefony/mcp/tools.js +696 -0
  16. package/dist/nodefony/service/DevkitService.js +93 -29
  17. package/dist/nodefony/src/card.js +1 -2
  18. package/dist/nodefony/src/errors/DevkitError.js +1 -2
  19. package/dist/nodefony/src/profileSession.js +14 -0
  20. package/dist/types/index.d.ts +15 -1
  21. package/dist/types/nodefony/config/config.d.ts +1 -1
  22. package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
  23. package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
  24. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
  25. package/dist/types/nodefony/mcp/observe.d.ts +83 -0
  26. package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
  27. package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
  28. package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
  29. package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
  30. package/dist/types/nodefony/mcp/tools.d.ts +109 -0
  31. package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
  32. package/dist/types/nodefony/src/profileSession.d.ts +12 -0
  33. package/docs/index.md +217 -20
  34. package/package.json +17 -12
  35. package/skills/nodefony-add-crud/SKILL.md +5 -0
  36. package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
  37. package/skills/nodefony-add-service/SKILL.md +14 -3
  38. package/skills/nodefony-authentication/SKILL.md +234 -0
  39. package/skills/nodefony-browser/SKILL.md +65 -21
  40. package/skills/nodefony-browser/references/socket.md +13 -4
  41. package/skills/nodefony-browser/references/sondes.md +13 -0
  42. package/skills/nodefony-browser/scripts/audit.mjs +59 -10
  43. package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
  44. package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
  45. package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
  46. package/skills/nodefony-browser/scripts/socket.mjs +70 -4
  47. package/skills/nodefony-browser/scripts/watch.mjs +101 -0
  48. package/skills/nodefony-dev/SKILL.md +78 -82
  49. package/skills/nodefony-dev/scripts/docs.mjs +24 -8
  50. package/skills/nodefony-devops/SKILL.md +98 -74
  51. package/skills/nodefony-devops/references/compose.md +8 -0
  52. package/skills/nodefony-devops/references/dimensionnement.md +122 -0
  53. package/skills/nodefony-devops/references/frontal.md +282 -48
  54. package/skills/nodefony-devops/references/kubernetes.md +41 -14
  55. package/skills/nodefony-devops/references/secrets.md +30 -5
  56. package/skills/nodefony-devops/references/variables.md +66 -31
  57. package/skills/nodefony-migrate-schema/SKILL.md +5 -2
  58. package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
  59. package/skills/nodefony-protect-route/SKILL.md +124 -21
  60. package/skills/nodefony-security-review/SKILL.md +127 -0
  61. package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
  62. package/skills/nodefony-troubleshoot/SKILL.md +225 -0
  63. package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
  64. package/skills/nodefony-upgrade/SKILL.md +130 -0
  65. 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
- Sept outils : **`nodefony_inspect`** (ce qui est monté), **`nodefony_check`**
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é). Ce sont les mêmes briques que les commandes — une
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, mcpText, type IMcpTool } from "nodefony";
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 · 8 inchangé(s)
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
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
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
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorateParam.js
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 __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
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 { Kernel, Module, services } from "nodefony";
9
+ import ProfileSessionController_default from "./nodefony/controllers/ProfileSessionController.js";
10
+ import { Module, services } from "nodefony";
10
11
  import { controllers } from "@nodefony/framework";
11
- //#region index.ts
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
- controllers([DevkitController_default, McpController_default]),
43
- services([DevkitService_default]),
44
- __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])
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
- //#endregion
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, canonicalResourceUri } from "nodefony";
1
+ import { BUILTIN_MCP_TOOL_KEYS } from "../mcp/tools.js";
2
2
  import { z } from "zod";
3
- //#region nodefony/config/config.ts
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
- * Rôle **serveur de ressource OAuth 2.1** de la porte MCP — éteint par défaut.
30
- *
31
- * ⭐ **Un seul réglage commande : {@link authorizationServers}.** Vide, la porte
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 = z.strictObject({
127
- /** Répond-on aux requêtes MCP ? Coupé, la route rend `404`. */
128
- enabled: z.boolean().default(true).describe("Active l'endpoint MCP (POST /nodefony/mcp)"),
129
- /**
130
- * Origines acceptées quand l'en-tête `Origin` est PRÉSENT.
131
- *
132
- * ⭐ La subtilité qui fait toute la sécurité : un client MCP natif est un
133
- * process, pas un navigateur — **il n'envoie pas d'`Origin`**. Une page web
134
- * malveillante, elle, en envoie toujours un vers `localhost`. La règle est
135
- * donc : *absent → on passe* ; *présent et hors de cette liste → `403`*.
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
- //#region nodefony/config/defineModuleConfig.ts
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 __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
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
- //#region nodefony/controllers/DevkitController.ts
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
- route("devkit-card", {
50
- path: "/card",
51
- method: "GET"
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 };