@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/docs/index.md
CHANGED
|
@@ -106,13 +106,13 @@ la rend éprouvable sans serveur, et ce qui l'empêche d'inventer quoi que ce so
|
|
|
106
106
|
npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
Neuf outils. Quatre sont les commandes que vous connaissez déjà :
|
|
110
110
|
`nodefony_inspect` (ce qui est monté), `nodefony_check` (ce qui manque),
|
|
111
111
|
`nodefony_symbols` (ce qu'une API signifie), `nodefony_card` (par où commencer).
|
|
112
112
|
`nodefony_docs` sert la documentation des modules. `nodefony_admin_list` et
|
|
113
113
|
`nodefony_admin_call` ouvrent les lectures du data plane d'administration — le
|
|
114
114
|
catalogue, puis l'appel — et exigent un jeton porteur du scope `admin:read` :
|
|
115
|
-
sans lui, ils n'apparaissent pas dans la liste.
|
|
115
|
+
sans lui, ils n'apparaissent pas dans la liste. `nodefony_observe` mesure ce que l'application fait réellement de ses requêtes, pendant qu'elle tourne ; `nodefony_profile_route` mesure une route en lui envoyant lui-même ses requêtes (sections suivantes).
|
|
116
116
|
|
|
117
117
|
**Il n'y a pas de process à lancer.** Depuis la révision `2026-07-28` du
|
|
118
118
|
transport, un serveur MCP est un endpoint `POST` sans session : c'est donc une
|
|
@@ -143,13 +143,145 @@ Trois choses à savoir avant de s'étonner d'un refus :
|
|
|
143
143
|
|
|
144
144
|
Les outils intégrés sont en lecture seule, et `mcp.tools` est une allowlist.
|
|
145
145
|
|
|
146
|
-
###
|
|
146
|
+
### Mesurer l'application qui tourne — `nodefony_observe`
|
|
147
|
+
|
|
148
|
+
Un agent de code qui vient de modifier un contrôleur ne voit pas l'effet de son
|
|
149
|
+
changement : la barre de debug et la console d'administration affichent les
|
|
150
|
+
mesures du profileur, mais il ne les lit pas. `nodefony_observe` les lui rend.
|
|
151
|
+
Pendant une fenêtre de 1 à 50 secondes (10 par défaut), l'outil relève le
|
|
152
|
+
profileur de développement toutes les deux secondes et rend un rapport :
|
|
153
|
+
|
|
154
|
+
1. un **verdict** — `ok`, `anomalies (N)`, et `INCOMPLÈTE` quand les chiffres ne
|
|
155
|
+
portent pas sur tout (profils perdus, aucun trafic, appel interrompu), avec
|
|
156
|
+
la raison ;
|
|
157
|
+
2. les **anomalies**, nommées par route et par contrôleur : une requête SQL
|
|
158
|
+
répétée dans une boucle (« N+1 »), les erreurs `5xx` avec leur `requestId` et
|
|
159
|
+
leurs lignes de journal, les routes dont le p95 dépasse 500 ms avec ce qui
|
|
160
|
+
coûte par requête, un blocage de la boucle d'événements de 100 ms ou plus ;
|
|
161
|
+
3. un tableau par route, les requêtes SQL les plus coûteuses, et un bloc JSON
|
|
162
|
+
pour comparer deux appels — avant et après une correction.
|
|
163
|
+
|
|
164
|
+
```jsonc
|
|
165
|
+
{ "name": "nodefony_observe", "arguments": { "durationSec": 10 } }
|
|
166
|
+
// "route": "<nom de route>" pour n'en observer qu'une ; "includeTooling": true
|
|
167
|
+
// pour compter aussi les routes /nodefony/… de l'outillage
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**L'outil ne génère aucun trafic.** On ouvre la page, on joue les tests ou on
|
|
171
|
+
envoie des requêtes pendant que la fenêtre court ; sans trafic, il n'a rien à
|
|
172
|
+
juger, et il le dit.
|
|
173
|
+
|
|
174
|
+
Ce que les chiffres veulent dire :
|
|
175
|
+
|
|
176
|
+
- **durées et temps « par requête »** portent sur les requêtes **servies**
|
|
177
|
+
(statut inférieur à 400, sans exception). Un refus `401`/`403` rendu en
|
|
178
|
+
quelques millisecondes ne mesure pas la route : il est compté à part ;
|
|
179
|
+
- **`action.self`** est le temps de l'action et des services qu'elle appelle,
|
|
180
|
+
une fois retirés les phases imbriquées (initialisation, rendu, envoi) et le
|
|
181
|
+
SQL — les intervalles sont fusionnés, une requête lancée pendant le rendu
|
|
182
|
+
n'est pas comptée deux fois. **« SQL (cumul) »** additionne les durées des
|
|
183
|
+
requêtes, même lancées en parallèle ;
|
|
184
|
+
- **la boucle d'événements** est rapportée en retard moyen et en **pire
|
|
185
|
+
blocage** : un gel unique de 200 ms ne déplace la moyenne d'un intervalle de
|
|
186
|
+
deux secondes que de quelques millisecondes ;
|
|
187
|
+
- **la forme d'une requête SQL** remplace chaque valeur par `?` : le rapport ne
|
|
188
|
+
contient jamais une donnée. Le journal d'une erreur n'en garde que les lignes
|
|
189
|
+
WARNING et plus graves ;
|
|
190
|
+
- la **variation du tas** est mesurée sans GC forcé : elle est indicative,
|
|
191
|
+
jamais une anomalie.
|
|
192
|
+
|
|
193
|
+
Ce qui encadre l'outil :
|
|
194
|
+
|
|
195
|
+
- c'est un **outil long** : chaque relevé part en progression, et 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) ;
|
|
196
|
+
- un **rechargement du serveur** pendant la fenêtre (un fichier modifié) ne fait
|
|
197
|
+
pas perdre l'observation : l'outil rend un rapport partiel qui le dit, avant
|
|
198
|
+
que le serveur ne ferme ses connexions ;
|
|
199
|
+
- le **journal des erreurs** est lu au relevé qui voit la requête — le syslog de
|
|
200
|
+
développement ne garde que ses dernières lignes — et seulement pour un appel
|
|
201
|
+
**authentifié** (scope `admin:read`), par la même voie que
|
|
202
|
+
`nodefony_admin_call` ;
|
|
203
|
+
- le profileur garde les 500 dernières requêtes : au-delà de ce débit entre deux
|
|
204
|
+
relevés, les profils perdus sont comptés, et le verdict le dit.
|
|
205
|
+
|
|
206
|
+
La lecture de chaque anomalie et le geste qui la corrige sont dans le skill
|
|
207
|
+
d'agent `nodefony-troubleshoot`, section « Mesurer l'application qui tourne ».
|
|
208
|
+
|
|
209
|
+
### Mesurer une route — `nodefony_profile_route`
|
|
210
|
+
|
|
211
|
+
Là où `nodefony_observe` attend le trafic, `nodefony_profile_route` le **produit** :
|
|
212
|
+
il envoie lui-même N requêtes `GET` (ou `HEAD`) sur un chemin de l'application,
|
|
213
|
+
retrouve leurs profils par l'identifiant qu'il pose (`x-request-id`) et rend le
|
|
214
|
+
même rapport. C'est la boucle « je modifie → je mesure » en un appel.
|
|
215
|
+
|
|
216
|
+
```jsonc
|
|
217
|
+
// tools/call
|
|
218
|
+
{
|
|
219
|
+
"name": "nodefony_profile_route",
|
|
220
|
+
"arguments": { "path": "/orders?page=2", "count": 50 },
|
|
221
|
+
}
|
|
222
|
+
// optionnel : "concurrency" (défaut 4, max 8), "warmup" (défaut 3, hors mesures),
|
|
223
|
+
// "method": "HEAD", "as": "<identifiant>", "anonymous": true
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`path` est un **chemin**, jamais une URL : hôte, schéma et `//` sont refusés,
|
|
227
|
+
l'outil ne vise que l'application elle-même. Au plus 200 requêtes par appel —
|
|
228
|
+
sous les 500 profils que garde le profileur — et 45 secondes de tir.
|
|
229
|
+
|
|
230
|
+
**Au nom de qui.** Une page protégée vue sans compte ne rend que des refus.
|
|
231
|
+
Quand le jeton MCP porte le scope `admin:write`, l'outil joue son
|
|
232
|
+
**propriétaire** : il ouvre une vraie session à son nom par le flux de connexion
|
|
233
|
+
de l'application (un ticket à usage unique, présenté en boucle locale à
|
|
234
|
+
`POST /nodefony/devkit/profile-session`), tire avec, puis la détruit — l'audit
|
|
235
|
+
l'écrit sous la raison `devkit`. `as` joue un autre compte, à condition que ses
|
|
236
|
+
rôles soient inclus dans ceux du propriétaire, hiérarchie comprise : jouer un
|
|
237
|
+
compte ne monte jamais en droits. Sans ce scope, l'outil joue l'anonyme et le
|
|
238
|
+
rapport donne la commande :
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npx nodefony security:mcp:token --scope "admin:read admin:write" --write
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Extrait de deux appels réels, avant puis après la correction d'un N+1 (la même
|
|
245
|
+
lecture, une requête par compte, puis une seule requête `$in`) :
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
Verdict : anomalies (1) — observation complète
|
|
249
|
+
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)
|
|
250
|
+
Joué en tant que : `admin` (ROLE_ADMIN, ROLE_NODEFONY_ADMIN, ROLE_DEV, ROLE_SUPERVISOR)
|
|
251
|
+
Reçu par le client : 2xx 30 · 3xx 0 · 401 0 · 403 0 · autres 4xx 0 · 5xx 0 · sans réponse 0
|
|
252
|
+
|
|
253
|
+
Anomalies (les plus coûteuses d'abord) :
|
|
254
|
+
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`).
|
|
255
|
+
|
|
256
|
+
route | type | n | p50 | p95 | max | 2xx/3xx/4xx/5xx | SQL par requête moy/max | temps par requête
|
|
257
|
+
`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
|
|
258
|
+
|
|
259
|
+
— après correction —
|
|
260
|
+
Verdict : ok — observation complète
|
|
261
|
+
`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
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Ce que le rapport dit **en tête**, avant tout chiffre : les requêtes refusées
|
|
265
|
+
(`401`/`403` — verdict « rien à juger »), redirigées (vers la page de
|
|
266
|
+
connexion), restées sans réponse, et les profils non retrouvés. Une mesure qui ne
|
|
267
|
+
porte pas sur ce qui était demandé le dit d'abord.
|
|
268
|
+
|
|
269
|
+
À savoir :
|
|
270
|
+
|
|
271
|
+
- ce sont de **vraies requêtes** : une page qui écrit en `GET` écrit N fois — un
|
|
272
|
+
compteur, ou une session créée pour chaque visiteur anonyme ;
|
|
273
|
+
- les zones à jeton (`jwt`, `external-jwt`) ne sont pas jouées : leurs requêtes
|
|
274
|
+
rendent `401`, et le rapport le dit ;
|
|
275
|
+
- un tir plus court que cinq périodes de la sonde laisse la boucle d'événements
|
|
276
|
+
« non mesurée » plutôt que d'afficher un zéro trompeur.
|
|
277
|
+
|
|
278
|
+
### Obtenir le porteur — `security:mcp:token`
|
|
147
279
|
|
|
148
280
|
Dès que la porte demande un jeton, il faut en produire un. L'application **le signe elle-même**, en
|
|
149
281
|
ligne de commande, sans qu'aucun serveur n'écoute :
|
|
150
282
|
|
|
151
283
|
```bash
|
|
152
|
-
npx nodefony security:token --write # pose NF_MCP_TOKEN chez les agents présents
|
|
284
|
+
npx nodefony security:mcp:token --write # pose NF_MCP_TOKEN chez les agents présents
|
|
153
285
|
npx nodefony ai:mcp --auth # l'en-tête porte ${NF_MCP_TOKEN}, jamais le jeton
|
|
154
286
|
```
|
|
155
287
|
|
|
@@ -203,7 +335,8 @@ métier — et c'est précisément ce qu'un agent invente le plus mal. N'importe
|
|
|
203
335
|
module de votre application peut donc en publier :
|
|
204
336
|
|
|
205
337
|
```ts
|
|
206
|
-
import { Module,
|
|
338
|
+
import { Module, type IMcpTool } from "nodefony";
|
|
339
|
+
import { mcpText } from "@nodefony/ai";
|
|
207
340
|
|
|
208
341
|
class Shop extends Module {
|
|
209
342
|
getMcpTools(): IMcpTool[] {
|
|
@@ -221,6 +354,10 @@ class Shop extends Module {
|
|
|
221
354
|
properties: { sku: { type: "string", description: "Référence" } },
|
|
222
355
|
required: ["sku"],
|
|
223
356
|
},
|
|
357
|
+
// Il LIT la base et ne sort pas de l'application : il le dit, sinon la
|
|
358
|
+
// spec le tient pour destructif et l'agent demande confirmation à
|
|
359
|
+
// chaque lecture.
|
|
360
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
224
361
|
handler: async (args) => mcpText(await this.stock(String(args.sku))),
|
|
225
362
|
},
|
|
226
363
|
];
|
|
@@ -228,8 +365,14 @@ class Shop extends Module {
|
|
|
228
365
|
}
|
|
229
366
|
```
|
|
230
367
|
|
|
231
|
-
|
|
368
|
+
Quatre choses à savoir :
|
|
232
369
|
|
|
370
|
+
- **un outil dit ce qu'il fait** (`annotations`, indices `ToolAnnotations` de la
|
|
371
|
+
spec MCP) : sans annotation, la spec le tient pour un outil qui écrit, détruit
|
|
372
|
+
et sort de l'application. Un outil qui lit se déclare `readOnlyHint: true` ;
|
|
373
|
+
un outil qui modifie, `readOnlyHint: false`. Un indice n'est jamais une
|
|
374
|
+
autorisation — le détail des cinq indices et de leurs défauts :
|
|
375
|
+
[écrire un outil](../../ai/docs/mcp.md#dire-ce-que-fait-loutil--les-annotations) ;
|
|
233
376
|
- **rien ne s'enregistre au démarrage** : la liste est relue à chaque requête,
|
|
234
377
|
donc un module ajouté apparaît sans redémarrer quoi que ce soit ;
|
|
235
378
|
- **`mcp.tools` ne filtre que les outils intégrés** — le vôtre est publié dès
|
|
@@ -277,6 +420,56 @@ est absent de `tools/list` **et** inappelable en le nommant, et le refus dit
|
|
|
277
420
|
> `policy: "dev"`. Avant d'exposer une donnée par un outil, se demander si elle
|
|
278
421
|
> supporterait d'être lue **sans identification**, par qui a accès à la machine.
|
|
279
422
|
|
|
423
|
+
### Un outil LONG — progression et annulation
|
|
424
|
+
|
|
425
|
+
Le troisième paramètre du handler sert l'outil qui dure : `run.signal` est
|
|
426
|
+
abattu quand le client annule — qu'il ferme le flux de la réponse (révision courante
|
|
427
|
+
de MCP) ou envoie `notifications/cancelled` (révision précédente) —,
|
|
428
|
+
`run.progress()` lui donne signe de vie.
|
|
429
|
+
Un outil court l'ignore ; un appel direct (un test) ne le fournit pas, d'où le
|
|
430
|
+
`run?.` :
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
{
|
|
434
|
+
name: "shop_reindex",
|
|
435
|
+
description: "Réindexe le catalogue, lot par lot. Long : signale chaque lot.",
|
|
436
|
+
inputSchema: { type: "object", properties: {} },
|
|
437
|
+
handler: async (_args, _caller, run) => {
|
|
438
|
+
const lots = await this.lots();
|
|
439
|
+
for (const [i, lot] of lots.entries()) {
|
|
440
|
+
if (run?.signal.aborted) return mcpText("interrompu", true);
|
|
441
|
+
await this.reindex(lot, run?.signal);
|
|
442
|
+
run?.progress(i + 1, lots.length, `lot ${i + 1}/${lots.length}`);
|
|
443
|
+
}
|
|
444
|
+
return mcpText(`${lots.length} lots réindexés`);
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Ce que la porte fait du reste :
|
|
450
|
+
|
|
451
|
+
- **le flux s'ouvre à la première progression, ou au bout de cinq secondes.**
|
|
452
|
+
Un agent qui envoie un `progressToken` et accepte `text/event-stream` reçoit
|
|
453
|
+
des `notifications/progress`, puis la réponse, qui clôt le flux. Un outil
|
|
454
|
+
encore en cours après le délai de grâce (`MCP_STREAM_GRACE_MS`, 5 s) ouvre le
|
|
455
|
+
flux même sans jeton : la norme oblige le client à accepter une réponse en
|
|
456
|
+
flux, et c'est ce qui désarme le délai d'inactivité de la requête (30 s par
|
|
457
|
+
défaut) — sans cela, un outil long répondant en JSON recevrait un `408`. Un
|
|
458
|
+
outil qui répond avant reste en JSON ;
|
|
459
|
+
- **la norme est tenue pour vous** : une valeur qui ne croît pas est ignorée, la
|
|
460
|
+
cadence est bornée (une notification par 100 ms au plus), et plus rien ne
|
|
461
|
+
part une fois l'outil terminé ;
|
|
462
|
+
- **fermer le flux, c'est annuler** (transport HTTP, révision 2026-07-28) :
|
|
463
|
+
`run.signal` est abattu, à l'outil de s'arrêter.
|
|
464
|
+
|
|
465
|
+
Le journal `notifications/message` n'est **pas** proposé : la révision
|
|
466
|
+
2026-07-28 le déprécie (« New implementations SHOULD NOT adopt it »). Ce qu'un
|
|
467
|
+
outil veut dire en route passe par le `message` de la progression ; ce qui doit
|
|
468
|
+
atteindre le modèle va dans la réponse finale. `subscriptions/listen` est refusé
|
|
469
|
+
par une erreur explicite (`-32601`) : le catalogue ne change qu'au rechargement
|
|
470
|
+
de l'application, qui redémarre son process, si bien qu'un flux d'abonnement
|
|
471
|
+
casserait au lieu de notifier. Le `tools/list` suivant rend l'état à jour.
|
|
472
|
+
|
|
280
473
|
## Les skills d'agent — répondre à « comment fait-on ça, ici ? »
|
|
281
474
|
|
|
282
475
|
La carte dit **où aller**. Elle ne dit pas **comment faire**. Or c'est là qu'un
|
|
@@ -284,20 +477,24 @@ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
|
|
|
284
477
|
service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
|
|
285
478
|
dans le corps de l'action.
|
|
286
479
|
|
|
287
|
-
Le paquet livre donc
|
|
288
|
-
un qui porte la conduite commune de toute tâche,
|
|
289
|
-
coûte cher :
|
|
290
|
-
|
|
291
|
-
| Skill | Le besoin qu'il couvre
|
|
292
|
-
| ------------------------------- |
|
|
293
|
-
| `nodefony-dev` | conduire une tâche de bout en bout, avant toute modification
|
|
294
|
-
| `nodefony-add-crud` | exposer une ressource REST complète, entité comprise
|
|
295
|
-
| `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur
|
|
296
|
-
| `nodefony-
|
|
297
|
-
| `nodefony-
|
|
298
|
-
| `nodefony-
|
|
299
|
-
| `nodefony-
|
|
300
|
-
| `nodefony-
|
|
480
|
+
Le paquet livre donc onze **skills** au format [Agent Skills](https://agentskills.io) :
|
|
481
|
+
un qui porte la conduite commune de toute tâche, un qui part du symptôme d'une panne,
|
|
482
|
+
et un par geste où l'invention coûte cher :
|
|
483
|
+
|
|
484
|
+
| Skill | Le besoin qu'il couvre |
|
|
485
|
+
| ------------------------------- | ---------------------------------------------------------------- |
|
|
486
|
+
| `nodefony-dev` | conduire une tâche de bout en bout, avant toute modification |
|
|
487
|
+
| `nodefony-add-crud` | exposer une ressource REST complète, entité comprise |
|
|
488
|
+
| `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur |
|
|
489
|
+
| `nodefony-authentication` | connecter un utilisateur ou un programme, sans rien réécrire |
|
|
490
|
+
| `nodefony-protect-route` | réserver une route à qui est habilité |
|
|
491
|
+
| `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse |
|
|
492
|
+
| `nodefony-browser` | voir **et mesurer** un écran, sans navigateur sur le poste |
|
|
493
|
+
| `nodefony-migrate-schema` | faire évoluer le schéma d'une base et le porter en production |
|
|
494
|
+
| `nodefony-devops` | exploiter l'application en conteneur, sans réécrire son contrat |
|
|
495
|
+
| `nodefony-troubleshoot` | partir d'un symptôme de panne pour trouver le geste qui la règle |
|
|
496
|
+
| `nodefony-security-review` | relire la sécurité d'un diff avant de le commiter |
|
|
497
|
+
| `nodefony-upgrade` | monter le framework de version sans rien laisser derrière |
|
|
301
498
|
|
|
302
499
|
Tous portent le préfixe `nodefony-` : leurs pointeurs arrivent dans le dossier où
|
|
303
500
|
vous écrivez aussi les vôtres, et sans namespace un skill maison du même nom
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/devkit",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
|
|
6
6
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
@@ -20,25 +20,30 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
|
|
23
|
+
"clean": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
23
24
|
"typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json",
|
|
24
25
|
"test": "vitest run",
|
|
25
|
-
"coverage": "vitest run --coverage"
|
|
26
|
+
"coverage": "vitest run --coverage",
|
|
27
|
+
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
26
28
|
},
|
|
27
29
|
"peerDependencies": {
|
|
28
|
-
"@nodefony/
|
|
29
|
-
"@nodefony/
|
|
30
|
-
"nodefony": "^10.0.0-beta.
|
|
31
|
-
"
|
|
30
|
+
"@nodefony/ai": "^10.0.0-beta.4",
|
|
31
|
+
"@nodefony/framework": "^10.0.0-beta.4",
|
|
32
|
+
"@nodefony/http": "^10.0.0-beta.4",
|
|
33
|
+
"lighthouse": "^13.0.0",
|
|
34
|
+
"nodefony": "^10.0.0-beta.4",
|
|
32
35
|
"playwright": "^1.50.0",
|
|
33
|
-
"
|
|
36
|
+
"zod": "^4.6.7"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
|
-
"@
|
|
37
|
-
"@nodefony/
|
|
38
|
-
"nodefony": "^10.0.0-beta.
|
|
39
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
40
|
+
"@nodefony/ai": "^10.0.0-beta.4",
|
|
41
|
+
"@nodefony/framework": "^10.0.0-beta.4",
|
|
42
|
+
"@nodefony/http": "^10.0.0-beta.4",
|
|
43
|
+
"nodefony": "^10.0.0-beta.4"
|
|
39
44
|
},
|
|
40
45
|
"dependencies": {
|
|
41
|
-
"axe-core": "4.
|
|
46
|
+
"axe-core": "4.14.0",
|
|
42
47
|
"tslib": "2.8.1"
|
|
43
48
|
},
|
|
44
49
|
"peerDependenciesMeta": {
|
|
@@ -72,6 +77,6 @@
|
|
|
72
77
|
"esm"
|
|
73
78
|
],
|
|
74
79
|
"engines": {
|
|
75
|
-
"node": ">=24.
|
|
80
|
+
"node": ">=24.14.0"
|
|
76
81
|
}
|
|
77
82
|
}
|
|
@@ -42,6 +42,11 @@ gabarit porte des détails qui ne se devinent pas : le repository résolu au pre
|
|
|
42
42
|
se connecte qu'au démarrage), la pagination bornée **et son tri déclaré**, les codes
|
|
43
43
|
201/204/404/409/422, et l'en-tête `Location`.
|
|
44
44
|
|
|
45
|
+
Le controller généré **déclare** ses entrées : chaque route porte un `schema` (`idParams`, le schéma
|
|
46
|
+
de création ou de retouche de l'entité), et l'action reçoit `input` — validé à la porte (422) avant
|
|
47
|
+
tout appel au service, qui revalide pour ses autres portes (CLI, socket). Pour ajouter une action,
|
|
48
|
+
**garde cette forme** : un `@Body` ou un `@Param` sur une action à schéma est refusé au démarrage.
|
|
49
|
+
|
|
45
50
|
## La grammaire de champs
|
|
46
51
|
|
|
47
52
|
`nom:type[?][=défaut][:index|:unique]` — **non-null par défaut**, `?` rend facultatif, `=valeur`
|
|
@@ -6,9 +6,11 @@ description: >
|
|
|
6
6
|
Porte la façon de fermer un canal à certains rôles, le piège du canal public par défaut, celui
|
|
7
7
|
du canal dont le nom est calculé, et la façade cliente à employer côté navigateur. À charger
|
|
8
8
|
AVANT d'écrire du code WebSocket, un canal, ou un abonnement client.
|
|
9
|
+
Dit aussi quand un flux SSE suffit, et sa façade.
|
|
9
10
|
Déclencheurs : "flux temps réel", "websocket", "canal", "push", "notifications en direct",
|
|
10
11
|
"abonnement", "RealtimeController", "@RealtimeChannel", "socket client", "temps réel privé",
|
|
11
|
-
"réserver un canal à un rôle", "mon canal est public", "diffuser à plusieurs onglets"
|
|
12
|
+
"réserver un canal à un rôle", "mon canal est public", "diffuser à plusieurs onglets",
|
|
13
|
+
"SSE", "server-sent events", "EventSource", "barre de progression", "réponse en streaming".
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
# add-realtime-channel — le temps réel par sa couche
|
|
@@ -37,6 +39,33 @@ câblage. **N'écris pas de `new WebSocket(...)` ni de `ws.on(...)` côté serve
|
|
|
37
39
|
existe, il est employé par le framework, et le reprendre à la main te prive du routage par canal,
|
|
38
40
|
de l'authentification partagée avec HTTP, et de la contre-pression.
|
|
39
41
|
|
|
42
|
+
## WebSocket, ou un simple flux SSE ?
|
|
43
|
+
|
|
44
|
+
| Besoin | Choisir |
|
|
45
|
+
| -------------------------------------------------------------- | ------------------------------------------- |
|
|
46
|
+
| Le serveur pousse, le client écoute (progression, journal) | **SSE** — une réponse HTTP qui ne finit pas |
|
|
47
|
+
| La réponse d'UNE requête arrive par morceaux (modèle, export) | **SSE** sur cette requête |
|
|
48
|
+
| Échange dans les deux sens, canaux, état partagé entre onglets | **WebSocket** — le reste de cette page |
|
|
49
|
+
|
|
50
|
+
SSE n'est pas une route d'un genre particulier : l'action d'une route ordinaire choisit de
|
|
51
|
+
répondre en flux.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
@route("progress-run", { path: "/run" })
|
|
55
|
+
async run() {
|
|
56
|
+
const sse = await this.renderSse(); // en-têtes, battement, contre-pression
|
|
57
|
+
await sse.send({ step: "calcul" }, { event: "step" });
|
|
58
|
+
return sse.close();
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`this.acceptsSse()` dit si le client a demandé `text/event-stream` (une même route peut répondre
|
|
63
|
+
en JSON ou en flux). Côté navigateur, `new NodefonySse(url)` (`nodefony/client`), qui se lit comme
|
|
64
|
+
`EventSource` ; les liaisons de front existent comme pour la socket. **N'écris jamais
|
|
65
|
+
`Content-Type: text/event-stream` à la main** : tu perdrais le battement qui empêche un proxy de
|
|
66
|
+
couper la connexion, et la contre-pression (`await sse.send()` freine si le client lit lentement).
|
|
67
|
+
Détail : `node_modules/@nodefony/http/docs/sse.md`.
|
|
68
|
+
|
|
40
69
|
## Fermer un canal
|
|
41
70
|
|
|
42
71
|
```ts
|
|
@@ -53,27 +82,55 @@ aucun rôle particulier n'est requis.
|
|
|
53
82
|
|
|
54
83
|
## 🔴 Le piège du canal dont le nom est calculé
|
|
55
84
|
|
|
56
|
-
|
|
57
|
-
l'exécution (`` `room:${id}` ``) **n'est couvert par
|
|
58
|
-
et rien ne le signale.
|
|
85
|
+
La politique d'un `@RealtimeChannel` est indexée par le nom **EXACT** du canal. Un canal dont le
|
|
86
|
+
nom se construit à l'exécution (`` `room:${id}` ``) **n'est couvert par aucun décorateur** — il
|
|
87
|
+
naît public, et rien ne le signale.
|
|
59
88
|
|
|
60
89
|
```ts
|
|
61
90
|
@RealtimeChannel("room:lobby", { authenticated: true }) // ✅ couvert
|
|
62
|
-
// `room:42` construit à la volée // 🔴 PAS couvert
|
|
91
|
+
// `room:42` construit à la volée // 🔴 PAS couvert par le décorateur
|
|
63
92
|
```
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
La parade est une règle **par préfixe** dans la configuration de sécurité, qui garde à la fois
|
|
95
|
+
les canaux ET les actions dont le nom commence ainsi :
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
use("@nodefony/security", {
|
|
99
|
+
realtimeChannels: [
|
|
100
|
+
{ pattern: "room:", authenticated: true, roles: ["ROLE_USER"] },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Elle exprime une classe d'identité (connecté, rôles), jamais une liste de membres : « seuls les
|
|
106
|
+
participants de la salle 42 » ne s'écrit pas encore dans le framework. Ne le simule pas avec un
|
|
107
|
+
nom de canal difficile à deviner.
|
|
108
|
+
|
|
109
|
+
## Savoir QUI appelle une action
|
|
110
|
+
|
|
111
|
+
Les paramètres décorés d'une route valent aussi dans une `@RealtimeAction` : `@Body("clé")` lit la
|
|
112
|
+
charge de l'appel, `@CurrentUser()` l'utilisateur de la connexion (`undefined` pour un anonyme).
|
|
113
|
+
L'auteur d'un message se prend là, côté SERVEUR — jamais dans la charge, où le client peut mentir.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
@RealtimeAction("chat:send")
|
|
117
|
+
send(@Body("text") text: string, @CurrentUser() user: IUser) {
|
|
118
|
+
return { author: user.identifier, text };
|
|
119
|
+
}
|
|
120
|
+
```
|
|
67
121
|
|
|
68
122
|
## Côté client — la façade, pas le socket
|
|
69
123
|
|
|
70
124
|
```ts
|
|
71
|
-
import {
|
|
125
|
+
import { NodefonySocket } from "nodefony/client";
|
|
72
126
|
|
|
73
|
-
const client =
|
|
127
|
+
const client = NodefonySocket.shared({ url: "/api/live/realtime" });
|
|
74
128
|
client.subscribe("ops:alerts", (payload) => { … });
|
|
75
129
|
```
|
|
76
130
|
|
|
131
|
+
L'adresse est **obligatoire** : `shared()` sans `url` lève une erreur, parce que la route dépend de
|
|
132
|
+
l'application. Une application générée monte `/api/live/realtime`.
|
|
133
|
+
|
|
77
134
|
En React, les hooks du paquet client font la même chose avec le cycle de vie du composant.
|
|
78
135
|
**Importe depuis `nodefony/client`**, jamais depuis la racine `nodefony` : côté navigateur, elle
|
|
79
136
|
n'expose pas ces symboles et le compilateur refuse.
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Crée un service injectable dans une application Nodefony par `nodefony create service`, et le
|
|
5
5
|
fait entrer dans le conteneur — la moitié qu'on oublie. Porte la distinction entre le nom de la
|
|
6
6
|
CLASSE et le nom de l'INSTANCE, les deux façons d'obtenir un service depuis un autre
|
|
7
|
-
(`@
|
|
7
|
+
(`@Inject` au constructeur ou `container.get` à l'usage), le choix de sa durée de vie
|
|
8
8
|
(un exemplaire pour l'application, ou un par requête), et le défaut mesuré qu'un service
|
|
9
9
|
écrit à la main produit : une classe qui compile, dont les tests passent, et que le conteneur
|
|
10
10
|
ignore. À charger AVANT d'écrire une classe de service ou d'appeler un service depuis un autre.
|
|
@@ -63,7 +63,7 @@ et à l'introspection, et n'est construit qu'à la première requête qui le ré
|
|
|
63
63
|
C'est le piège n°1, et il ne produit aucune erreur — juste un `null` :
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
|
-
@injectable() // ← nomme la CLASSE : @
|
|
66
|
+
@injectable() // ← nomme la CLASSE : @Inject("BillingService")
|
|
67
67
|
export class BillingService extends Service {
|
|
68
68
|
constructor() {
|
|
69
69
|
super("billing", ...); // ← nomme l'INSTANCE : container.get("billing")
|
|
@@ -88,11 +88,22 @@ dépend est refusé au démarrage. Pour **lire** une donnée de la requête depu
|
|
|
88
88
|
besoin de portée : `RequestContext.getScope()` ou `RequestContext.get()` au moment de l'appel.
|
|
89
89
|
Détail : `node_modules/nodefony/docs/service.md`.
|
|
90
90
|
|
|
91
|
+
**Qui est connecté, depuis un service** : `RequestContext.getAuthenticatedUser()` (l'utilisateur
|
|
92
|
+
connecté, ou `null`) et `RequestContext.getUserId()` (`string | null`). Jamais
|
|
93
|
+
`RequestContext.getUser()` pour décider : il rend l'utilisateur BRUT, anonyme compris.
|
|
94
|
+
|
|
95
|
+
**Le payload de la requête est une forme FERMÉE** (`RequestContextPayload`) :
|
|
96
|
+
`RequestContext.set("maCle", …)` sur une clé qu'il ne déclare pas ne compile pas. Ta clé s'ajoute
|
|
97
|
+
par augmentation — `declare module "nodefony" { interface RequestContextPayload { tenant?: string } }`
|
|
98
|
+
— mais `RequestContext.release()`, qui vide le payload à la fin de l'unité de travail, ne la
|
|
99
|
+
vide PAS d'office.
|
|
100
|
+
Détail : `node_modules/nodefony/docs/request-context.md`.
|
|
101
|
+
|
|
91
102
|
## Obtenir un service depuis un autre — deux voies, un choix
|
|
92
103
|
|
|
93
104
|
| Voie | Quand |
|
|
94
105
|
| ------------------------------ | ----------------------------------------------------------------------------- |
|
|
95
|
-
| `@
|
|
106
|
+
| `@Inject("X")` au constructeur | **par défaut** — la dépendance est déclarée, l'ordre de démarrage la respecte |
|
|
96
107
|
| `container.get("x")` à l'usage | quand la dépendance est facultative, tardive, ou choisie à l'exécution |
|
|
97
108
|
|
|
98
109
|
`create service --inject` pose la première. Le second est visible partout dans les exemples, et
|