@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,200 @@
|
|
|
1
|
+
import { BUILTIN_MCP_TOOL_KEYS, canonicalResourceUri } from "nodefony";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region nodefony/config/config.ts
|
|
4
|
+
/**
|
|
5
|
+
* @nodefony/devkit — CONFIGURATION DU MODULE (schéma Zod = source unique).
|
|
6
|
+
*
|
|
7
|
+
* ⭐ TL;DR : CE SCHÉMA EST LA CONFIG. Chaque `.default(...)` est la valeur d'usine ;
|
|
8
|
+
* changer un défaut du module = ÉDITER ICI (et nulle part ailleurs). L'app, elle,
|
|
9
|
+
* ne surcharge que ses écarts, via `use("@nodefony/devkit", { … })`.
|
|
10
|
+
*
|
|
11
|
+
* RÈGLE D'OR (ADR-0006) : ce fichier porte le schéma commenté (type + validation +
|
|
12
|
+
* défaut + doc) ET matérialise les défauts par `parse({})`. Le builder
|
|
13
|
+
* (`defineDevkitConfig`) importe le schéma d'ici et ne retape aucune valeur —
|
|
14
|
+
* un défaut écrit à deux endroits finit toujours par diverger.
|
|
15
|
+
*
|
|
16
|
+
* SURCHARGE (précédence croissante) :
|
|
17
|
+
* • défauts du schéma (ce fichier) ;
|
|
18
|
+
* • app : `use("@nodefony/devkit", { … })` dans `nodefony.config.ts` ;
|
|
19
|
+
* • déploiement : `NF__DEVKIT__<CHEMIN>=valeur` (override env générique, `__` = niveau).
|
|
20
|
+
*
|
|
21
|
+
* Le schéma reste **PUR** : aucune lecture de `process.env`, aucun accès au kernel
|
|
22
|
+
* (il est évalué à l'import — le kernel n'existe pas encore).
|
|
23
|
+
*
|
|
24
|
+
* Champ sensible (clé, mot de passe) → `.meta({ secret: true })` EN DERNIER de la
|
|
25
|
+
* chaîne : chaque méthode zod clone, une `.default()` posée après `.meta()` perdrait
|
|
26
|
+
* la métadonnée. Ces flags ressortent dans le JSON Schema lu par Studio.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Rôle **serveur de ressource OAuth 2.1** de la porte MCP — éteint par défaut.
|
|
30
|
+
*
|
|
31
|
+
* ⭐ **Un seul réglage commande : {@link authorizationServers}.** Vide, la porte
|
|
32
|
+
* se comporte exactement comme avant — anonyme, aucune métadonnée publiée,
|
|
33
|
+
* aucun `401`. Non vide, le rôle s'allume ENTIÈREMENT. Un interrupteur séparé
|
|
34
|
+
* aurait permis deux états qui se contredisent (« protégé mais sans serveur
|
|
35
|
+
* d'autorisation », « métadonnées publiées mais jetons ignorés »), et c'est
|
|
36
|
+
* précisément ce genre d'état que personne ne diagnostique.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ **Allumer ce rôle ne suffit pas à protéger quoi que ce soit** : il faut
|
|
39
|
+
* aussi qu'un service du conteneur sache VÉRIFIER un jeton. S'il n'y en a pas,
|
|
40
|
+
* la porte refuse de servir (et le dit fort) plutôt que d'accepter n'importe
|
|
41
|
+
* quel porteur.
|
|
42
|
+
*/
|
|
43
|
+
const mcpAuthorizationSchema = z.strictObject({
|
|
44
|
+
/**
|
|
45
|
+
* Serveurs d'autorisation capables d'émettre un jeton pour cette porte.
|
|
46
|
+
*
|
|
47
|
+
* Vide (défaut) = rôle éteint. Non vide, la spécification MCP impose que le
|
|
48
|
+
* document de métadonnées les publie (« MUST include the
|
|
49
|
+
* `authorization_servers` field containing at least one authorization
|
|
50
|
+
* server ») : sans eux, un client apprendrait qu'un jeton est nécessaire
|
|
51
|
+
* sans jamais pouvoir en obtenir un.
|
|
52
|
+
*/
|
|
53
|
+
authorizationServers: z.array(z.string()).default([]).describe("Émetteurs de jetons acceptés (issuer OAuth) ; vide = aucune autorisation"),
|
|
54
|
+
/**
|
|
55
|
+
* URI canonique publique de cette porte — l'audience que les jetons doivent
|
|
56
|
+
* porter (RFC 8707).
|
|
57
|
+
*
|
|
58
|
+
* 🔴 **Elle s'écrit, elle ne se devine pas depuis l'en-tête `Host`.** Si
|
|
59
|
+
* l'URI publiée et l'audience attendue venaient toutes deux de la requête,
|
|
60
|
+
* un `Host` forgé obtiendrait un jeton d'audience arbitraire *et* passerait
|
|
61
|
+
* la vérification : la liaison d'audience, dont c'est l'unique raison
|
|
62
|
+
* d'être, ne protégerait plus rien.
|
|
63
|
+
*
|
|
64
|
+
* Ex. `https://mon-app.example/nodefony/mcp`.
|
|
65
|
+
*/
|
|
66
|
+
resource: z.string().default("").describe("URI publique de la porte MCP (audience attendue des jetons)"),
|
|
67
|
+
/**
|
|
68
|
+
* Autres adresses de CETTE MÊME porte, dont les jetons sont aussi acceptés.
|
|
69
|
+
*
|
|
70
|
+
* Le cas courant : l'application sert la porte en clair sur un port et en
|
|
71
|
+
* TLS sur un autre. Un jeton émis pour l'une était alors refusé sur l'autre
|
|
72
|
+
* — la liaison d'audience (RFC 8707) faisant son travail. Nommer la seconde
|
|
73
|
+
* adresse est la bonne réponse ; relâcher la liaison n'en serait pas une.
|
|
74
|
+
*
|
|
75
|
+
* 🔴 Ces valeurs s'ÉCRIVENT. Dérivées du `Host`, un en-tête forgé
|
|
76
|
+
* obtiendrait un jeton d'audience arbitraire ET passerait la vérification.
|
|
77
|
+
* Seule `resource` est publiée en RFC 9728 : le document n'admet qu'une
|
|
78
|
+
* valeur, et un client n'a besoin que d'une adresse.
|
|
79
|
+
*
|
|
80
|
+
* ⚠️ L'ÉMETTEUR doit servir ces audiences (`security.jwt.audiences`), sinon
|
|
81
|
+
* il refuse de les émettre — `invalid_target`, et à juste titre.
|
|
82
|
+
*/
|
|
83
|
+
additionalResources: z.array(z.string()).default([]).describe("Autres URI de la même porte dont les jetons sont acceptés (jamais dérivées du Host)"),
|
|
84
|
+
/** Nom lisible, affiché pendant le consentement. */
|
|
85
|
+
resourceName: z.string().optional().describe("Nom affiché au consentement"),
|
|
86
|
+
/** Page de documentation destinée à un humain. */
|
|
87
|
+
resourceDocumentation: z.string().optional().describe("URL de documentation de la ressource"),
|
|
88
|
+
/**
|
|
89
|
+
* Servir une requête SANS jeton comme anonyme, au lieu de la refuser.
|
|
90
|
+
*
|
|
91
|
+
* `false` (défaut) : sans jeton, `401` + `WWW-Authenticate` — c'est ce qui
|
|
92
|
+
* apprend au client qu'une autorisation existe et où l'obtenir.
|
|
93
|
+
* `true` : la porte reste ouverte aux outils publics, et les outils
|
|
94
|
+
* réservés restent retenus. Utile en développement ; c'est un choix qui
|
|
95
|
+
* s'écrit, jamais un comportement dont on hérite.
|
|
96
|
+
*/
|
|
97
|
+
anonymous: z.boolean().default(false).describe("Tolérer les requêtes sans jeton (outils publics seulement)")
|
|
98
|
+
}).superRefine((value, ctx) => {
|
|
99
|
+
if (value.authorizationServers.length === 0) return;
|
|
100
|
+
if (value.resource.length === 0) {
|
|
101
|
+
ctx.addIssue({
|
|
102
|
+
code: "custom",
|
|
103
|
+
path: ["resource"],
|
|
104
|
+
message: "`mcp.authorization.resource` est requis dès qu'un serveur d'autorisation est déclaré : c'est l'URI publique de la porte, et l'audience que les jetons doivent porter (RFC 8707)."
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
canonicalResourceUri(value.resource);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: "custom",
|
|
113
|
+
path: ["resource"],
|
|
114
|
+
message: error.message
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* Réglages du serveur MCP — extrait en constante pour une raison mécanique.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ **Piège Zod 4** : un `.default({})` posé à plat sur un sous-objet NE
|
|
122
|
+
* ré-applique PAS les défauts de ses champs. Le pattern du dépôt est donc
|
|
123
|
+
* `sous.default(() => sous.parse({}))` — le callback force la ré-évaluation, et
|
|
124
|
+
* une application qui n'écrit rien obtient bien les valeurs d'usine.
|
|
125
|
+
*/
|
|
126
|
+
const mcpSchema = z.strictObject({
|
|
127
|
+
/** Répond-on aux requêtes MCP ? Coupé, la route rend `404`. */
|
|
128
|
+
enabled: z.boolean().default(true).describe("Active l'endpoint MCP (POST /nodefony/mcp)"),
|
|
129
|
+
/**
|
|
130
|
+
* Origines acceptées quand l'en-tête `Origin` est PRÉSENT.
|
|
131
|
+
*
|
|
132
|
+
* ⭐ La subtilité qui fait toute la sécurité : un client MCP natif est un
|
|
133
|
+
* process, pas un navigateur — **il n'envoie pas d'`Origin`**. Une page web
|
|
134
|
+
* malveillante, elle, en envoie toujours un vers `localhost`. La règle est
|
|
135
|
+
* donc : *absent → on passe* ; *présent et hors de cette liste → `403`*.
|
|
136
|
+
* C'est ce que la spec exige (`transports/streamable-http` §Security :
|
|
137
|
+
* « Servers MUST validate the `Origin` header […] MUST respond with HTTP 403
|
|
138
|
+
* Forbidden »), et c'est ce qui ferme le DNS rebinding.
|
|
139
|
+
*
|
|
140
|
+
* Vide (défaut) = aucune origine de navigateur n'est admise.
|
|
141
|
+
*/
|
|
142
|
+
allowedOrigins: z.array(z.string()).default([]).describe("Origines navigateur admises ; vide = aucune"),
|
|
143
|
+
/**
|
|
144
|
+
* Accepter un appel dont l'adresse distante n'est pas locale.
|
|
145
|
+
*
|
|
146
|
+
* La spec recommande de n'écouter que sur la boucle locale ; le serveur de
|
|
147
|
+
* l'application, lui, écoute pour le développement. La garde est donc portée
|
|
148
|
+
* par la route : toute adresse non locale reçoit `403`.
|
|
149
|
+
*/
|
|
150
|
+
allowRemote: z.boolean().default(false).describe("Autorise les appels depuis une adresse non locale"),
|
|
151
|
+
/**
|
|
152
|
+
* Outils exposés — **allowlist**, et lecture seule.
|
|
153
|
+
*
|
|
154
|
+
* Tant qu'aucun outil n'écrit, le pire qu'obtienne un appelant illégitime est
|
|
155
|
+
* la description de l'application. Le jour où un générateur sera exposé, ce
|
|
156
|
+
* sera un ajout explicite ici — et cette ligne sera l'endroit où quelqu'un
|
|
157
|
+
* décide de donner à un agent le droit d'écrire des fichiers.
|
|
158
|
+
*/
|
|
159
|
+
tools: z.array(z.string()).default([...BUILTIN_MCP_TOOL_KEYS]).describe("Outils MCP activés (allowlist, lecture seule)"),
|
|
160
|
+
/**
|
|
161
|
+
* Rôle serveur de ressource OAuth 2.1 — **éteint par défaut**.
|
|
162
|
+
*
|
|
163
|
+
* Tant qu'aucun serveur d'autorisation n'est déclaré, la porte se comporte
|
|
164
|
+
* comme elle l'a toujours fait : anonyme, protégée par son seul périmètre
|
|
165
|
+
* (`policy: "dev"` + gardes `Origin`/localité).
|
|
166
|
+
*/
|
|
167
|
+
authorization: mcpAuthorizationSchema.default(() => mcpAuthorizationSchema.parse({})).describe("Serveur de ressource OAuth 2.1 (RFC 9728/6750/8707)")
|
|
168
|
+
});
|
|
169
|
+
const devkitConfigSchema = z.strictObject({
|
|
170
|
+
/** Interrupteur du module — l'app peut le charger sans l'activer. */
|
|
171
|
+
enabled: z.boolean().default(true).describe("Active les fonctionnalités du module devkit"),
|
|
172
|
+
/**
|
|
173
|
+
* Serveur MCP — la porte par laquelle un agent externe interroge l'app.
|
|
174
|
+
*
|
|
175
|
+
* ## Pourquoi aucune autorisation OAuth
|
|
176
|
+
*
|
|
177
|
+
* L'autorisation MCP est **optionnelle** (spec 2026-07-28, `authorization`
|
|
178
|
+
* §« Protocol Requirements »), et la faire en HTTP signifierait implémenter
|
|
179
|
+
* OAuth 2.1 en entier — serveur de ressource, métadonnées `RFC 9728`,
|
|
180
|
+
* découverte du serveur d'autorisation, PKCE, liaison d'audience. Un client
|
|
181
|
+
* MCP conforme ne sait de toute façon pas présenter un cookie de session
|
|
182
|
+
* Nodefony : il ne connaît que le jeton Bearer OAuth.
|
|
183
|
+
*
|
|
184
|
+
* Ce qui protège ici, c'est le PÉRIMÈTRE, et il est borné par construction :
|
|
185
|
+
* le module est `policy: "dev"`, donc **cette route n'existe pas en
|
|
186
|
+
* production** (le Kernel écarte le module au boot). Restent les deux gardes
|
|
187
|
+
* que la spec impose au transport lui-même, et qui visent le risque réel
|
|
188
|
+
* d'un serveur local — une page web ouverte dans le navigateur du
|
|
189
|
+
* développeur : {@link allowedOrigins} et {@link allowRemote}.
|
|
190
|
+
*
|
|
191
|
+
* ⚠️ **Écart assumé et énoncé** : la spec dit aussi « Servers SHOULD
|
|
192
|
+
* implement proper authentication for all connections ». Nous ne le faisons
|
|
193
|
+
* pas. Une capacité absente s'énonce plutôt qu'elle ne se masque.
|
|
194
|
+
*/
|
|
195
|
+
mcp: mcpSchema.default(() => mcpSchema.parse({})).describe("Serveur MCP pour agents externes (développement uniquement)")
|
|
196
|
+
});
|
|
197
|
+
/** Défauts matérialisés (passés au `super()` du Module). */
|
|
198
|
+
const defaults = devkitConfigSchema.parse({});
|
|
199
|
+
//#endregion
|
|
200
|
+
export { defaults as default, devkitConfigSchema };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { devkitConfigSchema } from "./config.js";
|
|
2
|
+
import { parseModuleConfig } from "nodefony";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region nodefony/config/defineModuleConfig.ts
|
|
5
|
+
/**
|
|
6
|
+
* @nodefony/devkit — LE COMMENT : builder PUR de la config.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ TL;DR : ce fichier VALIDE et GÈLE. Il ne porte AUCUNE valeur — les défauts
|
|
9
|
+
* vivent dans le schéma de `./config.ts` (règle d'or ADR-0006). Pour changer un
|
|
10
|
+
* défaut, c'est là-bas ; ici, on ne fait que le faire respecter.
|
|
11
|
+
*
|
|
12
|
+
* Le nom du FICHIER est le même dans tous les modules Nodefony
|
|
13
|
+
* (`defineModuleConfig.ts`) ; la FONCTION, elle, est préfixée par le module —
|
|
14
|
+
* deux modules importés côte à côte ne se marchent pas dessus.
|
|
15
|
+
*
|
|
16
|
+
* L'override env générique (`NF__DEVKIT__<CHEMIN>`) est appliqué par le core,
|
|
17
|
+
* pas ici. Si le module a besoin d'une variable d'env DÉDIÉE, elle s'applique
|
|
18
|
+
* APRÈS le parse, dans cette fonction, pour que le schéma reste pur.
|
|
19
|
+
*
|
|
20
|
+
* @param config - config brute venue de `use("@nodefony/devkit", { … })`.
|
|
21
|
+
* @returns config validée et gelée.
|
|
22
|
+
* @throws BootConfigurationError si un champ est invalide ou une clé inconnue
|
|
23
|
+
* (anomalies Zod agrégées) — le boot s'interrompt, en dev comme en prod.
|
|
24
|
+
*/
|
|
25
|
+
function defineDevkitConfig(config = {}) {
|
|
26
|
+
return Object.freeze(parseModuleConfig(devkitConfigSchema, config, "@nodefony/devkit"));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* JSON Schema de la config — Studio en dérive son formulaire d'édition
|
|
30
|
+
* (libellés, types, défauts, descriptions), sans une ligne d'UI écrite à la main.
|
|
31
|
+
*/
|
|
32
|
+
function devkitConfigJsonSchema() {
|
|
33
|
+
return z.toJSONSchema(devkitConfigSchema);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { defineDevkitConfig, devkitConfigJsonSchema };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
3
|
+
import { Controller, controller, route } from "@nodefony/framework";
|
|
4
|
+
//#region nodefony/controllers/DevkitController.ts
|
|
5
|
+
/**
|
|
6
|
+
* La carte de visite de l'application, servie en HTTP.
|
|
7
|
+
*
|
|
8
|
+
* ## Pourquoi cette route n'est pas `/`
|
|
9
|
+
*
|
|
10
|
+
* Ce qu'elle rend — modules chargés, chemins de documentation, commandes à
|
|
11
|
+
* lancer — aide pendant le développement et n'est, en production, qu'une
|
|
12
|
+
* divulgation de l'architecture. Elle vit donc dans un module `policy: "dev"`,
|
|
13
|
+
* absent du boot en production, et sous le préfixe réservé `/nodefony` plutôt
|
|
14
|
+
* qu'à la racine : `/` appartient à l'application, qui doit pouvoir y répondre
|
|
15
|
+
* ce qu'elle assume devant ses utilisateurs.
|
|
16
|
+
*
|
|
17
|
+
* ## Pourquoi aucune garde `@IsGranted`
|
|
18
|
+
*
|
|
19
|
+
* Le module n'existe qu'en développement : c'est la `policy` qui le protège, pas
|
|
20
|
+
* un rôle. Exiger un rôle imposerait `@nodefony/security` à toute application
|
|
21
|
+
* qui installe le devkit — y compris celles qui n'ont pas de firewall du tout.
|
|
22
|
+
* Une garde qui force une dépendance protège moins qu'elle ne coûte.
|
|
23
|
+
*
|
|
24
|
+
* **Mince par design** : la composition de la carte vit dans le service (et sa
|
|
25
|
+
* construction dans une fonction pure) ; le controller ne fait que traduire en
|
|
26
|
+
* HTTP.
|
|
27
|
+
*/
|
|
28
|
+
let DevkitController = class DevkitController extends Controller {
|
|
29
|
+
constructor(context) {
|
|
30
|
+
super("devkit", context);
|
|
31
|
+
}
|
|
32
|
+
/** Résout le service depuis le conteneur partagé (clé d'instance). */
|
|
33
|
+
#service() {
|
|
34
|
+
const svc = this.get("devkit");
|
|
35
|
+
if (!svc) throw new Error("DevkitService non enregistré");
|
|
36
|
+
return svc;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `GET /nodefony/devkit/api/card` — qui répond, et où aller ensuite.
|
|
40
|
+
*
|
|
41
|
+
* La réponse est recalculée à chaque appel : elle décrit l'application TELLE
|
|
42
|
+
* QU'ELLE EST, pas telle qu'elle était au démarrage.
|
|
43
|
+
*/
|
|
44
|
+
async card() {
|
|
45
|
+
return this.renderJson(this.#service().getCard());
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
__decorate([
|
|
49
|
+
route("devkit-card", {
|
|
50
|
+
path: "/card",
|
|
51
|
+
method: "GET"
|
|
52
|
+
}),
|
|
53
|
+
__decorateMetadata("design:type", Function),
|
|
54
|
+
__decorateMetadata("design:paramtypes", []),
|
|
55
|
+
__decorateMetadata("design:returntype", Promise)
|
|
56
|
+
], DevkitController.prototype, "card", null);
|
|
57
|
+
DevkitController = __decorate([controller("/nodefony/devkit/api"), __decorateMetadata("design:paramtypes", [Object])], DevkitController);
|
|
58
|
+
var DevkitController_default = DevkitController;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { DevkitController_default as default };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
3
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
|
|
4
|
+
import { ACCESS_TOKEN_VERIFIER, JsonRpcError, Nodefony, authorizeProtectedResource, checkMcpAccess, collectMcpTools, handleMcpMessage, jsonRpcFailure, mcpCallerRoles, protectedResourceMetadataUrl, readBearerHeader } from "nodefony";
|
|
5
|
+
import { Body, Controller, Headers, controller, route } from "@nodefony/framework";
|
|
6
|
+
//#region nodefony/controllers/McpController.ts
|
|
7
|
+
/**
|
|
8
|
+
* Serveur **Model Context Protocol** de l'application — la porte par laquelle
|
|
9
|
+
* un agent externe l'interroge.
|
|
10
|
+
*
|
|
11
|
+
* ## Pourquoi une route, et pas un process
|
|
12
|
+
*
|
|
13
|
+
* La révision `2026-07-28` du transport « Streamable HTTP » a supprimé les
|
|
14
|
+
* sessions de niveau protocole et le flux `GET` : il ne reste qu'**un endpoint
|
|
15
|
+
* qui accepte `POST`**, chaque message étant autonome. Un serveur MCP n'a donc
|
|
16
|
+
* plus besoin d'être un process séparé lancé par le client — c'est une route de
|
|
17
|
+
* l'application, qui tourne déjà. Conséquence directe en développement : quand
|
|
18
|
+
* le superviseur relance le serveur sur une sauvegarde, rien n'est perdu, et la
|
|
19
|
+
* réponse suivante vient du code qui vient d'être rechargé. Aucun cache à
|
|
20
|
+
* invalider — la fraîcheur est une propriété du protocole.
|
|
21
|
+
*
|
|
22
|
+
* ## Ce qui protège cette route
|
|
23
|
+
*
|
|
24
|
+
* **Par défaut, le périmètre — et lui seul.** Ce module est `policy: "dev"`,
|
|
25
|
+
* donc cette route n'existe pas en production ; restent les deux gardes que la
|
|
26
|
+
* spec impose au transport, portées par {@link checkMcpAccess} : l'en-tête
|
|
27
|
+
* `Origin` et la localité de l'appelant. Elles visent le seul vecteur réel
|
|
28
|
+
* contre un serveur local — une page web ouverte dans le navigateur du
|
|
29
|
+
* développeur.
|
|
30
|
+
*
|
|
31
|
+
* **Dès qu'un serveur d'autorisation est déclaré** (`mcp.authorization`), la
|
|
32
|
+
* porte prend son rôle de *resource server* OAuth 2.1 : elle publie ses
|
|
33
|
+
* métadonnées (RFC 9728, publiées par `@nodefony/framework` depuis
|
|
34
|
+
* `DevkitService.publishedProtectedResources()`), valide le porteur
|
|
35
|
+
* présenté, et refuse en citant `resource_metadata` — l'en-tête qui apprend au
|
|
36
|
+
* client où obtenir un jeton (RFC 6750). L'audience attendue est l'URI
|
|
37
|
+
* canonique de la porte (RFC 8707) : c'est ce qui empêche un jeton émis pour un
|
|
38
|
+
* autre service d'être rejoué ici.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ **Le serveur d'AUTORISATION n'est pas de notre ressort**, et ne l'a jamais
|
|
41
|
+
* été : la spec le place « beyond the scope […] or a separate entity ». Avoir
|
|
42
|
+
* écrit l'inverse a servi d'excuse à ne rien faire pendant tout un cycle.
|
|
43
|
+
*
|
|
44
|
+
* 🔴 **La vérification du jeton est déléguée, et son absence est fatale.** Ce
|
|
45
|
+
* module ne peut pas dépendre de `@nodefony/security` (il disparaît en
|
|
46
|
+
* production, pas elle) : il cherche un `accessTokenVerifier` dans le conteneur
|
|
47
|
+
* — nom GÉNÉRIQUE, car le contrat prend l'audience en paramètre : un seul
|
|
48
|
+
* vérificateur sert toutes les ressources protégées d'une application, celle-ci
|
|
49
|
+
* étant seulement la première.
|
|
50
|
+
* Rôle déclaré + aucun vérificateur = `503` et journal `CRITIC`, jamais une
|
|
51
|
+
* porte qui laisse passer les porteurs sans les lire.
|
|
52
|
+
*
|
|
53
|
+
* **Mince par design** : tout le protocole vit AU CŒUR (`nodefony`, en fonctions
|
|
54
|
+
* pures) ; ce controller ne fait que traduire HTTP ↔ JSON-RPC et fournir au
|
|
55
|
+
* collecteur ce que lui seul connaît — le service du module, le broker
|
|
56
|
+
* d'administration, la racine du projet. C'est ce qui permet à un autre module
|
|
57
|
+
* d'ouvrir la même porte ailleurs (en production, sous authentification) sans
|
|
58
|
+
* réécrire une ligne de protocole.
|
|
59
|
+
*/
|
|
60
|
+
let McpController = class McpController extends Controller {
|
|
61
|
+
constructor(context) {
|
|
62
|
+
super("devkit-mcp", context);
|
|
63
|
+
}
|
|
64
|
+
/** Résout le service du module depuis le conteneur partagé. */
|
|
65
|
+
#service() {
|
|
66
|
+
const svc = this.get("devkit");
|
|
67
|
+
if (!svc) throw new Error("DevkitService non enregistré");
|
|
68
|
+
return svc;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Ce qui, dans cette application, sait valider un jeton — ou rien.
|
|
72
|
+
*
|
|
73
|
+
* Résolu par NOM dans le conteneur, et non importé : ce module est
|
|
74
|
+
* `policy: "dev"` et ne peut pas dépendre de `@nodefony/security`, qui porte
|
|
75
|
+
* la cryptographie. Une application peut donc fournir sa propre
|
|
76
|
+
* implémentation du contrat sous ce nom, sans que cette porte en sache quoi
|
|
77
|
+
* que ce soit.
|
|
78
|
+
*
|
|
79
|
+
* L'absence est un cas NORMAL (rôle éteint) ou une faute de configuration
|
|
80
|
+
* (rôle allumé) — c'est l'appelant qui tranche, pas cette méthode.
|
|
81
|
+
*/
|
|
82
|
+
#tokenVerifier() {
|
|
83
|
+
return this.get(ACCESS_TOKEN_VERIFIER) ?? void 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* L'en-tête porte-t-il un gabarit d'interpolation jamais substitué ?
|
|
87
|
+
*
|
|
88
|
+
* Un `.mcp.json` déclare son jeton par `${NF_MCP_TOKEN}` : le client
|
|
89
|
+
* développe la variable, ou transmet la chaîne telle quelle si elle manque.
|
|
90
|
+
* Le second cas ne se distingue d'un jeton invalide par AUCUN symptôme
|
|
91
|
+
* observable de l'extérieur, et c'est pourtant le plus fréquent des deux —
|
|
92
|
+
* une session lancée depuis un terminal où la variable n'était pas posée.
|
|
93
|
+
*
|
|
94
|
+
* @param header - la valeur brute de l'en-tête `Authorization`
|
|
95
|
+
* @returns vrai si ce qui est présenté est resté un gabarit
|
|
96
|
+
*/
|
|
97
|
+
#isUnsubstitutedTemplate(header) {
|
|
98
|
+
const read = readBearerHeader(header);
|
|
99
|
+
return read.kind === "token" && read.token.startsWith("${") && read.token.endsWith("}");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* `POST /nodefony/mcp` — un message JSON-RPC entre, une réponse sort.
|
|
103
|
+
*
|
|
104
|
+
* Les trois statuts que rend cette route sont ceux que la spec impose, et pas
|
|
105
|
+
* un de plus : `202` sans corps pour une notification acceptée, `403` pour un
|
|
106
|
+
* appel dont l'origine ou l'adresse est refusée, `404` pour une méthode
|
|
107
|
+
* inconnue (afin de la distinguer d'un serveur qui n'hébergerait pas
|
|
108
|
+
* d'endpoint MCP du tout).
|
|
109
|
+
*/
|
|
110
|
+
async mcp(body, origin, protocolVersion, authorization) {
|
|
111
|
+
const settings = this.#service().mcpSettings();
|
|
112
|
+
if (!settings.enabled) return this.renderJson({ error: "MCP désactivé" }, 404);
|
|
113
|
+
const verdict = checkMcpAccess({
|
|
114
|
+
origin,
|
|
115
|
+
remoteAddress: this.context?.remoteAddress ?? void 0
|
|
116
|
+
}, {
|
|
117
|
+
allowedOrigins: settings.allowedOrigins,
|
|
118
|
+
allowRemote: settings.allowRemote
|
|
119
|
+
});
|
|
120
|
+
if (!verdict.allowed) {
|
|
121
|
+
this.log(`MCP refusé — ${verdict.why}`, "WARNING");
|
|
122
|
+
return this.renderJson(jsonRpcFailure(null, JsonRpcError.INVALID_REQUEST, "origine refusée"), 403);
|
|
123
|
+
}
|
|
124
|
+
if (body === null || typeof body !== "object") return this.renderJson(jsonRpcFailure(null, JsonRpcError.PARSE_ERROR, "corps attendu : un message JSON-RPC"), 400);
|
|
125
|
+
const authz = settings.authorization;
|
|
126
|
+
const gateProtected = settings.authorization.authorizationServers.length > 0;
|
|
127
|
+
let caller = {
|
|
128
|
+
authenticated: false,
|
|
129
|
+
scopes: [],
|
|
130
|
+
roles: mcpCallerRoles({
|
|
131
|
+
protected: gateProtected,
|
|
132
|
+
authenticated: false,
|
|
133
|
+
scopes: []
|
|
134
|
+
})
|
|
135
|
+
};
|
|
136
|
+
if (gateProtected) {
|
|
137
|
+
const authVerdict = await authorizeProtectedResource(authorization, {
|
|
138
|
+
resource: authz.resource,
|
|
139
|
+
acceptedResources: authz.additionalResources,
|
|
140
|
+
metadataUrl: protectedResourceMetadataUrl(authz.resource),
|
|
141
|
+
scopes: this.#service().declaredMcpScopes(),
|
|
142
|
+
allowAnonymous: authz.anonymous
|
|
143
|
+
}, this.#tokenVerifier());
|
|
144
|
+
switch (authVerdict.outcome) {
|
|
145
|
+
case "unverifiable":
|
|
146
|
+
this.log(authVerdict.why ? `MCP — la vérification du jeton a ÉCHOUÉ, le jeton n'est donc ni accepté ni refusé : la porte refuse de servir (503). Cause — ${authVerdict.why}` : "MCP — `mcp.authorization` déclare un serveur d'autorisation, mais aucun service du conteneur ne sait vérifier un jeton (`accessTokenVerifier`). La porte refuse de servir : accepter les porteurs sans les valider serait pire que rester anonyme.", "CRITIC");
|
|
147
|
+
return this.renderJson(jsonRpcFailure(null, JsonRpcError.INTERNAL_ERROR, "autorisation indisponible"), 503);
|
|
148
|
+
case "challenge":
|
|
149
|
+
if (this.#isUnsubstitutedTemplate(authorization)) this.log("MCP — l'en-tête `Authorization` porte un GABARIT non substitué : la variable d'environnement du jeton n'est pas posée dans le process du client. Émettre le jeton (`nodefony security:token`) et le poser, ou rendre la porte anonyme pour ce client (`nodefony ai:mcp --no-auth`) — sans en-tête, les outils publics sont servis.", "WARNING");
|
|
150
|
+
return this.renderJson(jsonRpcFailure(null, JsonRpcError.INVALID_REQUEST, "autorisation requise"), authVerdict.status, { "WWW-Authenticate": authVerdict.wwwAuthenticate });
|
|
151
|
+
case "authenticated":
|
|
152
|
+
caller = {
|
|
153
|
+
authenticated: true,
|
|
154
|
+
scopes: authVerdict.principal.scopes,
|
|
155
|
+
subject: authVerdict.principal.subject,
|
|
156
|
+
roles: mcpCallerRoles({
|
|
157
|
+
protected: true,
|
|
158
|
+
authenticated: true,
|
|
159
|
+
scopes: authVerdict.principal.scopes
|
|
160
|
+
})
|
|
161
|
+
};
|
|
162
|
+
break;
|
|
163
|
+
case "anonymous":
|
|
164
|
+
if (authVerdict.rejected) this.log("MCP — jeton REJETÉ (expiré, mauvaise audience ou signature invalide) : la requête est servie en ANONYME, les outils réservés restent retenus. Renouveler le jeton (`nodefony security:token`) pour les retrouver.", "WARNING");
|
|
165
|
+
caller = {
|
|
166
|
+
authenticated: false,
|
|
167
|
+
scopes: [],
|
|
168
|
+
roles: mcpCallerRoles({
|
|
169
|
+
protected: true,
|
|
170
|
+
authenticated: false,
|
|
171
|
+
scopes: []
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const kernel = Nodefony.getKernel();
|
|
177
|
+
let withheldCount = 0;
|
|
178
|
+
const tools = collectMcpTools({
|
|
179
|
+
builtins: settings.tools,
|
|
180
|
+
modules: kernel?.modules,
|
|
181
|
+
caller,
|
|
182
|
+
onSkip: (why) => this.log(`MCP — ${why}`, "WARNING"),
|
|
183
|
+
onWithheld: (name, why) => {
|
|
184
|
+
withheldCount += 1;
|
|
185
|
+
this.log(`MCP — outil « ${name} » retenu : ${why}`, "DEBUG");
|
|
186
|
+
},
|
|
187
|
+
deps: this.#service().mcpToolDeps()
|
|
188
|
+
});
|
|
189
|
+
const reply = await handleMcpMessage(body, {
|
|
190
|
+
tools,
|
|
191
|
+
caller,
|
|
192
|
+
withheldCount,
|
|
193
|
+
serverInfo: {
|
|
194
|
+
name: kernel?.projectName ?? "nodefony",
|
|
195
|
+
version: Nodefony.version
|
|
196
|
+
}
|
|
197
|
+
}, { protocolVersion });
|
|
198
|
+
if (reply.body === null) return this.renderResponse("", void 0, reply.status);
|
|
199
|
+
return this.renderJson(reply.body, reply.status);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
__decorate([
|
|
203
|
+
route("devkit-mcp", {
|
|
204
|
+
path: "/mcp",
|
|
205
|
+
method: "POST"
|
|
206
|
+
}),
|
|
207
|
+
__decorateParam(0, Body()),
|
|
208
|
+
__decorateParam(1, Headers("origin")),
|
|
209
|
+
__decorateParam(2, Headers("mcp-protocol-version")),
|
|
210
|
+
__decorateParam(3, Headers("authorization")),
|
|
211
|
+
__decorateMetadata("design:type", Function),
|
|
212
|
+
__decorateMetadata("design:paramtypes", [
|
|
213
|
+
Object,
|
|
214
|
+
String,
|
|
215
|
+
String,
|
|
216
|
+
String
|
|
217
|
+
]),
|
|
218
|
+
__decorateMetadata("design:returntype", Promise)
|
|
219
|
+
], McpController.prototype, "mcp", null);
|
|
220
|
+
McpController = __decorate([controller("/nodefony"), __decorateMetadata("design:paramtypes", [Object])], McpController);
|
|
221
|
+
var McpController_default = McpController;
|
|
222
|
+
//#endregion
|
|
223
|
+
export { McpController_default as default };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
|
|
3
|
+
import { MCP_ENDPOINT_PATH, buildProtectedResourceMetadata, protectedResourceMetadataPath } from "nodefony";
|
|
4
|
+
import { Controller, controller, route } from "@nodefony/framework";
|
|
5
|
+
//#region nodefony/controllers/OAuthMetadataController.ts
|
|
6
|
+
/**
|
|
7
|
+
* Chemin bien connu où se publient les métadonnées de la porte MCP.
|
|
8
|
+
*
|
|
9
|
+
* **Dérivé, jamais recopié** : il se compose du chemin de l'endpoint par la
|
|
10
|
+
* règle d'insertion de la RFC 9728 §3.1. Le littéral qu'on serait tenté
|
|
11
|
+
* d'écrire ici deviendrait faux le jour où la porte déménage — et l'erreur
|
|
12
|
+
* serait parfaitement silencieuse : un client sonde ce chemin, reçoit `404`,
|
|
13
|
+
* et conclut que l'application n'a pas d'autorisation.
|
|
14
|
+
*/
|
|
15
|
+
const METADATA_PATH = protectedResourceMetadataPath(MCP_ENDPOINT_PATH);
|
|
16
|
+
/**
|
|
17
|
+
* Métadonnées OAuth 2.1 de la porte MCP — **le seul moyen normalisé** pour
|
|
18
|
+
* qu'un agent apprenne où obtenir un jeton.
|
|
19
|
+
*
|
|
20
|
+
* ## Pourquoi un controller séparé
|
|
21
|
+
*
|
|
22
|
+
* Trois raisons, et aucune n'est esthétique. Le chemin vit **hors** du préfixe
|
|
23
|
+
* `/nodefony` (la RFC impose `/.well-known/…` juste après l'hôte). Le document
|
|
24
|
+
* est **public par conception** — un client doit pouvoir le lire avant d'avoir
|
|
25
|
+
* le moindre jeton, donc il ne porte ni garde d'origine ni exigence de
|
|
26
|
+
* localité, contrairement à la porte elle-même. Et il répond en `GET`, quand la
|
|
27
|
+
* porte n'accepte que `POST`.
|
|
28
|
+
*
|
|
29
|
+
* ## Ce que publier coûte, et ce que ne rien publier coûte
|
|
30
|
+
*
|
|
31
|
+
* Le document ne révèle rien de sensible : l'émetteur des jetons, les scopes
|
|
32
|
+
* compris, un nom. En revanche, ne PAS le publier a un coût mesuré — un client
|
|
33
|
+
* qui ne le trouve pas suppose que le serveur d'autorisation est le serveur
|
|
34
|
+
* lui-même, et part sonder des chemins qui n'existent pas. C'est exactement ce
|
|
35
|
+
* qui a produit le bruit OAuth observé sur cette porte.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 **Rôle éteint = `404`, jamais un document vide.** Un document sans serveur
|
|
38
|
+
* d'autorisation apprendrait au client qu'un jeton est nécessaire sans jamais
|
|
39
|
+
* lui dire où le demander : la spécification MCP l'interdit d'ailleurs
|
|
40
|
+
* explicitement (« MUST include […] at least one authorization server »).
|
|
41
|
+
*/
|
|
42
|
+
let OAuthMetadataController = class OAuthMetadataController extends Controller {
|
|
43
|
+
constructor(context) {
|
|
44
|
+
super("devkit-oauth-metadata", context);
|
|
45
|
+
}
|
|
46
|
+
/** Résout le service du module depuis le conteneur partagé. */
|
|
47
|
+
#service() {
|
|
48
|
+
const svc = this.get("devkit");
|
|
49
|
+
if (!svc) throw new Error("DevkitService non enregistré");
|
|
50
|
+
return svc;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `GET /.well-known/oauth-protected-resource/nodefony/mcp` — RFC 9728 §3.
|
|
54
|
+
*
|
|
55
|
+
* @returns le document JSON, ou `404` si cette porte n'est pas protégée
|
|
56
|
+
*/
|
|
57
|
+
async metadata() {
|
|
58
|
+
const settings = this.#service().mcpSettings();
|
|
59
|
+
const authz = settings.authorization;
|
|
60
|
+
if (!settings.enabled || authz.authorizationServers.length === 0) return this.renderJson({ error: "not_found" }, 404);
|
|
61
|
+
let document;
|
|
62
|
+
try {
|
|
63
|
+
document = buildProtectedResourceMetadata({
|
|
64
|
+
resource: authz.resource,
|
|
65
|
+
authorizationServers: authz.authorizationServers,
|
|
66
|
+
scopesSupported: authz.scopesSupported,
|
|
67
|
+
resourceName: authz.resourceName,
|
|
68
|
+
resourceDocumentation: authz.resourceDocumentation
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.log(`MCP — métadonnées impubliables : ${error.message}`, "CRITIC");
|
|
72
|
+
return this.renderJson({ error: "server_error" }, 500);
|
|
73
|
+
}
|
|
74
|
+
return this.renderJson(document, 200, { "Cache-Control": "public, max-age=3600" });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
__decorate([
|
|
78
|
+
route("devkit-mcp-protected-resource", {
|
|
79
|
+
path: METADATA_PATH,
|
|
80
|
+
method: "GET"
|
|
81
|
+
}),
|
|
82
|
+
__decorateMetadata("design:type", Function),
|
|
83
|
+
__decorateMetadata("design:paramtypes", []),
|
|
84
|
+
__decorateMetadata("design:returntype", Promise)
|
|
85
|
+
], OAuthMetadataController.prototype, "metadata", null);
|
|
86
|
+
OAuthMetadataController = __decorate([controller(""), __decorateMetadata("design:paramtypes", [Object])], OAuthMetadataController);
|
|
87
|
+
var OAuthMetadataController_default = OAuthMetadataController;
|
|
88
|
+
//#endregion
|
|
89
|
+
export { OAuthMetadataController_default as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|