@creezio/mcp-facade 0.22.0

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 (174) hide show
  1. package/README.md +420 -0
  2. package/dist/.creezio-src-hash.json +4 -0
  3. package/dist/admin/adapters.d.ts +10 -0
  4. package/dist/admin/adapters.d.ts.map +1 -0
  5. package/dist/admin/adapters.js +21 -0
  6. package/dist/admin/adapters.js.map +1 -0
  7. package/dist/admin/http-routes.d.ts +11 -0
  8. package/dist/admin/http-routes.d.ts.map +1 -0
  9. package/dist/admin/http-routes.js +66 -0
  10. package/dist/admin/http-routes.js.map +1 -0
  11. package/dist/admin/index.d.ts +12 -0
  12. package/dist/admin/index.d.ts.map +1 -0
  13. package/dist/admin/index.js +8 -0
  14. package/dist/admin/index.js.map +1 -0
  15. package/dist/admin/mcp-admin.d.ts +165 -0
  16. package/dist/admin/mcp-admin.d.ts.map +1 -0
  17. package/dist/admin/mcp-admin.js +419 -0
  18. package/dist/admin/mcp-admin.js.map +1 -0
  19. package/dist/admin/tool-policy-guard.d.ts +106 -0
  20. package/dist/admin/tool-policy-guard.d.ts.map +1 -0
  21. package/dist/admin/tool-policy-guard.js +187 -0
  22. package/dist/admin/tool-policy-guard.js.map +1 -0
  23. package/dist/admin/types.d.ts +62 -0
  24. package/dist/admin/types.d.ts.map +1 -0
  25. package/dist/admin/types.js +6 -0
  26. package/dist/admin/types.js.map +1 -0
  27. package/dist/brand-facade.d.ts +41 -0
  28. package/dist/brand-facade.d.ts.map +1 -0
  29. package/dist/brand-facade.js +67 -0
  30. package/dist/brand-facade.js.map +1 -0
  31. package/dist/core-tools.d.ts +22 -0
  32. package/dist/core-tools.d.ts.map +1 -0
  33. package/dist/core-tools.js +93 -0
  34. package/dist/core-tools.js.map +1 -0
  35. package/dist/dyn-import.d.ts +13 -0
  36. package/dist/dyn-import.d.ts.map +1 -0
  37. package/dist/dyn-import.js +2 -0
  38. package/dist/dyn-import.js.map +1 -0
  39. package/dist/facade.d.ts +40 -0
  40. package/dist/facade.d.ts.map +1 -0
  41. package/dist/facade.js +334 -0
  42. package/dist/facade.js.map +1 -0
  43. package/dist/hono-bind.d.ts +48 -0
  44. package/dist/hono-bind.d.ts.map +1 -0
  45. package/dist/hono-bind.js +70 -0
  46. package/dist/hono-bind.js.map +1 -0
  47. package/dist/hono-proxy.d.ts +63 -0
  48. package/dist/hono-proxy.d.ts.map +1 -0
  49. package/dist/hono-proxy.js +185 -0
  50. package/dist/hono-proxy.js.map +1 -0
  51. package/dist/index.d.ts +32 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +23 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/jwt.d.ts +20 -0
  56. package/dist/jwt.d.ts.map +1 -0
  57. package/dist/jwt.js +84 -0
  58. package/dist/jwt.js.map +1 -0
  59. package/dist/module-ops-tools.d.ts +43 -0
  60. package/dist/module-ops-tools.d.ts.map +1 -0
  61. package/dist/module-ops-tools.js +301 -0
  62. package/dist/module-ops-tools.js.map +1 -0
  63. package/dist/namespace.d.ts +23 -0
  64. package/dist/namespace.d.ts.map +1 -0
  65. package/dist/namespace.js +51 -0
  66. package/dist/namespace.js.map +1 -0
  67. package/dist/oauth/cors-policy.d.ts +3 -0
  68. package/dist/oauth/cors-policy.d.ts.map +1 -0
  69. package/dist/oauth/cors-policy.js +25 -0
  70. package/dist/oauth/cors-policy.js.map +1 -0
  71. package/dist/oauth/hono-app.d.ts +18 -0
  72. package/dist/oauth/hono-app.d.ts.map +1 -0
  73. package/dist/oauth/hono-app.js +175 -0
  74. package/dist/oauth/hono-app.js.map +1 -0
  75. package/dist/oauth/index.d.ts +9 -0
  76. package/dist/oauth/index.d.ts.map +1 -0
  77. package/dist/oauth/index.js +6 -0
  78. package/dist/oauth/index.js.map +1 -0
  79. package/dist/oauth/rate-limit.d.ts +11 -0
  80. package/dist/oauth/rate-limit.d.ts.map +1 -0
  81. package/dist/oauth/rate-limit.js +32 -0
  82. package/dist/oauth/rate-limit.js.map +1 -0
  83. package/dist/oauth/routes.d.ts +8 -0
  84. package/dist/oauth/routes.d.ts.map +1 -0
  85. package/dist/oauth/routes.js +671 -0
  86. package/dist/oauth/routes.js.map +1 -0
  87. package/dist/oauth/store.d.ts +113 -0
  88. package/dist/oauth/store.d.ts.map +1 -0
  89. package/dist/oauth/store.js +390 -0
  90. package/dist/oauth/store.js.map +1 -0
  91. package/dist/oauth/types.d.ts +97 -0
  92. package/dist/oauth/types.d.ts.map +1 -0
  93. package/dist/oauth/types.js +5 -0
  94. package/dist/oauth/types.js.map +1 -0
  95. package/dist/open-external-tab-host-tools.d.ts +67 -0
  96. package/dist/open-external-tab-host-tools.d.ts.map +1 -0
  97. package/dist/open-external-tab-host-tools.js +92 -0
  98. package/dist/open-external-tab-host-tools.js.map +1 -0
  99. package/dist/policy.d.ts +59 -0
  100. package/dist/policy.d.ts.map +1 -0
  101. package/dist/policy.js +88 -0
  102. package/dist/policy.js.map +1 -0
  103. package/dist/runtime.d.ts +22 -0
  104. package/dist/runtime.d.ts.map +1 -0
  105. package/dist/runtime.js +20 -0
  106. package/dist/runtime.js.map +1 -0
  107. package/dist/types.d.ts +163 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +2 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist-cjs/admin/adapters.js +27 -0
  112. package/dist-cjs/admin/adapters.js.map +1 -0
  113. package/dist-cjs/admin/http-routes.js +69 -0
  114. package/dist-cjs/admin/http-routes.js.map +1 -0
  115. package/dist-cjs/admin/index.js +40 -0
  116. package/dist-cjs/admin/index.js.map +1 -0
  117. package/dist-cjs/admin/mcp-admin.js +441 -0
  118. package/dist-cjs/admin/mcp-admin.js.map +1 -0
  119. package/dist-cjs/admin/tool-policy-guard.js +193 -0
  120. package/dist-cjs/admin/tool-policy-guard.js.map +1 -0
  121. package/dist-cjs/admin/types.js +7 -0
  122. package/dist-cjs/admin/types.js.map +1 -0
  123. package/dist-cjs/brand-facade.js +70 -0
  124. package/dist-cjs/brand-facade.js.map +1 -0
  125. package/dist-cjs/core-tools.js +97 -0
  126. package/dist-cjs/core-tools.js.map +1 -0
  127. package/dist-cjs/dyn-import.js +5 -0
  128. package/dist-cjs/dyn-import.js.map +1 -0
  129. package/dist-cjs/facade.js +337 -0
  130. package/dist-cjs/facade.js.map +1 -0
  131. package/dist-cjs/hono-bind.js +74 -0
  132. package/dist-cjs/hono-bind.js.map +1 -0
  133. package/dist-cjs/hono-proxy.js +189 -0
  134. package/dist-cjs/hono-proxy.js.map +1 -0
  135. package/dist-cjs/index.js +120 -0
  136. package/dist-cjs/index.js.map +1 -0
  137. package/dist-cjs/jwt.js +91 -0
  138. package/dist-cjs/jwt.js.map +1 -0
  139. package/dist-cjs/module-ops-tools.js +308 -0
  140. package/dist-cjs/module-ops-tools.js.map +1 -0
  141. package/dist-cjs/namespace.js +56 -0
  142. package/dist-cjs/namespace.js.map +1 -0
  143. package/dist-cjs/oauth/cors-policy.js +29 -0
  144. package/dist-cjs/oauth/cors-policy.js.map +1 -0
  145. package/dist-cjs/oauth/hono-app.js +178 -0
  146. package/dist-cjs/oauth/hono-app.js.map +1 -0
  147. package/dist-cjs/oauth/index.js +48 -0
  148. package/dist-cjs/oauth/index.js.map +1 -0
  149. package/dist-cjs/oauth/rate-limit.js +37 -0
  150. package/dist-cjs/oauth/rate-limit.js.map +1 -0
  151. package/dist-cjs/oauth/routes.js +674 -0
  152. package/dist-cjs/oauth/routes.js.map +1 -0
  153. package/dist-cjs/oauth/store.js +418 -0
  154. package/dist-cjs/oauth/store.js.map +1 -0
  155. package/dist-cjs/oauth/types.js +6 -0
  156. package/dist-cjs/oauth/types.js.map +1 -0
  157. package/dist-cjs/open-external-tab-host-tools.js +96 -0
  158. package/dist-cjs/open-external-tab-host-tools.js.map +1 -0
  159. package/dist-cjs/package.json +3 -0
  160. package/dist-cjs/policy.js +93 -0
  161. package/dist-cjs/policy.js.map +1 -0
  162. package/dist-cjs/runtime.js +24 -0
  163. package/dist-cjs/runtime.js.map +1 -0
  164. package/dist-cjs/types.js +3 -0
  165. package/dist-cjs/types.js.map +1 -0
  166. package/package.json +92 -0
  167. package/ui/index.ts +5 -0
  168. package/ui/mcp-admin-client.tsx +270 -0
  169. package/ui/primitives/badge.tsx +37 -0
  170. package/ui/primitives/button.tsx +44 -0
  171. package/ui/primitives/card.tsx +44 -0
  172. package/ui/primitives/cn.ts +6 -0
  173. package/ui/primitives/input.tsx +19 -0
  174. package/ui/primitives/tabs.tsx +51 -0
