@nodefony/devkit 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +318 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +47 -0
- package/dist/nodefony/command/CardCommand.js +70 -0
- package/dist/nodefony/config/config.js +200 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controllers/DevkitController.js +60 -0
- package/dist/nodefony/controllers/McpController.js +223 -0
- package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
- package/dist/nodefony/interfaces/IDevkitService.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DevkitService.js +198 -0
- package/dist/nodefony/src/card.js +2 -0
- package/dist/nodefony/src/errors/DevkitError.js +21 -0
- package/dist/nodefony/src/mcp/guard.js +51 -0
- package/dist/nodefony/src/mcp/protocol.js +127 -0
- package/dist/nodefony/src/mcp/server.js +133 -0
- package/dist/nodefony/src/mcp/tools.js +163 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
- package/dist/types/nodefony/config/config.d.ts +25 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
- package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
- package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
- package/dist/types/nodefony/src/card.d.ts +18 -0
- package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
- package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
- package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
- package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
- package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
- package/docs/index.md +358 -0
- package/package.json +77 -0
- package/skills/nodefony-add-crud/SKILL.md +199 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
- package/skills/nodefony-add-service/SKILL.md +90 -0
- package/skills/nodefony-browser/SKILL.md +416 -0
- package/skills/nodefony-browser/references/socket.md +115 -0
- package/skills/nodefony-browser/references/sondes.md +175 -0
- package/skills/nodefony-browser/scripts/audit.mjs +169 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
- package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
- package/skills/nodefony-browser/scripts/socket.mjs +354 -0
- package/skills/nodefony-browser/scripts/watch.mjs +125 -0
- package/skills/nodefony-migrate-schema/SKILL.md +359 -0
- package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
- package/skills/nodefony-protect-route/SKILL.md +195 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { INSPECT_SUBJECTS, collectCheckReport, countCheckFindings, lookupSymbol, readAdminSubject, readSymbolsGraph } from "nodefony";
|
|
2
|
+
//#region nodefony/src/mcp/tools.ts
|
|
3
|
+
/** Préfixe des noms d'outils — un agent voit à qui il parle. */
|
|
4
|
+
const PREFIX = "nodefony_";
|
|
5
|
+
/**
|
|
6
|
+
* Liste des sujets, rendue lisible pour la description de l'outil.
|
|
7
|
+
*
|
|
8
|
+
* Dérivée de {@link INSPECT_SUBJECTS} : ajouter un sujet au cœur le publie ici
|
|
9
|
+
* sans rien réécrire, et il ne peut pas exister de sujet annoncé qu'on ne
|
|
10
|
+
* saurait pas lire.
|
|
11
|
+
*/
|
|
12
|
+
function subjectLines() {
|
|
13
|
+
return Object.entries(INSPECT_SUBJECTS).map(([key, spec]) => `- \`${key}\` : ${spec.summary}`).join("\n");
|
|
14
|
+
}
|
|
15
|
+
/** Catalogue complet, avant filtrage par l'allowlist de configuration. */
|
|
16
|
+
function catalogue() {
|
|
17
|
+
return {
|
|
18
|
+
inspect: {
|
|
19
|
+
name: `${PREFIX}inspect`,
|
|
20
|
+
description: `Lit l'état RÉEL de cette application Nodefony : les routes réellement montées, les modules chargés, les services enregistrés, la configuration effective et la provenance de chaque valeur, les stores de données, les entités de l'ORM. À utiliser avant d'écrire du code qui suppose une route, un service ou une clé de configuration — la réponse vient de l'application qui tourne, pas d'une lecture des sources, donc elle ne peut pas se tromper sur ce qui est chargé.
|
|
21
|
+
|
|
22
|
+
Sujets disponibles :\n${subjectLines()}`,
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
subject: {
|
|
27
|
+
type: "string",
|
|
28
|
+
enum: Object.keys(INSPECT_SUBJECTS),
|
|
29
|
+
description: "Ce qu'on veut voir"
|
|
30
|
+
},
|
|
31
|
+
target: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Paramètre du sujet, quand il en attend un (ex. le nom d'un module pour `module`)"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: ["subject"]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
card: {
|
|
40
|
+
name: `${PREFIX}card`,
|
|
41
|
+
description: "Carte de visite de l'application : son identité, les modules chargés, où trouver la documentation, et les commandes à lancer. À utiliser en ARRIVANT sur une application inconnue, avant toute autre exploration — elle dit en un appel ce qu'il y a et où aller ensuite.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
check: {
|
|
48
|
+
name: `${PREFIX}check`,
|
|
49
|
+
description: "Diagnostic STATIQUE de l'application : classes écrites que rien n'enregistre (entité, controller ou service jamais câblé), paquets importés sans être déclarés, variables d'environnement requises absentes, modules du manifeste non installés, ports occupés, et le bilan du dernier démarrage. À utiliser APRÈS avoir écrit ou généré du code, avant de conclure que c'est fini : ni la compilation ni les tests ne voient qu'une classe n'est branchée à rien.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
symbols: {
|
|
56
|
+
name: `${PREFIX}symbols`,
|
|
57
|
+
description: "Interroge le graphe symbolique du framework : ce qu'est un symbole (classe, interface, fonction), où il est défini, ce qu'il étend ou implémente, et la première phrase de sa documentation. À utiliser AVANT d'ouvrir un fichier pour comprendre une API du framework — la réponse est immédiate et vaut pour la version réellement installée. Sans argument, rend le résumé du graphe ; avec `module`, la surface exportée d'un paquet.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
name: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Nom exact du symbole (ex. `AbstractCrudService`, `IKernel`)"
|
|
64
|
+
},
|
|
65
|
+
module: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Paquet dont on veut la surface exportée (ex. `@nodefony/http`)"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Outils publiés, filtrés par l'allowlist de configuration.
|
|
76
|
+
*
|
|
77
|
+
* Une clé inconnue dans la configuration est simplement ignorée : elle ne peut
|
|
78
|
+
* rien ouvrir. C'est le sens d'une allowlist — ce qui n'est pas nommé ICI
|
|
79
|
+
* n'existe pas, et la faute de frappe d'un utilisateur ne peut pas activer
|
|
80
|
+
* autre chose que ce qu'il voulait.
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ **`Object.hasOwn` n'est pas une précaution de style.** Sans lui,
|
|
83
|
+
* `tools: ["toString"]` résolvait une méthode héritée d'`Object.prototype` :
|
|
84
|
+
* la valeur n'étant pas `undefined`, elle franchissait le filtre et un outil
|
|
85
|
+
* fantôme entrait dans le catalogue publié. Trouvé par le test, pas à la
|
|
86
|
+
* relecture.
|
|
87
|
+
*
|
|
88
|
+
* @param enabled - clés d'outils autorisées (`devkit.mcp.tools`)
|
|
89
|
+
*/
|
|
90
|
+
function listMcpTools(enabled) {
|
|
91
|
+
const all = catalogue();
|
|
92
|
+
return enabled.filter((key) => Object.hasOwn(all, key)).map((key) => all[key]).filter((tool) => tool !== void 0);
|
|
93
|
+
}
|
|
94
|
+
/** Rend un contenu textuel — les données partent en JSON indenté, lisible. */
|
|
95
|
+
function text(value, isError = false) {
|
|
96
|
+
const rendered = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
97
|
+
return isError ? {
|
|
98
|
+
content: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: rendered
|
|
101
|
+
}],
|
|
102
|
+
isError: true
|
|
103
|
+
} : { content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: rendered
|
|
106
|
+
}] };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Exécute un outil par son nom public.
|
|
110
|
+
*
|
|
111
|
+
* Un échec métier (sujet inconnu, module absent) rend un résultat `isError`,
|
|
112
|
+
* **pas** une erreur JSON-RPC : le protocole réserve celle-ci aux fautes de
|
|
113
|
+
* protocole. La distinction compte pour l'agent — une erreur de protocole
|
|
114
|
+
* signifie « tu t'y prends mal », un `isError` signifie « ta demande est
|
|
115
|
+
* recevable, voici pourquoi elle n'aboutit pas » ; c'est la seconde qu'il peut
|
|
116
|
+
* corriger seul.
|
|
117
|
+
*
|
|
118
|
+
* @param name - nom public de l'outil (`nodefony_inspect`…)
|
|
119
|
+
* @param args - arguments fournis par l'agent
|
|
120
|
+
* @param enabled - allowlist de configuration
|
|
121
|
+
* @param deps - briques qui répondent réellement
|
|
122
|
+
* @returns le résultat de l'outil, ou `null` si le nom n'est pas exposé
|
|
123
|
+
*/
|
|
124
|
+
async function callMcpTool(name, args, enabled, deps) {
|
|
125
|
+
if (!listMcpTools(enabled).find((tool) => tool.name === name)) return null;
|
|
126
|
+
if (name === `${PREFIX}card`) return text(deps.getCard());
|
|
127
|
+
if (name === `${PREFIX}inspect`) {
|
|
128
|
+
const subject = typeof args.subject === "string" ? args.subject : "";
|
|
129
|
+
const target = typeof args.target === "string" ? args.target : void 0;
|
|
130
|
+
const read = await readAdminSubject(deps.broker, subject, target);
|
|
131
|
+
if (!read.ok) return text(read.message, true);
|
|
132
|
+
return text(read.data);
|
|
133
|
+
}
|
|
134
|
+
if (name === `${PREFIX}check`) {
|
|
135
|
+
const report = await collectCheckReport(deps.projectRoot);
|
|
136
|
+
return text({
|
|
137
|
+
verdict: countCheckFindings(report) === 0 ? "ok" : "manquements",
|
|
138
|
+
total: countCheckFindings(report),
|
|
139
|
+
...report
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (name === `${PREFIX}symbols`) {
|
|
143
|
+
const graph = readSymbolsGraph(deps.projectRoot);
|
|
144
|
+
if (graph === null) return text("aucun graphe symbolique atteignable — il est publié par le paquet `nodefony` (node_modules/nodefony/.ai/symbols.json) et régénérable dans ce dépôt par `npm run generate-symbols`", true);
|
|
145
|
+
const wanted = typeof args.name === "string" ? args.name : null;
|
|
146
|
+
const module = typeof args.module === "string" ? args.module : null;
|
|
147
|
+
if (wanted !== null) {
|
|
148
|
+
const sym = lookupSymbol(graph, wanted);
|
|
149
|
+
return sym ? text(sym) : text(`« ${wanted} » est introuvable dans le graphe (${Object.keys(graph.symbols).length} symboles)`, true);
|
|
150
|
+
}
|
|
151
|
+
const entries = Object.values(graph.symbols).filter((s) => module === null || s.module === module);
|
|
152
|
+
if (module !== null) return text(entries.sort((a, b) => a.name.localeCompare(b.name)));
|
|
153
|
+
const parPaquet = Object.create(null);
|
|
154
|
+
for (const sym of entries) parPaquet[sym.module] = (parPaquet[sym.module] ?? 0) + 1;
|
|
155
|
+
return text({
|
|
156
|
+
total: entries.length,
|
|
157
|
+
parPaquet
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return text(`outil « ${name} » publié mais non implémenté`, true);
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
export { callMcpTool, listMcpTools };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Kernel, Module } from "nodefony";
|
|
2
|
+
import { type DevkitConfigInput } from "./nodefony/config/config.js";
|
|
3
|
+
import DevkitService from "./nodefony/service/DevkitService.js";
|
|
4
|
+
import DevkitController from "./nodefony/controllers/DevkitController.js";
|
|
5
|
+
import McpController from "./nodefony/controllers/McpController.js";
|
|
6
|
+
/**
|
|
7
|
+
* @nodefony/devkit — Outillage de developpement d une application Nodefony : carte de visite et portes de decouverte pour un agent
|
|
8
|
+
*
|
|
9
|
+
* Module applicatif : un workspace npm à part entière
|
|
10
|
+
* (`src/packages/@nodefony/devkit/`), chargé par le manifeste `modules` de
|
|
11
|
+
* `nodefony.config.ts`. Le Kernel l'importe PAR SON NOM (`@nodefony/devkit`) —
|
|
12
|
+
* d'où le workspace, qui le rend résolvable.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Rend la config du module typée à l'appel : `use("@nodefony/devkit", { … })`
|
|
16
|
+
* dans `nodefony.config.ts` propose les clés du schéma et refuse les fautes de
|
|
17
|
+
* frappe, au lieu d'avaler un `Record<string, unknown>`.
|
|
18
|
+
*/
|
|
19
|
+
declare module "nodefony" {
|
|
20
|
+
interface NodefonyModuleConfig {
|
|
21
|
+
"@nodefony/devkit": DevkitConfigInput;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
declare class DevkitModule extends Module {
|
|
25
|
+
/**
|
|
26
|
+
* ⚠️ Ce module ne pose PLUS de commande CLI.
|
|
27
|
+
*
|
|
28
|
+
* `devkit:card` vivait ici, et n'existait donc que lorsque le module était
|
|
29
|
+
* chargé : hors développement (`policy: "dev"`) le CLI répondait
|
|
30
|
+
* `unknown command`, et sur une application non encore construite le Kernel
|
|
31
|
+
* refusait de démarrer avant elle. Une carte de visite qui disparaît selon
|
|
32
|
+
* l'environnement n'accueille personne. Elle est désormais servie par le cœur
|
|
33
|
+
* (`nodefony card`, alias `devkit:card`, standalone 0-boot — fast-path de
|
|
34
|
+
* `CliKernel.start`), qui ne lit que des fichiers.
|
|
35
|
+
*
|
|
36
|
+
* Ce module garde la porte HTTP : elle, et elle seule, connaît les modules
|
|
37
|
+
* réellement CHARGÉS.
|
|
38
|
+
*/
|
|
39
|
+
constructor(kernel: Kernel);
|
|
40
|
+
/**
|
|
41
|
+
* Valide la config au boot — défauts du schéma fusionnés avec ce que l'app
|
|
42
|
+
* passe dans `use()`. Une clé inconnue ou mal typée plante ICI, avec le champ
|
|
43
|
+
* fautif nommé, plutôt qu'en `undefined.x` au premier appel en production.
|
|
44
|
+
*/
|
|
45
|
+
onKernelRegister(): Promise<this>;
|
|
46
|
+
}
|
|
47
|
+
export default DevkitModule;
|
|
48
|
+
export { DevkitService, DevkitController, McpController };
|
|
49
|
+
export { buildCard } from "./nodefony/src/card.js";
|
|
50
|
+
export { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
51
|
+
export { devkitConfigSchema, type DevkitConfig, type DevkitConfigInput, } from "./nodefony/config/config.js";
|
|
52
|
+
export type { IDevkitAppInfo, IDevkitCard, IDevkitCardInput, IDevkitDoor, IDevkitService, IDevkitVerb, } from "./nodefony/interfaces/IDevkitService.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command, CliKernel } from "nodefony";
|
|
2
|
+
import type { IDevkitCard } from "../interfaces/IDevkitService.js";
|
|
3
|
+
/**
|
|
4
|
+
* `nodefony devkit:card [-j]` — qui répond, et où aller ensuite.
|
|
5
|
+
*
|
|
6
|
+
* ## Pourquoi une commande, alors que la route existe
|
|
7
|
+
*
|
|
8
|
+
* La route HTTP vit sous `/nodefony`, que le pare-feu d'une application réelle
|
|
9
|
+
* couvre : un agent qui code ne s'authentifie pas, et n'a pas de navigateur. La
|
|
10
|
+
* porte qu'il a déjà, c'est le terminal. Même source (le service), deux rendus —
|
|
11
|
+
* ajouter une porte n'ajoute jamais une vérité.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ Le module est `policy: "dev"` : hors développement il n'est pas chargé, donc
|
|
14
|
+
* cette commande **n'existe pas**. C'est voulu — et c'est pour ça qu'elle
|
|
15
|
+
* s'invoque `NODE_ENV=development npx nodefony devkit:card` depuis un terminal
|
|
16
|
+
* qui n'aurait pas posé la variable.
|
|
17
|
+
*/
|
|
18
|
+
declare class CardCommand extends Command {
|
|
19
|
+
constructor(cli: CliKernel);
|
|
20
|
+
generate(opts: {
|
|
21
|
+
json?: boolean;
|
|
22
|
+
}): Promise<this>;
|
|
23
|
+
/**
|
|
24
|
+
* Rend la carte pour un HUMAIN (ou un agent qui lit un terminal).
|
|
25
|
+
*
|
|
26
|
+
* Statique et PURE : elle ne touche ni au kernel ni au service, donc elle
|
|
27
|
+
* s'éprouve seule. Sortie sur `stdout` plutôt que par le journal — une carte
|
|
28
|
+
* de visite n'est pas un événement de log, et le préfixe horodaté rendrait le
|
|
29
|
+
* copier-coller inutilisable.
|
|
30
|
+
*/
|
|
31
|
+
static format(card: IDevkitCard): string;
|
|
32
|
+
}
|
|
33
|
+
export default CardCommand;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const devkitConfigSchema: z.ZodObject<{
|
|
3
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
4
|
+
mcp: z.ZodDefault<z.ZodObject<{
|
|
5
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
allowedOrigins: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
7
|
+
allowRemote: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
9
|
+
authorization: z.ZodDefault<z.ZodObject<{
|
|
10
|
+
authorizationServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
11
|
+
resource: z.ZodDefault<z.ZodString>;
|
|
12
|
+
additionalResources: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
13
|
+
resourceName: z.ZodOptional<z.ZodString>;
|
|
14
|
+
resourceDocumentation: z.ZodOptional<z.ZodString>;
|
|
15
|
+
anonymous: z.ZodDefault<z.ZodBoolean>;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
}, z.core.$strict>>;
|
|
18
|
+
}, z.core.$strict>;
|
|
19
|
+
/** Config telle que l'APP l'écrit dans `use()` — tous les champs optionnels. */
|
|
20
|
+
export type DevkitConfigInput = z.input<typeof devkitConfigSchema>;
|
|
21
|
+
/** Config telle que le CODE la lit — défauts appliqués, rien d'optionnel. */
|
|
22
|
+
export type DevkitConfig = z.output<typeof devkitConfigSchema>;
|
|
23
|
+
/** Défauts matérialisés (passés au `super()` du Module). */
|
|
24
|
+
declare const defaults: DevkitConfig;
|
|
25
|
+
export default defaults;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DevkitConfig, DevkitConfigInput } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* @nodefony/devkit — LE COMMENT : builder PUR de la config.
|
|
4
|
+
*
|
|
5
|
+
* ⭐ TL;DR : ce fichier VALIDE et GÈLE. Il ne porte AUCUNE valeur — les défauts
|
|
6
|
+
* vivent dans le schéma de `./config.ts` (règle d'or ADR-0006). Pour changer un
|
|
7
|
+
* défaut, c'est là-bas ; ici, on ne fait que le faire respecter.
|
|
8
|
+
*
|
|
9
|
+
* Le nom du FICHIER est le même dans tous les modules Nodefony
|
|
10
|
+
* (`defineModuleConfig.ts`) ; la FONCTION, elle, est préfixée par le module —
|
|
11
|
+
* deux modules importés côte à côte ne se marchent pas dessus.
|
|
12
|
+
*
|
|
13
|
+
* L'override env générique (`NF__DEVKIT__<CHEMIN>`) est appliqué par le core,
|
|
14
|
+
* pas ici. Si le module a besoin d'une variable d'env DÉDIÉE, elle s'applique
|
|
15
|
+
* APRÈS le parse, dans cette fonction, pour que le schéma reste pur.
|
|
16
|
+
*
|
|
17
|
+
* @param config - config brute venue de `use("@nodefony/devkit", { … })`.
|
|
18
|
+
* @returns config validée et gelée.
|
|
19
|
+
* @throws BootConfigurationError si un champ est invalide ou une clé inconnue
|
|
20
|
+
* (anomalies Zod agrégées) — le boot s'interrompt, en dev comme en prod.
|
|
21
|
+
*/
|
|
22
|
+
export declare function defineDevkitConfig(config?: DevkitConfigInput): DevkitConfig;
|
|
23
|
+
/**
|
|
24
|
+
* JSON Schema de la config — Studio en dérive son formulaire d'édition
|
|
25
|
+
* (libellés, types, défauts, descriptions), sans une ligne d'UI écrite à la main.
|
|
26
|
+
*/
|
|
27
|
+
export declare function devkitConfigJsonSchema(): unknown;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Controller } from "@nodefony/framework";
|
|
2
|
+
import type { ContextType } from "@nodefony/http";
|
|
3
|
+
/**
|
|
4
|
+
* La carte de visite de l'application, servie en HTTP.
|
|
5
|
+
*
|
|
6
|
+
* ## Pourquoi cette route n'est pas `/`
|
|
7
|
+
*
|
|
8
|
+
* Ce qu'elle rend — modules chargés, chemins de documentation, commandes à
|
|
9
|
+
* lancer — aide pendant le développement et n'est, en production, qu'une
|
|
10
|
+
* divulgation de l'architecture. Elle vit donc dans un module `policy: "dev"`,
|
|
11
|
+
* absent du boot en production, et sous le préfixe réservé `/nodefony` plutôt
|
|
12
|
+
* qu'à la racine : `/` appartient à l'application, qui doit pouvoir y répondre
|
|
13
|
+
* ce qu'elle assume devant ses utilisateurs.
|
|
14
|
+
*
|
|
15
|
+
* ## Pourquoi aucune garde `@IsGranted`
|
|
16
|
+
*
|
|
17
|
+
* Le module n'existe qu'en développement : c'est la `policy` qui le protège, pas
|
|
18
|
+
* un rôle. Exiger un rôle imposerait `@nodefony/security` à toute application
|
|
19
|
+
* qui installe le devkit — y compris celles qui n'ont pas de firewall du tout.
|
|
20
|
+
* Une garde qui force une dépendance protège moins qu'elle ne coûte.
|
|
21
|
+
*
|
|
22
|
+
* **Mince par design** : la composition de la carte vit dans le service (et sa
|
|
23
|
+
* construction dans une fonction pure) ; le controller ne fait que traduire en
|
|
24
|
+
* HTTP.
|
|
25
|
+
*/
|
|
26
|
+
declare class DevkitController extends Controller {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(context: ContextType);
|
|
29
|
+
/**
|
|
30
|
+
* `GET /nodefony/devkit/api/card` — qui répond, et où aller ensuite.
|
|
31
|
+
*
|
|
32
|
+
* La réponse est recalculée à chaque appel : elle décrit l'application TELLE
|
|
33
|
+
* QU'ELLE EST, pas telle qu'elle était au démarrage.
|
|
34
|
+
*/
|
|
35
|
+
card(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
|
|
36
|
+
}
|
|
37
|
+
export default DevkitController;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Controller } from "@nodefony/framework";
|
|
2
|
+
import type { ContextType } from "@nodefony/http";
|
|
3
|
+
/**
|
|
4
|
+
* Serveur **Model Context Protocol** de l'application — la porte par laquelle
|
|
5
|
+
* un agent externe l'interroge.
|
|
6
|
+
*
|
|
7
|
+
* ## Pourquoi une route, et pas un process
|
|
8
|
+
*
|
|
9
|
+
* La révision `2026-07-28` du transport « Streamable HTTP » a supprimé les
|
|
10
|
+
* sessions de niveau protocole et le flux `GET` : il ne reste qu'**un endpoint
|
|
11
|
+
* qui accepte `POST`**, chaque message étant autonome. Un serveur MCP n'a donc
|
|
12
|
+
* plus besoin d'être un process séparé lancé par le client — c'est une route de
|
|
13
|
+
* l'application, qui tourne déjà. Conséquence directe en développement : quand
|
|
14
|
+
* le superviseur relance le serveur sur une sauvegarde, rien n'est perdu, et la
|
|
15
|
+
* réponse suivante vient du code qui vient d'être rechargé. Aucun cache à
|
|
16
|
+
* invalider — la fraîcheur est une propriété du protocole.
|
|
17
|
+
*
|
|
18
|
+
* ## Ce qui protège cette route
|
|
19
|
+
*
|
|
20
|
+
* **Par défaut, le périmètre — et lui seul.** Ce module est `policy: "dev"`,
|
|
21
|
+
* donc cette route n'existe pas en production ; restent les deux gardes que la
|
|
22
|
+
* spec impose au transport, portées par {@link checkMcpAccess} : l'en-tête
|
|
23
|
+
* `Origin` et la localité de l'appelant. Elles visent le seul vecteur réel
|
|
24
|
+
* contre un serveur local — une page web ouverte dans le navigateur du
|
|
25
|
+
* développeur.
|
|
26
|
+
*
|
|
27
|
+
* **Dès qu'un serveur d'autorisation est déclaré** (`mcp.authorization`), la
|
|
28
|
+
* porte prend son rôle de *resource server* OAuth 2.1 : elle publie ses
|
|
29
|
+
* métadonnées (RFC 9728, publiées par `@nodefony/framework` depuis
|
|
30
|
+
* `DevkitService.publishedProtectedResources()`), valide le porteur
|
|
31
|
+
* présenté, et refuse en citant `resource_metadata` — l'en-tête qui apprend au
|
|
32
|
+
* client où obtenir un jeton (RFC 6750). L'audience attendue est l'URI
|
|
33
|
+
* canonique de la porte (RFC 8707) : c'est ce qui empêche un jeton émis pour un
|
|
34
|
+
* autre service d'être rejoué ici.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ **Le serveur d'AUTORISATION n'est pas de notre ressort**, et ne l'a jamais
|
|
37
|
+
* été : la spec le place « beyond the scope […] or a separate entity ». Avoir
|
|
38
|
+
* écrit l'inverse a servi d'excuse à ne rien faire pendant tout un cycle.
|
|
39
|
+
*
|
|
40
|
+
* 🔴 **La vérification du jeton est déléguée, et son absence est fatale.** Ce
|
|
41
|
+
* module ne peut pas dépendre de `@nodefony/security` (il disparaît en
|
|
42
|
+
* production, pas elle) : il cherche un `accessTokenVerifier` dans le conteneur
|
|
43
|
+
* — nom GÉNÉRIQUE, car le contrat prend l'audience en paramètre : un seul
|
|
44
|
+
* vérificateur sert toutes les ressources protégées d'une application, celle-ci
|
|
45
|
+
* étant seulement la première.
|
|
46
|
+
* Rôle déclaré + aucun vérificateur = `503` et journal `CRITIC`, jamais une
|
|
47
|
+
* porte qui laisse passer les porteurs sans les lire.
|
|
48
|
+
*
|
|
49
|
+
* **Mince par design** : tout le protocole vit AU CŒUR (`nodefony`, en fonctions
|
|
50
|
+
* pures) ; ce controller ne fait que traduire HTTP ↔ JSON-RPC et fournir au
|
|
51
|
+
* collecteur ce que lui seul connaît — le service du module, le broker
|
|
52
|
+
* d'administration, la racine du projet. C'est ce qui permet à un autre module
|
|
53
|
+
* d'ouvrir la même porte ailleurs (en production, sous authentification) sans
|
|
54
|
+
* réécrire une ligne de protocole.
|
|
55
|
+
*/
|
|
56
|
+
declare class McpController extends Controller {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(context: ContextType);
|
|
59
|
+
/**
|
|
60
|
+
* `POST /nodefony/mcp` — un message JSON-RPC entre, une réponse sort.
|
|
61
|
+
*
|
|
62
|
+
* Les trois statuts que rend cette route sont ceux que la spec impose, et pas
|
|
63
|
+
* un de plus : `202` sans corps pour une notification acceptée, `403` pour un
|
|
64
|
+
* appel dont l'origine ou l'adresse est refusée, `404` pour une méthode
|
|
65
|
+
* inconnue (afin de la distinguer d'un serveur qui n'hébergerait pas
|
|
66
|
+
* d'endpoint MCP du tout).
|
|
67
|
+
*/
|
|
68
|
+
mcp(body: unknown, origin?: string, protocolVersion?: string, authorization?: string): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
|
|
69
|
+
}
|
|
70
|
+
export default McpController;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Controller } from "@nodefony/framework";
|
|
2
|
+
import type { ContextType } from "@nodefony/http";
|
|
3
|
+
/**
|
|
4
|
+
* Métadonnées OAuth 2.1 de la porte MCP — **le seul moyen normalisé** pour
|
|
5
|
+
* qu'un agent apprenne où obtenir un jeton.
|
|
6
|
+
*
|
|
7
|
+
* ## Pourquoi un controller séparé
|
|
8
|
+
*
|
|
9
|
+
* Trois raisons, et aucune n'est esthétique. Le chemin vit **hors** du préfixe
|
|
10
|
+
* `/nodefony` (la RFC impose `/.well-known/…` juste après l'hôte). Le document
|
|
11
|
+
* est **public par conception** — un client doit pouvoir le lire avant d'avoir
|
|
12
|
+
* le moindre jeton, donc il ne porte ni garde d'origine ni exigence de
|
|
13
|
+
* localité, contrairement à la porte elle-même. Et il répond en `GET`, quand la
|
|
14
|
+
* porte n'accepte que `POST`.
|
|
15
|
+
*
|
|
16
|
+
* ## Ce que publier coûte, et ce que ne rien publier coûte
|
|
17
|
+
*
|
|
18
|
+
* Le document ne révèle rien de sensible : l'émetteur des jetons, les scopes
|
|
19
|
+
* compris, un nom. En revanche, ne PAS le publier a un coût mesuré — un client
|
|
20
|
+
* qui ne le trouve pas suppose que le serveur d'autorisation est le serveur
|
|
21
|
+
* lui-même, et part sonder des chemins qui n'existent pas. C'est exactement ce
|
|
22
|
+
* qui a produit le bruit OAuth observé sur cette porte.
|
|
23
|
+
*
|
|
24
|
+
* 🔴 **Rôle éteint = `404`, jamais un document vide.** Un document sans serveur
|
|
25
|
+
* d'autorisation apprendrait au client qu'un jeton est nécessaire sans jamais
|
|
26
|
+
* lui dire où le demander : la spécification MCP l'interdit d'ailleurs
|
|
27
|
+
* explicitement (« MUST include […] at least one authorization server »).
|
|
28
|
+
*/
|
|
29
|
+
declare class OAuthMetadataController extends Controller {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(context: ContextType);
|
|
32
|
+
/**
|
|
33
|
+
* `GET /.well-known/oauth-protected-resource/nodefony/mcp` — RFC 9728 §3.
|
|
34
|
+
*
|
|
35
|
+
* @returns le document JSON, ou `404` si cette porte n'est pas protégée
|
|
36
|
+
*/
|
|
37
|
+
metadata(): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
|
|
38
|
+
}
|
|
39
|
+
export default OAuthMetadataController;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ICard, ICardAppInfo, ICardDoor, ICardInput, ICardVerb, IMcpToolDeps } from "nodefony";
|
|
2
|
+
import type { DevkitConfig } from "../config/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Le contrat de la carte est celui du CŒUR — ces noms n'en sont que les alias,
|
|
5
|
+
* conservés parce qu'ils forment la surface publique de ce module.
|
|
6
|
+
*
|
|
7
|
+
* La forme de la carte ne peut avoir qu'une définition : la CLI
|
|
8
|
+
* (`nodefony card`, standalone 0-boot) et la porte HTTP de ce module rendent le
|
|
9
|
+
* MÊME objet. Deux déclarations parallèles auraient divergé au premier champ
|
|
10
|
+
* ajouté, et chacune aurait passé ses propres tests.
|
|
11
|
+
*/
|
|
12
|
+
export type IDevkitCard = ICard;
|
|
13
|
+
/** Identité de l'application qui répond. */
|
|
14
|
+
export type IDevkitAppInfo = ICardAppInfo;
|
|
15
|
+
/** Une PORTE : un endroit où aller chercher la suite. */
|
|
16
|
+
export type IDevkitDoor = ICardDoor;
|
|
17
|
+
/** Un VERBE : une commande à lancer (toujours préfixée `npx`). */
|
|
18
|
+
export type IDevkitVerb = ICardVerb;
|
|
19
|
+
/** L'état minimal dont la carte se dérive — injecté, jamais lu. */
|
|
20
|
+
export type IDevkitCardInput = ICardInput;
|
|
21
|
+
/**
|
|
22
|
+
* API publique de `DevkitService` (injectable, nom `devkit`).
|
|
23
|
+
*
|
|
24
|
+
* L'interface est le CONTRAT : ce que les autres modules (et Studio) peuvent
|
|
25
|
+
* appeler. Tout ce qui n'est pas ici est un détail d'implémentation, libre de
|
|
26
|
+
* changer.
|
|
27
|
+
*/
|
|
28
|
+
export interface IDevkitService {
|
|
29
|
+
/** Snapshot de lecture — état courant du service. */
|
|
30
|
+
status(): {
|
|
31
|
+
ready: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Carte de visite de l'application, DÉRIVÉE de l'état du Kernel.
|
|
35
|
+
*
|
|
36
|
+
* Le module ne stocke rien en propre : ce qu'il rend est recalculé à la
|
|
37
|
+
* lecture. Une carte mise en cache mentirait au premier module ajouté.
|
|
38
|
+
*
|
|
39
|
+
* Ici — et ici seulement — la liste des modules est celle des modules
|
|
40
|
+
* réellement CHARGÉS (`source: "runtime"`) : le Kernel tourne, le gating
|
|
41
|
+
* `policy`/`when` a déjà eu lieu. La porte CLI, elle, répond à froid et le
|
|
42
|
+
* DIT.
|
|
43
|
+
*/
|
|
44
|
+
getCard(): IDevkitCard;
|
|
45
|
+
/**
|
|
46
|
+
* Réglages effectifs du serveur MCP (défauts du schéma + surcharges de l'app).
|
|
47
|
+
*
|
|
48
|
+
* Exposé sur le contrat parce que la porte HTTP les lit : gardes d'accès et
|
|
49
|
+
* allowlist d'outils viennent d'ICI, jamais d'une seconde lecture de la
|
|
50
|
+
* configuration — deux lectures divergent.
|
|
51
|
+
*/
|
|
52
|
+
mcpSettings(): DevkitConfig["mcp"];
|
|
53
|
+
/**
|
|
54
|
+
* Ce dont les outils MCP intégrés ont besoin pour répondre.
|
|
55
|
+
*
|
|
56
|
+
* Sur le contrat parce que la porte HTTP les passe au catalogue : les
|
|
57
|
+
* composer de son côté ferait une seconde source, qui divergerait de celle
|
|
58
|
+
* dont sont dérivés les scopes publiés.
|
|
59
|
+
*/
|
|
60
|
+
mcpToolDeps(): IMcpToolDeps;
|
|
61
|
+
/**
|
|
62
|
+
* Les scopes que la porte MCP EXIGE, dérivés des outils déclarés.
|
|
63
|
+
*
|
|
64
|
+
* Deux lecteurs, une seule source : le document RFC 9728 (`scopes_supported`)
|
|
65
|
+
* et le défi opposé à un porteur refusé (RFC 6750 `scope`). Vide = la porte
|
|
66
|
+
* n'exige rien, et le champ est alors omis du document.
|
|
67
|
+
*/
|
|
68
|
+
declaredMcpScopes(): readonly string[];
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { IDevkitService } from "./IDevkitService.js";
|