@nodefony/devkit 10.0.0-alpha.1

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