@@ -0,0 +1,674 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMcpOAuthRoutes = createMcpOAuthRoutes;
4
+ /**
5
+ * Endpoints OAuth 2.1 du serveur MCP — SoT kit (port TempoFlow gold).
6
+ * Branding / session / cookie injectables via McpOAuthRoutesConfig.
7
+ */
8
+ const hono_1 = require("hono");
9
+ const store_js_1 = require("./store.js");
10
+ const rate_limit_js_1 = require("./rate-limit.js");
11
+ const DEFAULT_CONSENT_SCOPES = `
12
+ <li>consulter et modifier les données métier exposées via MCP</li>
13
+ <li>appeler les tools autorisés pour votre compte</li>
14
+ <li>agir au nom de la session authentifiée</li>`;
15
+ function createMcpOAuthRoutes(config) {
16
+ const oauthRoutes = new hono_1.Hono();
17
+ const productName = config.productName;
18
+ const resourceName = config.resourceName || `${productName} MCP`;
19
+ const consentScopesHtml = config.consentScopesHtml?.trim() || DEFAULT_CONSENT_SCOPES;
20
+ const documentationPath = config.documentationPath || "/developers";
21
+ const dcrLimit = config.dcrRateLimit?.limit ?? 20;
22
+ const dcrWindow = config.dcrRateLimit?.windowMs ?? 60 * 60_000;
23
+ function tunnelRequiredJson(c) {
24
+ return c.json({
25
+ error: "temporarily_unavailable",
26
+ error_description: "tunnel requis — configurez l'accès mobile (APP_PUBLIC_URL / MCP_PUBLIC_URL)",
27
+ }, 503);
28
+ }
29
+ function authorizationServerMetadata() {
30
+ const base = (0, store_js_1.mcpBaseUrl)();
31
+ return {
32
+ issuer: base,
33
+ authorization_endpoint: `${base}/oauth/authorize`,
34
+ token_endpoint: `${base}/oauth/token`,
35
+ registration_endpoint: `${base}/oauth/register`,
36
+ scopes_supported: [store_js_1.MCP_SCOPE],
37
+ response_types_supported: ["code"],
38
+ response_modes_supported: ["query"],
39
+ grant_types_supported: ["authorization_code", "refresh_token"],
40
+ token_endpoint_auth_methods_supported: [
41
+ "client_secret_post",
42
+ "client_secret_basic",
43
+ "none",
44
+ ],
45
+ code_challenge_methods_supported: ["S256"],
46
+ service_documentation: `${base}${documentationPath}`,
47
+ };
48
+ }
49
+ function protectedResourceMetadata() {
50
+ const base = (0, store_js_1.mcpBaseUrl)();
51
+ return {
52
+ resource: (0, store_js_1.mcpResourceUrl)(),
53
+ authorization_servers: [base],
54
+ scopes_supported: [store_js_1.MCP_SCOPE],
55
+ bearer_methods_supported: ["header"],
56
+ resource_name: resourceName,
57
+ resource_documentation: `${base}${documentationPath}`,
58
+ };
59
+ }
60
+ for (const p of [
61
+ "/.well-known/oauth-authorization-server",
62
+ "/.well-known/oauth-authorization-server/mcp",
63
+ "/oauth/well-known/authorization-server",
64
+ ]) {
65
+ oauthRoutes.get(p, (c) => {
66
+ try {
67
+ return c.json(authorizationServerMetadata());
68
+ }
69
+ catch (e) {
70
+ if ((0, store_js_1.isMcpPublicUrlRequiredError)(e))
71
+ return tunnelRequiredJson(c);
72
+ throw e;
73
+ }
74
+ });
75
+ }
76
+ for (const p of [
77
+ "/.well-known/oauth-protected-resource",
78
+ "/.well-known/oauth-protected-resource/mcp",
79
+ "/oauth/well-known/protected-resource",
80
+ ]) {
81
+ oauthRoutes.get(p, (c) => {
82
+ try {
83
+ return c.json(protectedResourceMetadata());
84
+ }
85
+ catch (e) {
86
+ if ((0, store_js_1.isMcpPublicUrlRequiredError)(e))
87
+ return tunnelRequiredJson(c);
88
+ throw e;
89
+ }
90
+ });
91
+ }
92
+ function isValidRedirectUri(uri) {
93
+ if (typeof uri !== "string")
94
+ return false;
95
+ try {
96
+ const u = new URL(uri);
97
+ return (u.protocol === "https:" ||
98
+ (u.protocol === "http:" &&
99
+ (u.hostname === "localhost" || u.hostname === "127.0.0.1")));
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ oauthRoutes.post("/oauth/register", async (c) => {
106
+ const clientIp = c.req.header("cf-connecting-ip") ||
107
+ c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
108
+ "unknown";
109
+ const rate = (0, rate_limit_js_1.checkMcpRateLimit)(`mcp:dcr:${clientIp}`, dcrLimit, dcrWindow);
110
+ for (const [name, value] of Object.entries((0, rate_limit_js_1.rateLimitHeaders)(rate))) {
111
+ c.header(name, value);
112
+ }
113
+ if (!rate.ok) {
114
+ return c.json({
115
+ error: "rate_limit_exceeded",
116
+ error_description: "Trop d'enregistrements clients",
117
+ }, 429);
118
+ }
119
+ if (!(0, store_js_1.mcpOauthReady)()) {
120
+ return c.json({ error: "temporarily_unavailable" }, 503);
121
+ }
122
+ let body;
123
+ try {
124
+ body = await c.req.json();
125
+ }
126
+ catch {
127
+ return c.json({ error: "invalid_client_metadata", error_description: "JSON attendu" }, 400);
128
+ }
129
+ const redirectUris = Array.isArray(body.redirect_uris)
130
+ ? body.redirect_uris
131
+ : [];
132
+ if (!redirectUris.length || !redirectUris.every(isValidRedirectUri)) {
133
+ return c.json({
134
+ error: "invalid_redirect_uri",
135
+ error_description: "redirect_uris requis (https, ou http://localhost)",
136
+ }, 400);
137
+ }
138
+ const requestedScope = typeof body.scope === "string"
139
+ ? (0, store_js_1.normalizeMcpScopes)(body.scope)
140
+ : store_js_1.MCP_SCOPE;
141
+ if (!requestedScope) {
142
+ return c.json({
143
+ error: "invalid_client_metadata",
144
+ error_description: `Scopes permis : ${store_js_1.MCP_SCOPES.join(", ")}`,
145
+ }, 400);
146
+ }
147
+ const { client, client_secret } = (0, store_js_1.registerClient)({
148
+ client_name: typeof body.client_name === "string" ? body.client_name : undefined,
149
+ redirect_uris: redirectUris,
150
+ token_endpoint_auth_method: typeof body.token_endpoint_auth_method === "string"
151
+ ? body.token_endpoint_auth_method
152
+ : undefined,
153
+ scope: requestedScope,
154
+ });
155
+ (0, store_js_1.pruneExpiredCodes)();
156
+ return c.json({
157
+ client_id: client.client_id,
158
+ ...(client_secret ? { client_secret } : {}),
159
+ client_id_issued_at: Math.floor(Date.now() / 1000),
160
+ ...(client_secret ? { client_secret_expires_at: 0 } : {}),
161
+ client_name: client.client_name ?? undefined,
162
+ redirect_uris: client.redirect_uris,
163
+ token_endpoint_auth_method: client.token_endpoint_auth_method,
164
+ grant_types: ["authorization_code", "refresh_token"],
165
+ response_types: ["code"],
166
+ scope: client.scope ?? store_js_1.MCP_SCOPE,
167
+ }, 201);
168
+ });
169
+ function readAuthorizeParams(src) {
170
+ return {
171
+ client_id: src.client_id || "",
172
+ redirect_uri: src.redirect_uri || "",
173
+ response_type: src.response_type || "",
174
+ state: src.state || "",
175
+ scope: src.scope || store_js_1.MCP_SCOPE,
176
+ code_challenge: src.code_challenge || "",
177
+ code_challenge_method: src.code_challenge_method || "",
178
+ resource: src.resource || "",
179
+ };
180
+ }
181
+ function escapeHtml(s) {
182
+ return s
183
+ .replace(/&/g, "&amp;")
184
+ .replace(/</g, "&lt;")
185
+ .replace(/>/g, "&gt;")
186
+ .replace(/"/g, "&quot;")
187
+ .replace(/'/g, "&#39;");
188
+ }
189
+ function redirectWithError(redirectUri, state, error, description) {
190
+ const u = new URL(redirectUri);
191
+ u.searchParams.set("error", error);
192
+ if (description)
193
+ u.searchParams.set("error_description", description);
194
+ if (state)
195
+ u.searchParams.set("state", state);
196
+ return new Response(null, {
197
+ status: 302,
198
+ headers: { Location: u.toString() },
199
+ });
200
+ }
201
+ function oauthLog(event, fields) {
202
+ const safe = { event };
203
+ for (const [k, v] of Object.entries(fields)) {
204
+ if (v === undefined)
205
+ continue;
206
+ safe[k] = v;
207
+ }
208
+ console.info("[mcp-oauth]", JSON.stringify(safe));
209
+ }
210
+ function redirectHost(redirectUri) {
211
+ try {
212
+ return new URL(redirectUri).host;
213
+ }
214
+ catch {
215
+ return "invalid";
216
+ }
217
+ }
218
+ function consentPage(p, opts) {
219
+ const { clientName, sessionEmail, error } = opts;
220
+ const signedIn = Boolean(sessionEmail);
221
+ const hidden = [
222
+ "client_id",
223
+ "redirect_uri",
224
+ "response_type",
225
+ "state",
226
+ "scope",
227
+ "code_challenge",
228
+ "code_challenge_method",
229
+ "resource",
230
+ ]
231
+ .map((k) => `<input type="hidden" name="${k}" value="${escapeHtml(p[k])}">`)
232
+ .join("\n ");
233
+ let formAction = "/oauth/authorize";
234
+ try {
235
+ formAction = `${(0, store_js_1.mcpBaseUrl)()}/oauth/authorize`;
236
+ }
237
+ catch {
238
+ /* fallback relatif */
239
+ }
240
+ const credentialsBlock = signedIn
241
+ ? `<div class="session" data-auth="session">
242
+ Connecté en tant que <strong>${escapeHtml(sessionEmail || "")}</strong>
243
+ </div>`
244
+ : `<label for="username">Identifiant CRM</label>
245
+ <input id="username" name="email" type="text" autocomplete="username" required>
246
+ <label for="password">Mot de passe</label>
247
+ <input id="password" name="password" type="password" autocomplete="current-password" required>`;
248
+ return `<!doctype html>
249
+ <html lang="fr">
250
+ <head>
251
+ <meta charset="utf-8">
252
+ <meta name="viewport" content="width=device-width, initial-scale=1">
253
+ <title>Autoriser ${escapeHtml(clientName)} — ${escapeHtml(productName)}</title>
254
+ <style>
255
+ :root { color-scheme: light; }
256
+ * { box-sizing: border-box; }
257
+ body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
258
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
259
+ background: #f1f5f9; color: #0f172a; }
260
+ .card { width: 100%; max-width: 420px; background: #fff; border: 1px solid #e2e8f0;
261
+ border-radius: 16px; padding: 32px; margin: 16px; box-shadow: 0 10px 30px rgba(15,23,42,.06); }
262
+ h1 { font-size: 20px; margin: 0 0 4px; }
263
+ .sub { color: #64748b; font-size: 14px; margin: 0 0 20px; }
264
+ .client { display: inline-block; background: #e0f2fe; color: #0369a1; border-radius: 8px;
265
+ padding: 2px 8px; font-weight: 600; }
266
+ label { display: block; font-size: 13px; font-weight: 600; margin: 14px 0 4px; }
267
+ input[type=text], input[type=password] { width: 100%; padding: 10px 12px; border: 1px solid #cbd5e1;
268
+ border-radius: 10px; font-size: 15px; }
269
+ .session { margin: 14px 0 0; padding: 12px 14px; background: #ecfdf5; border: 1px solid #a7f3d0;
270
+ border-radius: 10px; font-size: 14px; color: #065f46; }
271
+ .scopes { margin: 18px 0; padding: 12px 14px; background: #f8fafc; border: 1px solid #e2e8f0;
272
+ border-radius: 10px; font-size: 13px; color: #334155; }
273
+ .scopes li { margin: 2px 0; }
274
+ .error { background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; border-radius: 10px;
275
+ padding: 10px 12px; font-size: 13px; margin-bottom: 8px; }
276
+ .actions { display: flex; gap: 10px; margin-top: 20px; }
277
+ button { flex: 1; padding: 11px 0; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; }
278
+ .approve { background: #0284c7; color: #fff; border: none; }
279
+ .deny { background: #fff; color: #475569; border: 1px solid #cbd5e1; }
280
+ </style>
281
+ </head>
282
+ <body>
283
+ <main class="card">
284
+ <h1>${escapeHtml(productName)}</h1>
285
+ <p class="sub"><span class="client">${escapeHtml(clientName)}</span> demande l'accès à votre CRM via MCP.</p>
286
+ ${error ? `<div class="error">${escapeHtml(error)}</div>` : ""}
287
+ <div class="scopes">
288
+ Cet accès permettra à l'application de :
289
+ <ul>
290
+ ${consentScopesHtml}
291
+ </ul>
292
+ </div>
293
+ <form method="post" action="${escapeHtml(formAction)}">
294
+ ${hidden}
295
+ ${credentialsBlock}
296
+ <div class="actions">
297
+ <button class="deny" type="submit" name="decision" value="deny" formnovalidate>Refuser</button>
298
+ <button class="approve" type="submit" name="decision" value="approve">Autoriser</button>
299
+ </div>
300
+ </form>
301
+ </main>
302
+ </body>
303
+ </html>`;
304
+ }
305
+ function validateAuthorizeBase(p) {
306
+ const client = (0, store_js_1.getClient)(p.client_id);
307
+ if (!client)
308
+ return { kind: "bad_client", message: "client_id inconnu" };
309
+ if (!client.redirect_uris.includes(p.redirect_uri)) {
310
+ return {
311
+ kind: "bad_client",
312
+ message: "redirect_uri non enregistrée pour ce client",
313
+ };
314
+ }
315
+ const requestedScope = (0, store_js_1.normalizeMcpScopes)(p.scope);
316
+ const clientScopes = new Set(String(client.scope || store_js_1.MCP_SCOPE).split(/\s+/));
317
+ if (!requestedScope ||
318
+ !requestedScope
319
+ .split(/\s+/)
320
+ .every((scope) => clientScopes.has(scope) || clientScopes.has("crm"))) {
321
+ return {
322
+ kind: "redirect_error",
323
+ error: "invalid_scope",
324
+ description: `Scopes permis : ${client.scope || store_js_1.MCP_SCOPE}`,
325
+ };
326
+ }
327
+ if (p.response_type !== "code") {
328
+ return {
329
+ kind: "redirect_error",
330
+ error: "unsupported_response_type",
331
+ description: "response_type=code requis",
332
+ };
333
+ }
334
+ if (!p.code_challenge || p.code_challenge_method !== "S256") {
335
+ return {
336
+ kind: "redirect_error",
337
+ error: "invalid_request",
338
+ description: "PKCE S256 obligatoire (code_challenge + code_challenge_method=S256)",
339
+ };
340
+ }
341
+ if (!(0, store_js_1.resourceAcceptable)(p.resource)) {
342
+ return {
343
+ kind: "redirect_error",
344
+ error: "invalid_target",
345
+ description: `resource doit être ${(0, store_js_1.mcpResourceUrl)()}`,
346
+ };
347
+ }
348
+ return { kind: "ok", clientName: client.client_name || p.client_id };
349
+ }
350
+ oauthRoutes.get("/oauth/authorize", async (c) => {
351
+ if (!(0, store_js_1.mcpOauthReady)())
352
+ return c.text("Migration v22 non appliquée", 503);
353
+ try {
354
+ (0, store_js_1.mcpBaseUrl)();
355
+ }
356
+ catch (e) {
357
+ if ((0, store_js_1.isMcpPublicUrlRequiredError)(e)) {
358
+ return c.text("Tunnel requis — configurez l'accès mobile avant de connecter un client MCP.", 503);
359
+ }
360
+ throw e;
361
+ }
362
+ const p = readAuthorizeParams({
363
+ client_id: c.req.query("client_id"),
364
+ redirect_uri: c.req.query("redirect_uri"),
365
+ response_type: c.req.query("response_type"),
366
+ state: c.req.query("state"),
367
+ scope: c.req.query("scope"),
368
+ code_challenge: c.req.query("code_challenge"),
369
+ code_challenge_method: c.req.query("code_challenge_method"),
370
+ resource: c.req.query("resource"),
371
+ });
372
+ const v = validateAuthorizeBase(p);
373
+ if (v.kind === "bad_client") {
374
+ oauthLog("authorize_get_reject", {
375
+ reason: "bad_client",
376
+ message: v.message,
377
+ client_id_prefix: p.client_id.slice(0, 12),
378
+ redirect_host: redirectHost(p.redirect_uri),
379
+ });
380
+ return c.text(`Requête invalide : ${v.message}`, 400);
381
+ }
382
+ if (v.kind === "redirect_error") {
383
+ oauthLog("authorize_get_reject", {
384
+ reason: v.error,
385
+ description: v.description,
386
+ client_id_prefix: p.client_id.slice(0, 12),
387
+ redirect_host: redirectHost(p.redirect_uri),
388
+ });
389
+ return redirectWithError(p.redirect_uri, p.state, v.error, v.description);
390
+ }
391
+ const session = await config.session.getSessionFromContext(c);
392
+ const sessionEmail = session?.email ? String(session.email) : null;
393
+ oauthLog("authorize_get", {
394
+ client_id_prefix: p.client_id.slice(0, 12),
395
+ redirect_host: redirectHost(p.redirect_uri),
396
+ has_session: Boolean(sessionEmail),
397
+ has_resource: Boolean(p.resource),
398
+ pkce: p.code_challenge_method === "S256",
399
+ });
400
+ c.header("Referrer-Policy", "same-origin");
401
+ c.header("Cache-Control", "no-store");
402
+ return c.html(consentPage(p, { clientName: v.clientName, sessionEmail }));
403
+ });
404
+ oauthRoutes.post("/oauth/authorize", async (c) => {
405
+ if (!(0, store_js_1.mcpOauthReady)())
406
+ return c.text("Migration v22 non appliquée", 503);
407
+ try {
408
+ (0, store_js_1.mcpBaseUrl)();
409
+ }
410
+ catch (e) {
411
+ if ((0, store_js_1.isMcpPublicUrlRequiredError)(e)) {
412
+ return c.text("Tunnel requis — configurez l'accès mobile avant de connecter un client MCP.", 503);
413
+ }
414
+ throw e;
415
+ }
416
+ const form = await c.req.parseBody();
417
+ const asStr = (v) => (typeof v === "string" ? v : "");
418
+ const p = readAuthorizeParams({
419
+ client_id: asStr(form.client_id),
420
+ redirect_uri: asStr(form.redirect_uri),
421
+ response_type: asStr(form.response_type),
422
+ state: asStr(form.state),
423
+ scope: asStr(form.scope),
424
+ code_challenge: asStr(form.code_challenge),
425
+ code_challenge_method: asStr(form.code_challenge_method),
426
+ resource: asStr(form.resource),
427
+ });
428
+ const v = validateAuthorizeBase(p);
429
+ if (v.kind === "bad_client") {
430
+ return c.text(`Requête invalide : ${v.message}`, 400);
431
+ }
432
+ if (v.kind === "redirect_error") {
433
+ return redirectWithError(p.redirect_uri, p.state, v.error, v.description);
434
+ }
435
+ if (asStr(form.decision) === "deny") {
436
+ oauthLog("authorize_deny", {
437
+ client_id_prefix: p.client_id.slice(0, 12),
438
+ redirect_host: redirectHost(p.redirect_uri),
439
+ });
440
+ return redirectWithError(p.redirect_uri, p.state, "access_denied", "Accès refusé");
441
+ }
442
+ const session = await config.session.getSessionFromContext(c);
443
+ const sessionEmail = session?.email ? String(session.email) : null;
444
+ let authVia = null;
445
+ let userEmail = null;
446
+ let userId = session?.sub ? String(session.sub) : null;
447
+ if (sessionEmail) {
448
+ authVia = "session";
449
+ userEmail = sessionEmail;
450
+ }
451
+ else {
452
+ const email = asStr(form.email).trim();
453
+ const password = asStr(form.password);
454
+ const user = await config.session.authenticateUser(email, password);
455
+ const okLegacy = !user && Boolean(config.session.validateCredentials?.(email, password));
456
+ if (!user && !okLegacy) {
457
+ oauthLog("authorize_approve_fail", {
458
+ reason: "invalid_credentials",
459
+ client_id_prefix: p.client_id.slice(0, 12),
460
+ redirect_host: redirectHost(p.redirect_uri),
461
+ });
462
+ c.header("Referrer-Policy", "same-origin");
463
+ c.header("Cache-Control", "no-store");
464
+ return c.html(consentPage(p, {
465
+ clientName: v.clientName,
466
+ sessionEmail: null,
467
+ error: "Identifiants invalides — réessayez.",
468
+ }), 200);
469
+ }
470
+ authVia = "credentials";
471
+ userEmail = user?.username || email;
472
+ userId = user?.id || config.session.getOwnerId() || null;
473
+ if (config.session.createSessionCookie) {
474
+ try {
475
+ await config.session.createSessionCookie(c, {
476
+ id: user?.id || userId,
477
+ username: userEmail,
478
+ });
479
+ }
480
+ catch (e) {
481
+ console.warn("[mcp-oauth] pose cookie session après login consent échouée", e);
482
+ }
483
+ }
484
+ }
485
+ if (!userId)
486
+ userId = config.session.getOwnerId() || null;
487
+ const code = (0, store_js_1.createAuthCode)({
488
+ client_id: p.client_id,
489
+ redirect_uri: p.redirect_uri,
490
+ scope: p.scope || store_js_1.MCP_SCOPE,
491
+ resource: p.resource || null,
492
+ code_challenge: p.code_challenge,
493
+ user_id: userId,
494
+ });
495
+ oauthLog("authorize_approve", {
496
+ client_id_prefix: p.client_id.slice(0, 12),
497
+ redirect_host: redirectHost(p.redirect_uri),
498
+ auth_via: authVia,
499
+ has_resource: Boolean(p.resource),
500
+ user_prefix: userEmail ? userEmail.slice(0, 3) : null,
501
+ has_user_id: Boolean(userId),
502
+ });
503
+ const u = new URL(p.redirect_uri);
504
+ u.searchParams.set("code", code);
505
+ if (p.state)
506
+ u.searchParams.set("state", p.state);
507
+ return c.redirect(u.toString(), 302);
508
+ });
509
+ function tokenError(c, error, description, status = 400) {
510
+ return c.json({ error, error_description: description }, status, {
511
+ "Cache-Control": "no-store",
512
+ Pragma: "no-cache",
513
+ });
514
+ }
515
+ function authenticateClient(clientId, clientSecretPost, basicHeader) {
516
+ let id = clientId;
517
+ let secret = clientSecretPost || null;
518
+ if (basicHeader?.toLowerCase().startsWith("basic ")) {
519
+ try {
520
+ const decoded = Buffer.from(basicHeader.slice(6), "base64").toString("utf8");
521
+ const sep = decoded.indexOf(":");
522
+ if (sep > 0) {
523
+ id = decodeURIComponent(decoded.slice(0, sep));
524
+ secret = decodeURIComponent(decoded.slice(sep + 1));
525
+ }
526
+ }
527
+ catch {
528
+ return { ok: false, description: "En-tête Basic invalide" };
529
+ }
530
+ }
531
+ const client = (0, store_js_1.getClient)(id);
532
+ if (!client)
533
+ return { ok: false, description: "client inconnu" };
534
+ if (!(0, store_js_1.verifyClientSecret)(client, secret)) {
535
+ return { ok: false, description: "client_secret invalide" };
536
+ }
537
+ return { ok: true, client_id: client.client_id };
538
+ }
539
+ oauthRoutes.post("/oauth/token", async (c) => {
540
+ if (!(0, store_js_1.mcpOauthReady)()) {
541
+ return tokenError(c, "temporarily_unavailable", "Migration v22 non appliquée");
542
+ }
543
+ try {
544
+ (0, store_js_1.mcpBaseUrl)();
545
+ }
546
+ catch (e) {
547
+ if ((0, store_js_1.isMcpPublicUrlRequiredError)(e)) {
548
+ return tokenError(c, "temporarily_unavailable", "tunnel requis — configurez l'accès mobile (APP_PUBLIC_URL / MCP_PUBLIC_URL)");
549
+ }
550
+ throw e;
551
+ }
552
+ let body = {};
553
+ const ct = c.req.header("content-type") || "";
554
+ try {
555
+ if (ct.includes("application/json")) {
556
+ body = await c.req.json();
557
+ }
558
+ else {
559
+ body = (await c.req.parseBody());
560
+ }
561
+ }
562
+ catch {
563
+ return tokenError(c, "invalid_request", "Corps form-urlencoded ou JSON attendu");
564
+ }
565
+ const asStr = (v) => (typeof v === "string" ? v : "");
566
+ const grantType = asStr(body.grant_type);
567
+ const auth = authenticateClient(asStr(body.client_id), asStr(body.client_secret), c.req.header("authorization"));
568
+ if (!auth.ok)
569
+ return tokenError(c, "invalid_client", auth.description, 401);
570
+ if (grantType === "authorization_code") {
571
+ const code = asStr(body.code);
572
+ const verifier = asStr(body.code_verifier);
573
+ const redirectUri = asStr(body.redirect_uri);
574
+ if (!code)
575
+ return tokenError(c, "invalid_request", "code requis");
576
+ if (!verifier) {
577
+ return tokenError(c, "invalid_request", "code_verifier requis (PKCE)");
578
+ }
579
+ const pending = (0, store_js_1.peekAuthCode)(code);
580
+ if (!pending) {
581
+ oauthLog("token_reject", {
582
+ grant: "authorization_code",
583
+ reason: "code_unknown_or_used",
584
+ client_id_prefix: auth.client_id.slice(0, 12),
585
+ });
586
+ return tokenError(c, "invalid_grant", "code inconnu, expiré ou déjà utilisé");
587
+ }
588
+ if (pending.client_id !== auth.client_id) {
589
+ oauthLog("token_reject", {
590
+ grant: "authorization_code",
591
+ reason: "client_mismatch",
592
+ client_id_prefix: auth.client_id.slice(0, 12),
593
+ });
594
+ return tokenError(c, "invalid_grant", "code émis pour un autre client");
595
+ }
596
+ if (redirectUri && redirectUri !== pending.redirect_uri) {
597
+ oauthLog("token_reject", {
598
+ grant: "authorization_code",
599
+ reason: "redirect_uri_mismatch",
600
+ client_id_prefix: auth.client_id.slice(0, 12),
601
+ redirect_host: redirectHost(redirectUri),
602
+ expected_host: redirectHost(pending.redirect_uri),
603
+ });
604
+ return tokenError(c, "invalid_grant", "redirect_uri différente de l'autorisation");
605
+ }
606
+ if (!(0, store_js_1.verifyPkceS256)(verifier, pending.code_challenge)) {
607
+ oauthLog("token_reject", {
608
+ grant: "authorization_code",
609
+ reason: "pkce_invalid",
610
+ client_id_prefix: auth.client_id.slice(0, 12),
611
+ });
612
+ return tokenError(c, "invalid_grant", "code_verifier PKCE invalide");
613
+ }
614
+ const resource = asStr(body.resource);
615
+ if (!(0, store_js_1.resourceAcceptable)(resource)) {
616
+ oauthLog("token_reject", {
617
+ grant: "authorization_code",
618
+ reason: "resource_invalid",
619
+ client_id_prefix: auth.client_id.slice(0, 12),
620
+ });
621
+ return tokenError(c, "invalid_target", `resource doit être ${(0, store_js_1.mcpResourceUrl)()}`);
622
+ }
623
+ const consumed = (0, store_js_1.consumeAuthCode)(code);
624
+ if (!consumed) {
625
+ oauthLog("token_reject", {
626
+ grant: "authorization_code",
627
+ reason: "code_race_consumed",
628
+ client_id_prefix: auth.client_id.slice(0, 12),
629
+ });
630
+ return tokenError(c, "invalid_grant", "code inconnu, expiré ou déjà utilisé");
631
+ }
632
+ const accessToken = await (0, store_js_1.signAccessToken)(auth.client_id, consumed.scope, consumed.user_id);
633
+ (0, store_js_1.touchOAuthClientLastUsed)(auth.client_id);
634
+ const refreshToken = (0, store_js_1.createRefreshToken)(auth.client_id, consumed.scope, consumed.resource, consumed.user_id);
635
+ oauthLog("token_ok", {
636
+ grant: "authorization_code",
637
+ client_id_prefix: auth.client_id.slice(0, 12),
638
+ has_resource: Boolean(resource || consumed.resource),
639
+ });
640
+ return c.json({
641
+ access_token: accessToken,
642
+ token_type: "bearer",
643
+ expires_in: store_js_1.ACCESS_TOKEN_TTL_S,
644
+ refresh_token: refreshToken,
645
+ scope: consumed.scope,
646
+ }, 200, { "Cache-Control": "no-store", Pragma: "no-cache" });
647
+ }
648
+ if (grantType === "refresh_token") {
649
+ const rt = asStr(body.refresh_token);
650
+ if (!rt) {
651
+ return tokenError(c, "invalid_request", "refresh_token requis");
652
+ }
653
+ const rotated = (0, store_js_1.rotateRefreshToken)(rt);
654
+ if (!rotated) {
655
+ return tokenError(c, "invalid_grant", "refresh_token invalide, expiré ou révoqué");
656
+ }
657
+ if (rotated.client_id !== auth.client_id) {
658
+ return tokenError(c, "invalid_grant", "refresh_token émis pour un autre client");
659
+ }
660
+ const accessToken = await (0, store_js_1.signAccessToken)(auth.client_id, rotated.scope, rotated.user_id);
661
+ (0, store_js_1.touchOAuthClientLastUsed)(auth.client_id);
662
+ return c.json({
663
+ access_token: accessToken,
664
+ token_type: "bearer",
665
+ expires_in: store_js_1.ACCESS_TOKEN_TTL_S,
666
+ refresh_token: rotated.refresh_token,
667
+ scope: rotated.scope,
668
+ }, 200, { "Cache-Control": "no-store", Pragma: "no-cache" });
669
+ }
670
+ return tokenError(c, "unsupported_grant_type", "authorization_code ou refresh_token");
671
+ });
672
+ return oauthRoutes;
673
+ }
674
+ //# sourceMappingURL=routes.js.map