kavachos 0.0.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 +21 -0
- package/dist/agent/index.d.ts +32 -0
- package/dist/agent/index.js +5 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/audit/index.d.ts +19 -0
- package/dist/audit/index.js +5 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/chunk-D2LJLY7F.js +207 -0
- package/dist/chunk-D2LJLY7F.js.map +1 -0
- package/dist/chunk-DTCKF26N.js +208 -0
- package/dist/chunk-DTCKF26N.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +9 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-XSYYQH75.js +153 -0
- package/dist/chunk-XSYYQH75.js.map +1 -0
- package/dist/chunk-XW2X3O53.js +92 -0
- package/dist/chunk-XW2X3O53.js.map +1 -0
- package/dist/index.d.ts +181 -0
- package/dist/index.js +862 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +222 -0
- package/dist/mcp/index.js +1005 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/permission/index.d.ts +84 -0
- package/dist/permission/index.js +5 -0
- package/dist/permission/index.js.map +1 -0
- package/dist/types-C5htunW6.d.ts +351 -0
- package/dist/types-fHHAt3tt.d.ts +2127 -0
- package/package.json +100 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import '../chunk-PZ5AY32C.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getRandomValues, randomUUID } from 'crypto';
|
|
4
|
+
import { jwtVerify, SignJWT } from 'jose';
|
|
5
|
+
|
|
6
|
+
var McpClientRegistrationSchema = z.object({
|
|
7
|
+
redirect_uris: z.array(z.string().url()),
|
|
8
|
+
token_endpoint_auth_method: z.enum(["none", "client_secret_basic", "client_secret_post"]).default("client_secret_basic").optional(),
|
|
9
|
+
grant_types: z.array(z.enum(["authorization_code", "refresh_token"])).default(["authorization_code"]).optional(),
|
|
10
|
+
response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
|
|
11
|
+
client_name: z.string().optional(),
|
|
12
|
+
client_uri: z.string().url().optional(),
|
|
13
|
+
logo_uri: z.string().url().optional(),
|
|
14
|
+
scope: z.string().optional(),
|
|
15
|
+
contacts: z.array(z.string()).optional(),
|
|
16
|
+
tos_uri: z.string().url().optional(),
|
|
17
|
+
policy_uri: z.string().url().optional(),
|
|
18
|
+
software_id: z.string().optional(),
|
|
19
|
+
software_version: z.string().optional()
|
|
20
|
+
});
|
|
21
|
+
var McpAuthorizeRequestSchema = z.object({
|
|
22
|
+
response_type: z.literal("code"),
|
|
23
|
+
client_id: z.string().min(1),
|
|
24
|
+
redirect_uri: z.string().min(1),
|
|
25
|
+
scope: z.string().optional(),
|
|
26
|
+
state: z.string().optional(),
|
|
27
|
+
code_challenge: z.string().min(43).max(128),
|
|
28
|
+
code_challenge_method: z.literal("S256"),
|
|
29
|
+
resource: z.string().url().optional()
|
|
30
|
+
});
|
|
31
|
+
var McpTokenRequestSchema = z.discriminatedUnion("grant_type", [
|
|
32
|
+
z.object({
|
|
33
|
+
grant_type: z.literal("authorization_code"),
|
|
34
|
+
code: z.string().min(1),
|
|
35
|
+
redirect_uri: z.string().min(1),
|
|
36
|
+
client_id: z.string().min(1),
|
|
37
|
+
client_secret: z.string().optional(),
|
|
38
|
+
code_verifier: z.string().min(43).max(128),
|
|
39
|
+
resource: z.string().url().optional()
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
grant_type: z.literal("refresh_token"),
|
|
43
|
+
refresh_token: z.string().min(1),
|
|
44
|
+
client_id: z.string().min(1),
|
|
45
|
+
client_secret: z.string().optional(),
|
|
46
|
+
scope: z.string().optional(),
|
|
47
|
+
resource: z.string().url().optional()
|
|
48
|
+
})
|
|
49
|
+
]);
|
|
50
|
+
function generateSecureToken(byteLength) {
|
|
51
|
+
const bytes = new Uint8Array(byteLength);
|
|
52
|
+
getRandomValues(bytes);
|
|
53
|
+
return uint8ArrayToBase64Url(bytes);
|
|
54
|
+
}
|
|
55
|
+
function generateAuthorizationCode() {
|
|
56
|
+
return randomUUID();
|
|
57
|
+
}
|
|
58
|
+
async function computeS256Challenge(codeVerifier) {
|
|
59
|
+
const encoder = new TextEncoder();
|
|
60
|
+
const data = encoder.encode(codeVerifier);
|
|
61
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
62
|
+
return uint8ArrayToBase64Url(new Uint8Array(digest));
|
|
63
|
+
}
|
|
64
|
+
async function verifyS256(codeVerifier, codeChallenge) {
|
|
65
|
+
const computed = await computeS256Challenge(codeVerifier);
|
|
66
|
+
return timingSafeEqual(computed, codeChallenge);
|
|
67
|
+
}
|
|
68
|
+
function timingSafeEqual(a, b) {
|
|
69
|
+
if (a.length !== b.length) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const encoder = new TextEncoder();
|
|
73
|
+
const bufA = encoder.encode(a);
|
|
74
|
+
const bufB = encoder.encode(b);
|
|
75
|
+
let diff = 0;
|
|
76
|
+
for (let i = 0; i < bufA.length; i++) {
|
|
77
|
+
diff |= bufA[i] ^ bufB[i];
|
|
78
|
+
}
|
|
79
|
+
return diff === 0;
|
|
80
|
+
}
|
|
81
|
+
function uint8ArrayToBase64Url(bytes) {
|
|
82
|
+
let binary = "";
|
|
83
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
84
|
+
binary += String.fromCharCode(bytes[i]);
|
|
85
|
+
}
|
|
86
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
87
|
+
}
|
|
88
|
+
async function parseRequestBody(request) {
|
|
89
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
90
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
91
|
+
const text = await request.text();
|
|
92
|
+
const params = new URLSearchParams(text);
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [key, value] of params.entries()) {
|
|
95
|
+
result[key] = value;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
if (contentType.includes("application/json")) {
|
|
100
|
+
const json = await request.json();
|
|
101
|
+
if (typeof json === "object" && json !== null) {
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const [key, value] of Object.entries(json)) {
|
|
104
|
+
if (typeof value === "string") {
|
|
105
|
+
result[key] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
function extractBasicAuth(request) {
|
|
114
|
+
const authorization = request.headers.get("authorization");
|
|
115
|
+
if (!authorization?.startsWith("Basic ")) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const encoded = authorization.slice(6);
|
|
120
|
+
const decoded = atob(encoded);
|
|
121
|
+
const colonIndex = decoded.indexOf(":");
|
|
122
|
+
if (colonIndex === -1) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const id = decoded.slice(0, colonIndex);
|
|
126
|
+
const secret = decoded.slice(colonIndex + 1);
|
|
127
|
+
if (!id || !secret) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return [id, secret];
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function extractBearerToken(request) {
|
|
136
|
+
const authorization = request.headers.get("authorization");
|
|
137
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return authorization.slice(7);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/mcp/authorize.ts
|
|
144
|
+
async function handleAuthorize(ctx, request) {
|
|
145
|
+
const url = new URL(request.url);
|
|
146
|
+
const params = {};
|
|
147
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
148
|
+
params[key] = value;
|
|
149
|
+
}
|
|
150
|
+
const parsed = McpAuthorizeRequestSchema.safeParse(params);
|
|
151
|
+
if (!parsed.success) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: {
|
|
155
|
+
code: "INVALID_REQUEST",
|
|
156
|
+
message: "Invalid authorization request parameters",
|
|
157
|
+
details: { issues: parsed.error.flatten().fieldErrors }
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const {
|
|
162
|
+
client_id: clientId,
|
|
163
|
+
redirect_uri: redirectUri,
|
|
164
|
+
scope: scopeParam,
|
|
165
|
+
state,
|
|
166
|
+
code_challenge: codeChallenge,
|
|
167
|
+
code_challenge_method: codeChallengeMethod,
|
|
168
|
+
resource
|
|
169
|
+
} = parsed.data;
|
|
170
|
+
const client = await ctx.findClient(clientId);
|
|
171
|
+
if (!client) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: {
|
|
175
|
+
code: "INVALID_CLIENT",
|
|
176
|
+
message: `Unknown client_id: ${clientId}`
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (client.disabled) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: {
|
|
184
|
+
code: "INVALID_CLIENT",
|
|
185
|
+
message: "Client is disabled"
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (!client.redirectUris.includes(redirectUri)) {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: {
|
|
193
|
+
code: "INVALID_REDIRECT_URI",
|
|
194
|
+
message: "redirect_uri does not match any registered redirect URI"
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (codeChallengeMethod !== "S256") {
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
error: {
|
|
202
|
+
code: "INVALID_REQUEST",
|
|
203
|
+
message: "Only S256 code_challenge_method is supported"
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (resource !== void 0) {
|
|
208
|
+
const allowedResources = ctx.config.allowedResources;
|
|
209
|
+
if (allowedResources && allowedResources.length > 0) {
|
|
210
|
+
if (!allowedResources.includes(resource)) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: {
|
|
214
|
+
code: "INVALID_TARGET",
|
|
215
|
+
message: `Resource '${resource}' is not a recognized MCP server`
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const requestedScopes = scopeParam ? scopeParam.split(" ").filter(Boolean) : [];
|
|
222
|
+
const supportedScopes = new Set(ctx.config.scopes ?? []);
|
|
223
|
+
const defaultScopes = /* @__PURE__ */ new Set(["openid", "profile", "email", "offline_access"]);
|
|
224
|
+
const allSupported = /* @__PURE__ */ new Set([...supportedScopes, ...defaultScopes]);
|
|
225
|
+
for (const scope of requestedScopes) {
|
|
226
|
+
if (!allSupported.has(scope)) {
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
error: {
|
|
230
|
+
code: "INVALID_SCOPE",
|
|
231
|
+
message: `Unsupported scope: ${scope}`
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const effectiveScopes = requestedScopes.length > 0 ? requestedScopes : ["openid"];
|
|
237
|
+
const userId = await ctx.resolveUserId(request);
|
|
238
|
+
if (!userId) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: {
|
|
242
|
+
code: "LOGIN_REQUIRED",
|
|
243
|
+
message: "User must be authenticated before authorization",
|
|
244
|
+
details: {
|
|
245
|
+
loginPage: ctx.config.loginPage,
|
|
246
|
+
// Pass all original query params so the login page can redirect back
|
|
247
|
+
returnTo: request.url
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const code = generateAuthorizationCode();
|
|
253
|
+
const now = /* @__PURE__ */ new Date();
|
|
254
|
+
const expiresAt = new Date(now.getTime() + ctx.config.codeTtl * 1e3);
|
|
255
|
+
const authCode = {
|
|
256
|
+
code,
|
|
257
|
+
clientId,
|
|
258
|
+
userId,
|
|
259
|
+
redirectUri,
|
|
260
|
+
scope: effectiveScopes,
|
|
261
|
+
codeChallenge,
|
|
262
|
+
codeChallengeMethod: "S256",
|
|
263
|
+
resource: resource ?? null,
|
|
264
|
+
expiresAt,
|
|
265
|
+
createdAt: now
|
|
266
|
+
};
|
|
267
|
+
await ctx.storeAuthorizationCode(authCode);
|
|
268
|
+
const redirectUrl = new URL(redirectUri);
|
|
269
|
+
redirectUrl.searchParams.set("code", code);
|
|
270
|
+
if (state) {
|
|
271
|
+
redirectUrl.searchParams.set("state", state);
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
data: {
|
|
276
|
+
redirectUri: redirectUrl.toString(),
|
|
277
|
+
code,
|
|
278
|
+
state: state ?? null
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/mcp/metadata.ts
|
|
284
|
+
function getAuthorizationServerMetadata(ctx) {
|
|
285
|
+
const { issuer, baseUrl, scopes } = ctx.config;
|
|
286
|
+
const defaultScopes = ["openid", "profile", "email", "offline_access"];
|
|
287
|
+
const allScopes = [.../* @__PURE__ */ new Set([...defaultScopes, ...scopes ?? []])];
|
|
288
|
+
return {
|
|
289
|
+
issuer,
|
|
290
|
+
authorization_endpoint: `${baseUrl}/mcp/authorize`,
|
|
291
|
+
token_endpoint: `${baseUrl}/mcp/token`,
|
|
292
|
+
registration_endpoint: `${baseUrl}/mcp/register`,
|
|
293
|
+
jwks_uri: `${baseUrl}/mcp/jwks`,
|
|
294
|
+
scopes_supported: allScopes,
|
|
295
|
+
response_types_supported: ["code"],
|
|
296
|
+
response_modes_supported: ["query"],
|
|
297
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
298
|
+
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
|
|
299
|
+
code_challenge_methods_supported: ["S256"],
|
|
300
|
+
revocation_endpoint: `${baseUrl}/mcp/revoke`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function getProtectedResourceMetadata(ctx) {
|
|
304
|
+
const { issuer, baseUrl, scopes } = ctx.config;
|
|
305
|
+
const defaultScopes = ["openid", "profile", "email", "offline_access"];
|
|
306
|
+
const allScopes = [.../* @__PURE__ */ new Set([...defaultScopes, ...scopes ?? []])];
|
|
307
|
+
return {
|
|
308
|
+
resource: issuer,
|
|
309
|
+
authorization_servers: [issuer],
|
|
310
|
+
jwks_uri: `${baseUrl}/mcp/jwks`,
|
|
311
|
+
scopes_supported: allScopes,
|
|
312
|
+
bearer_methods_supported: ["header"],
|
|
313
|
+
resource_signing_alg_values_supported: ["HS256"]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async function resolveClientIdMetadataDocument(clientId) {
|
|
317
|
+
try {
|
|
318
|
+
const url = new URL(clientId);
|
|
319
|
+
if (url.protocol !== "https:") {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const response = await fetch(clientId, {
|
|
323
|
+
headers: { Accept: "application/json" },
|
|
324
|
+
signal: AbortSignal.timeout(5e3)
|
|
325
|
+
});
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
const metadata = await response.json();
|
|
330
|
+
if (Array.isArray(metadata.redirect_uris)) {
|
|
331
|
+
return metadata.redirect_uris;
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function registerClient(ctx, body) {
|
|
339
|
+
const parsed = McpClientRegistrationSchema.safeParse(body);
|
|
340
|
+
if (!parsed.success) {
|
|
341
|
+
return {
|
|
342
|
+
success: false,
|
|
343
|
+
error: {
|
|
344
|
+
code: "INVALID_CLIENT_METADATA",
|
|
345
|
+
message: "Invalid client registration request",
|
|
346
|
+
details: { issues: parsed.error.flatten().fieldErrors }
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const data = parsed.data;
|
|
351
|
+
const redirectUris = data.redirect_uris;
|
|
352
|
+
const grantTypes = data.grant_types ?? ["authorization_code"];
|
|
353
|
+
const responseTypes = data.response_types ?? ["code"];
|
|
354
|
+
const authMethod = data.token_endpoint_auth_method ?? "client_secret_basic";
|
|
355
|
+
if (grantTypes.includes("authorization_code") && redirectUris.length === 0) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: {
|
|
359
|
+
code: "INVALID_REDIRECT_URI",
|
|
360
|
+
message: "redirect_uris are required when grant_types includes authorization_code"
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) {
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
error: {
|
|
368
|
+
code: "INVALID_CLIENT_METADATA",
|
|
369
|
+
message: "response_types must include 'code' when grant_types includes 'authorization_code'"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
for (const uri of redirectUris) {
|
|
374
|
+
try {
|
|
375
|
+
const parsed2 = new URL(uri);
|
|
376
|
+
if (parsed2.protocol !== "https:" && parsed2.hostname !== "localhost" && parsed2.hostname !== "127.0.0.1" && parsed2.hostname !== "[::1]") {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
error: {
|
|
380
|
+
code: "INVALID_REDIRECT_URI",
|
|
381
|
+
message: `redirect_uri must use HTTPS (or localhost for development): ${uri}`
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (parsed2.hash) {
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: {
|
|
389
|
+
code: "INVALID_REDIRECT_URI",
|
|
390
|
+
message: `redirect_uri must not contain a fragment: ${uri}`
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
error: {
|
|
398
|
+
code: "INVALID_REDIRECT_URI",
|
|
399
|
+
message: `redirect_uri is not a valid URL: ${uri}`
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const clientId = randomUUID();
|
|
405
|
+
const isPublic = authMethod === "none";
|
|
406
|
+
const clientSecret = isPublic ? null : generateSecureToken(48);
|
|
407
|
+
if (data.client_uri) {
|
|
408
|
+
const metadataUris = await resolveClientIdMetadataDocument(data.client_uri);
|
|
409
|
+
if (metadataUris !== null) {
|
|
410
|
+
const metadataSet = new Set(metadataUris);
|
|
411
|
+
const mismatched = redirectUris.filter((u) => !metadataSet.has(u));
|
|
412
|
+
if (mismatched.length > 0) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error: {
|
|
416
|
+
code: "INVALID_REDIRECT_URI",
|
|
417
|
+
message: "redirect_uris do not match those in the Client ID Metadata Document",
|
|
418
|
+
details: { mismatched }
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const now = /* @__PURE__ */ new Date();
|
|
425
|
+
const client = {
|
|
426
|
+
clientId,
|
|
427
|
+
clientSecret,
|
|
428
|
+
clientName: data.client_name ?? null,
|
|
429
|
+
clientUri: data.client_uri ?? null,
|
|
430
|
+
logoUri: data.logo_uri ?? null,
|
|
431
|
+
redirectUris,
|
|
432
|
+
grantTypes,
|
|
433
|
+
responseTypes,
|
|
434
|
+
tokenEndpointAuthMethod: authMethod,
|
|
435
|
+
scope: data.scope ?? null,
|
|
436
|
+
contacts: data.contacts ?? null,
|
|
437
|
+
tosUri: data.tos_uri ?? null,
|
|
438
|
+
policyUri: data.policy_uri ?? null,
|
|
439
|
+
softwareId: data.software_id ?? null,
|
|
440
|
+
softwareVersion: data.software_version ?? null,
|
|
441
|
+
clientType: isPublic ? "public" : "confidential",
|
|
442
|
+
disabled: false,
|
|
443
|
+
userId: null,
|
|
444
|
+
createdAt: now,
|
|
445
|
+
updatedAt: now
|
|
446
|
+
};
|
|
447
|
+
await ctx.storeClient(client);
|
|
448
|
+
const response = {
|
|
449
|
+
client_id: clientId,
|
|
450
|
+
client_id_issued_at: Math.floor(now.getTime() / 1e3),
|
|
451
|
+
redirect_uris: redirectUris,
|
|
452
|
+
token_endpoint_auth_method: authMethod,
|
|
453
|
+
grant_types: grantTypes,
|
|
454
|
+
response_types: responseTypes,
|
|
455
|
+
...data.client_name ? { client_name: data.client_name } : {},
|
|
456
|
+
...data.client_uri ? { client_uri: data.client_uri } : {},
|
|
457
|
+
...data.logo_uri ? { logo_uri: data.logo_uri } : {},
|
|
458
|
+
...data.scope ? { scope: data.scope } : {},
|
|
459
|
+
...data.contacts ? { contacts: data.contacts } : {},
|
|
460
|
+
...data.tos_uri ? { tos_uri: data.tos_uri } : {},
|
|
461
|
+
...data.policy_uri ? { policy_uri: data.policy_uri } : {},
|
|
462
|
+
...data.software_id ? { software_id: data.software_id } : {},
|
|
463
|
+
...data.software_version ? { software_version: data.software_version } : {},
|
|
464
|
+
...clientSecret !== null ? { client_secret: clientSecret, client_secret_expires_at: 0 } : {}
|
|
465
|
+
};
|
|
466
|
+
return { success: true, data: response };
|
|
467
|
+
}
|
|
468
|
+
async function getSigningKey(secret) {
|
|
469
|
+
const encoder = new TextEncoder();
|
|
470
|
+
return globalThis.crypto.subtle.importKey(
|
|
471
|
+
"raw",
|
|
472
|
+
encoder.encode(secret),
|
|
473
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
474
|
+
false,
|
|
475
|
+
["sign", "verify"]
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
async function issueAccessTokenJwt(ctx, userId, clientId, scopes, resource) {
|
|
479
|
+
const secret = ctx.config.signingSecret;
|
|
480
|
+
if (!secret) {
|
|
481
|
+
throw new Error("MCP signingSecret is required to issue tokens");
|
|
482
|
+
}
|
|
483
|
+
const key = await getSigningKey(secret);
|
|
484
|
+
const jti = randomUUID();
|
|
485
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
486
|
+
const exp = now + ctx.config.accessTokenTtl;
|
|
487
|
+
const expiresAt = new Date(exp * 1e3);
|
|
488
|
+
const audience = resource ?? ctx.config.issuer;
|
|
489
|
+
const jwt = await new SignJWT({
|
|
490
|
+
sub: userId,
|
|
491
|
+
client_id: clientId,
|
|
492
|
+
scope: scopes.join(" "),
|
|
493
|
+
jti
|
|
494
|
+
}).setProtectedHeader({ alg: "HS256", typ: "at+jwt" }).setIssuer(ctx.config.issuer).setAudience(audience).setIssuedAt(now).setExpirationTime(exp).sign(key);
|
|
495
|
+
return { jwt, jti, expiresAt };
|
|
496
|
+
}
|
|
497
|
+
function resolveClientCredentials(request, body) {
|
|
498
|
+
const basicAuth = extractBasicAuth(request);
|
|
499
|
+
if (basicAuth) {
|
|
500
|
+
return { clientId: basicAuth[0], clientSecret: basicAuth[1] };
|
|
501
|
+
}
|
|
502
|
+
const clientId = body.client_id;
|
|
503
|
+
if (!clientId) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
clientId,
|
|
508
|
+
clientSecret: body.client_secret ?? null
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async function handleTokenExchange(ctx, request) {
|
|
512
|
+
const body = await parseRequestBody(request);
|
|
513
|
+
const credentials = resolveClientCredentials(request, body);
|
|
514
|
+
if (!credentials) {
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
error: {
|
|
518
|
+
code: "INVALID_CLIENT",
|
|
519
|
+
message: "client_id is required"
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
body.client_id = credentials.clientId;
|
|
524
|
+
if (credentials.clientSecret) {
|
|
525
|
+
body.client_secret = credentials.clientSecret;
|
|
526
|
+
}
|
|
527
|
+
const parsed = McpTokenRequestSchema.safeParse(body);
|
|
528
|
+
if (!parsed.success) {
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
error: {
|
|
532
|
+
code: "INVALID_REQUEST",
|
|
533
|
+
message: "Invalid token request",
|
|
534
|
+
details: { issues: parsed.error.flatten().fieldErrors }
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const data = parsed.data;
|
|
539
|
+
if (data.grant_type === "authorization_code") {
|
|
540
|
+
return handleAuthorizationCodeGrant(ctx, data, credentials.clientSecret);
|
|
541
|
+
}
|
|
542
|
+
return handleRefreshTokenGrant(ctx, data, credentials.clientSecret);
|
|
543
|
+
}
|
|
544
|
+
async function handleAuthorizationCodeGrant(ctx, data, clientSecret) {
|
|
545
|
+
const client = await ctx.findClient(data.client_id);
|
|
546
|
+
if (!client) {
|
|
547
|
+
return {
|
|
548
|
+
success: false,
|
|
549
|
+
error: { code: "INVALID_CLIENT", message: "Unknown client_id" }
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (client.disabled) {
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
error: { code: "INVALID_CLIENT", message: "Client is disabled" }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (client.clientType === "confidential") {
|
|
559
|
+
if (!clientSecret || clientSecret !== client.clientSecret) {
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
error: { code: "INVALID_CLIENT", message: "Invalid client_secret" }
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const authCode = await ctx.consumeAuthorizationCode(data.code);
|
|
567
|
+
if (!authCode) {
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
error: { code: "INVALID_GRANT", message: "Invalid or expired authorization code" }
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (authCode.expiresAt < /* @__PURE__ */ new Date()) {
|
|
574
|
+
return {
|
|
575
|
+
success: false,
|
|
576
|
+
error: { code: "INVALID_GRANT", message: "Authorization code has expired" }
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (authCode.clientId !== data.client_id) {
|
|
580
|
+
return {
|
|
581
|
+
success: false,
|
|
582
|
+
error: { code: "INVALID_GRANT", message: "client_id does not match authorization code" }
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (authCode.redirectUri !== data.redirect_uri) {
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
error: {
|
|
589
|
+
code: "INVALID_GRANT",
|
|
590
|
+
message: "redirect_uri does not match authorization code"
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const pkceValid = await verifyS256(data.code_verifier, authCode.codeChallenge);
|
|
595
|
+
if (!pkceValid) {
|
|
596
|
+
return {
|
|
597
|
+
success: false,
|
|
598
|
+
error: { code: "INVALID_GRANT", message: "PKCE code_verifier verification failed" }
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (data.resource !== void 0 && authCode.resource !== null) {
|
|
602
|
+
if (data.resource !== authCode.resource) {
|
|
603
|
+
return {
|
|
604
|
+
success: false,
|
|
605
|
+
error: {
|
|
606
|
+
code: "INVALID_TARGET",
|
|
607
|
+
message: "resource parameter does not match authorization code"
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const resource = data.resource ?? authCode.resource;
|
|
613
|
+
const { jwt, expiresAt } = await issueAccessTokenJwt(
|
|
614
|
+
ctx,
|
|
615
|
+
authCode.userId,
|
|
616
|
+
data.client_id,
|
|
617
|
+
authCode.scope,
|
|
618
|
+
resource
|
|
619
|
+
);
|
|
620
|
+
const includeRefreshToken = authCode.scope.includes("offline_access");
|
|
621
|
+
const refreshToken = includeRefreshToken ? generateSecureToken(48) : null;
|
|
622
|
+
const tokenRecord = {
|
|
623
|
+
accessToken: jwt,
|
|
624
|
+
refreshToken,
|
|
625
|
+
tokenType: "Bearer",
|
|
626
|
+
expiresIn: ctx.config.accessTokenTtl,
|
|
627
|
+
scope: authCode.scope,
|
|
628
|
+
clientId: data.client_id,
|
|
629
|
+
userId: authCode.userId,
|
|
630
|
+
resource,
|
|
631
|
+
expiresAt,
|
|
632
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
633
|
+
};
|
|
634
|
+
await ctx.storeToken(tokenRecord);
|
|
635
|
+
const response = {
|
|
636
|
+
access_token: jwt,
|
|
637
|
+
token_type: "Bearer",
|
|
638
|
+
expires_in: ctx.config.accessTokenTtl,
|
|
639
|
+
scope: authCode.scope.join(" "),
|
|
640
|
+
...refreshToken ? { refresh_token: refreshToken } : {}
|
|
641
|
+
};
|
|
642
|
+
return { success: true, data: response };
|
|
643
|
+
}
|
|
644
|
+
async function handleRefreshTokenGrant(ctx, data, clientSecret) {
|
|
645
|
+
const client = await ctx.findClient(data.client_id);
|
|
646
|
+
if (!client) {
|
|
647
|
+
return {
|
|
648
|
+
success: false,
|
|
649
|
+
error: { code: "INVALID_CLIENT", message: "Unknown client_id" }
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (client.disabled) {
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
error: { code: "INVALID_CLIENT", message: "Client is disabled" }
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (client.clientType === "confidential") {
|
|
659
|
+
if (!clientSecret || clientSecret !== client.clientSecret) {
|
|
660
|
+
return {
|
|
661
|
+
success: false,
|
|
662
|
+
error: { code: "INVALID_CLIENT", message: "Invalid client_secret" }
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const existingToken = await ctx.findTokenByRefreshToken(data.refresh_token);
|
|
667
|
+
if (!existingToken) {
|
|
668
|
+
return {
|
|
669
|
+
success: false,
|
|
670
|
+
error: { code: "INVALID_GRANT", message: "Invalid refresh token" }
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if (existingToken.clientId !== data.client_id) {
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
error: { code: "INVALID_GRANT", message: "client_id does not match refresh token" }
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const refreshExpiry = new Date(
|
|
680
|
+
existingToken.createdAt.getTime() + ctx.config.refreshTokenTtl * 1e3
|
|
681
|
+
);
|
|
682
|
+
if (refreshExpiry < /* @__PURE__ */ new Date()) {
|
|
683
|
+
return {
|
|
684
|
+
success: false,
|
|
685
|
+
error: { code: "INVALID_GRANT", message: "Refresh token has expired" }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const scopes = data.scope ? data.scope.split(" ").filter((s) => existingToken.scope.includes(s)) : existingToken.scope;
|
|
689
|
+
const resource = data.resource ?? existingToken.resource;
|
|
690
|
+
await ctx.revokeToken(existingToken.accessToken);
|
|
691
|
+
const { jwt, expiresAt } = await issueAccessTokenJwt(
|
|
692
|
+
ctx,
|
|
693
|
+
existingToken.userId,
|
|
694
|
+
data.client_id,
|
|
695
|
+
scopes,
|
|
696
|
+
resource
|
|
697
|
+
);
|
|
698
|
+
const newRefreshToken = generateSecureToken(48);
|
|
699
|
+
const tokenRecord = {
|
|
700
|
+
accessToken: jwt,
|
|
701
|
+
refreshToken: newRefreshToken,
|
|
702
|
+
tokenType: "Bearer",
|
|
703
|
+
expiresIn: ctx.config.accessTokenTtl,
|
|
704
|
+
scope: scopes,
|
|
705
|
+
clientId: data.client_id,
|
|
706
|
+
userId: existingToken.userId,
|
|
707
|
+
resource,
|
|
708
|
+
expiresAt,
|
|
709
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
710
|
+
};
|
|
711
|
+
await ctx.storeToken(tokenRecord);
|
|
712
|
+
const response = {
|
|
713
|
+
access_token: jwt,
|
|
714
|
+
token_type: "Bearer",
|
|
715
|
+
expires_in: ctx.config.accessTokenTtl,
|
|
716
|
+
refresh_token: newRefreshToken,
|
|
717
|
+
scope: scopes.join(" ")
|
|
718
|
+
};
|
|
719
|
+
return { success: true, data: response };
|
|
720
|
+
}
|
|
721
|
+
async function getVerificationKey(secret) {
|
|
722
|
+
const encoder = new TextEncoder();
|
|
723
|
+
return globalThis.crypto.subtle.importKey(
|
|
724
|
+
"raw",
|
|
725
|
+
encoder.encode(secret),
|
|
726
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
727
|
+
false,
|
|
728
|
+
["verify"]
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
async function validateAccessToken(ctx, token, options) {
|
|
732
|
+
const secret = ctx.config.signingSecret;
|
|
733
|
+
if (!secret) {
|
|
734
|
+
return {
|
|
735
|
+
success: false,
|
|
736
|
+
error: {
|
|
737
|
+
code: "SERVER_ERROR",
|
|
738
|
+
message: "MCP signingSecret is not configured"
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
let payload;
|
|
743
|
+
try {
|
|
744
|
+
const key = await getVerificationKey(secret);
|
|
745
|
+
const result = await jwtVerify(token, key, {
|
|
746
|
+
issuer: ctx.config.issuer,
|
|
747
|
+
algorithms: ["HS256"],
|
|
748
|
+
...options?.expectedAudience ? { audience: options.expectedAudience } : {}
|
|
749
|
+
});
|
|
750
|
+
payload = result.payload;
|
|
751
|
+
} catch (err) {
|
|
752
|
+
const message = err instanceof Error ? err.message : "Token verification failed";
|
|
753
|
+
let code = "INVALID_TOKEN";
|
|
754
|
+
if (message.includes("expired")) {
|
|
755
|
+
code = "TOKEN_EXPIRED";
|
|
756
|
+
} else if (message.includes("audience")) {
|
|
757
|
+
code = "INVALID_AUDIENCE";
|
|
758
|
+
} else if (message.includes("issuer")) {
|
|
759
|
+
code = "INVALID_ISSUER";
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
success: false,
|
|
763
|
+
error: { code, message }
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (!payload.sub || !payload.client_id || !payload.jti) {
|
|
767
|
+
return {
|
|
768
|
+
success: false,
|
|
769
|
+
error: {
|
|
770
|
+
code: "INVALID_TOKEN",
|
|
771
|
+
message: "Token is missing required claims (sub, client_id, jti)"
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const audience = payload.aud;
|
|
776
|
+
if (!audience) {
|
|
777
|
+
return {
|
|
778
|
+
success: false,
|
|
779
|
+
error: {
|
|
780
|
+
code: "INVALID_AUDIENCE",
|
|
781
|
+
message: "Token has no audience claim. Tokens must be bound to a resource."
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const tokenScopes = payload.scope ? payload.scope.split(" ") : [];
|
|
786
|
+
const requiredScopes = options?.requiredScopes ?? [];
|
|
787
|
+
for (const required of requiredScopes) {
|
|
788
|
+
if (!tokenScopes.includes(required)) {
|
|
789
|
+
return {
|
|
790
|
+
success: false,
|
|
791
|
+
error: {
|
|
792
|
+
code: "INSUFFICIENT_SCOPE",
|
|
793
|
+
message: `Token is missing required scope: ${required}`,
|
|
794
|
+
details: {
|
|
795
|
+
required: requiredScopes,
|
|
796
|
+
present: tokenScopes
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const resource = Array.isArray(audience) ? audience[0] ?? null : audience;
|
|
803
|
+
const session = {
|
|
804
|
+
userId: payload.sub,
|
|
805
|
+
clientId: payload.client_id,
|
|
806
|
+
scopes: tokenScopes,
|
|
807
|
+
resource,
|
|
808
|
+
expiresAt: new Date(payload.exp * 1e3),
|
|
809
|
+
tokenId: payload.jti
|
|
810
|
+
};
|
|
811
|
+
return { success: true, data: session };
|
|
812
|
+
}
|
|
813
|
+
async function withMcpAuth(ctx, request, options) {
|
|
814
|
+
const token = extractBearerToken(request);
|
|
815
|
+
if (!token) {
|
|
816
|
+
const resourceMetadataUrl = `${ctx.config.baseUrl}/.well-known/oauth-protected-resource`;
|
|
817
|
+
return {
|
|
818
|
+
success: false,
|
|
819
|
+
error: {
|
|
820
|
+
code: "UNAUTHORIZED",
|
|
821
|
+
message: "Bearer token required",
|
|
822
|
+
details: {
|
|
823
|
+
"www-authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
return validateAccessToken(ctx, token, options);
|
|
829
|
+
}
|
|
830
|
+
function buildUnauthorizedResponse(ctx, error) {
|
|
831
|
+
const resourceMetadataUrl = `${ctx.config.baseUrl}/.well-known/oauth-protected-resource`;
|
|
832
|
+
const wwwAuthenticate = `Bearer resource_metadata="${resourceMetadataUrl}"`;
|
|
833
|
+
return new Response(
|
|
834
|
+
JSON.stringify({
|
|
835
|
+
jsonrpc: "2.0",
|
|
836
|
+
error: {
|
|
837
|
+
code: -32e3,
|
|
838
|
+
message: error.message,
|
|
839
|
+
"www-authenticate": wwwAuthenticate
|
|
840
|
+
},
|
|
841
|
+
id: null
|
|
842
|
+
}),
|
|
843
|
+
{
|
|
844
|
+
status: 401,
|
|
845
|
+
headers: {
|
|
846
|
+
"Content-Type": "application/json",
|
|
847
|
+
"WWW-Authenticate": wwwAuthenticate,
|
|
848
|
+
"Access-Control-Expose-Headers": "WWW-Authenticate"
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/mcp/server.ts
|
|
855
|
+
var DEFAULT_ACCESS_TOKEN_TTL = 3600;
|
|
856
|
+
var DEFAULT_REFRESH_TOKEN_TTL = 604800;
|
|
857
|
+
var DEFAULT_CODE_TTL = 600;
|
|
858
|
+
function createMcpModule(params) {
|
|
859
|
+
const config = params.config;
|
|
860
|
+
if (!config.issuer) {
|
|
861
|
+
throw new Error("McpConfig.issuer is required");
|
|
862
|
+
}
|
|
863
|
+
if (!config.baseUrl) {
|
|
864
|
+
throw new Error("McpConfig.baseUrl is required");
|
|
865
|
+
}
|
|
866
|
+
if (!config.signingSecret) {
|
|
867
|
+
throw new Error("McpConfig.signingSecret is required (>= 32 chars)");
|
|
868
|
+
}
|
|
869
|
+
if (config.signingSecret.length < 32) {
|
|
870
|
+
throw new Error("McpConfig.signingSecret must be at least 32 characters");
|
|
871
|
+
}
|
|
872
|
+
const resolvedConfig = {
|
|
873
|
+
...config,
|
|
874
|
+
issuer: config.issuer,
|
|
875
|
+
baseUrl: config.baseUrl,
|
|
876
|
+
signingSecret: config.signingSecret,
|
|
877
|
+
accessTokenTtl: config.accessTokenTtl ?? DEFAULT_ACCESS_TOKEN_TTL,
|
|
878
|
+
refreshTokenTtl: config.refreshTokenTtl ?? DEFAULT_REFRESH_TOKEN_TTL,
|
|
879
|
+
codeTtl: config.codeTtl ?? DEFAULT_CODE_TTL
|
|
880
|
+
};
|
|
881
|
+
const ctx = {
|
|
882
|
+
config: resolvedConfig,
|
|
883
|
+
storeClient: params.storeClient,
|
|
884
|
+
findClient: params.findClient,
|
|
885
|
+
storeAuthorizationCode: params.storeAuthorizationCode,
|
|
886
|
+
consumeAuthorizationCode: params.consumeAuthorizationCode,
|
|
887
|
+
storeToken: params.storeToken,
|
|
888
|
+
findTokenByRefreshToken: params.findTokenByRefreshToken,
|
|
889
|
+
revokeToken: params.revokeToken,
|
|
890
|
+
resolveUserId: params.resolveUserId
|
|
891
|
+
};
|
|
892
|
+
return {
|
|
893
|
+
getMetadata: () => getAuthorizationServerMetadata(ctx),
|
|
894
|
+
getProtectedResourceMetadata: () => getProtectedResourceMetadata(ctx),
|
|
895
|
+
registerClient: (body) => registerClient(ctx, body),
|
|
896
|
+
authorize: (request) => handleAuthorize(ctx, request),
|
|
897
|
+
token: (request) => handleTokenExchange(ctx, request),
|
|
898
|
+
validateToken: (token, requiredScopes) => validateAccessToken(ctx, token, { requiredScopes }),
|
|
899
|
+
middleware: (request) => withMcpAuth(ctx, request)
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function createMcpResponseHelpers(ctx) {
|
|
903
|
+
const corsHeaders = {
|
|
904
|
+
"Access-Control-Allow-Origin": "*",
|
|
905
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
906
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
907
|
+
"Access-Control-Max-Age": "86400"
|
|
908
|
+
};
|
|
909
|
+
return {
|
|
910
|
+
/** Metadata endpoints: 200 with JSON */
|
|
911
|
+
metadataResponse: (data) => new Response(JSON.stringify(data), {
|
|
912
|
+
status: 200,
|
|
913
|
+
headers: {
|
|
914
|
+
"Content-Type": "application/json",
|
|
915
|
+
...corsHeaders
|
|
916
|
+
}
|
|
917
|
+
}),
|
|
918
|
+
/** Registration: 201 with Cache-Control: no-store */
|
|
919
|
+
registrationResponse: (result) => {
|
|
920
|
+
if (!result.success) {
|
|
921
|
+
return new Response(
|
|
922
|
+
JSON.stringify({
|
|
923
|
+
error: "invalid_client_metadata",
|
|
924
|
+
error_description: result.error.message
|
|
925
|
+
}),
|
|
926
|
+
{
|
|
927
|
+
status: 400,
|
|
928
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return new Response(JSON.stringify(result.data), {
|
|
933
|
+
status: 201,
|
|
934
|
+
headers: {
|
|
935
|
+
"Content-Type": "application/json",
|
|
936
|
+
"Cache-Control": "no-store",
|
|
937
|
+
Pragma: "no-cache",
|
|
938
|
+
...corsHeaders
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
},
|
|
942
|
+
/** Authorization: 302 redirect or error */
|
|
943
|
+
authorizeResponse: (result) => {
|
|
944
|
+
if (!result.success) {
|
|
945
|
+
if (result.error.code === "LOGIN_REQUIRED") {
|
|
946
|
+
const details = result.error.details;
|
|
947
|
+
if (details?.loginPage) {
|
|
948
|
+
const loginUrl = new URL(details.loginPage);
|
|
949
|
+
if (details.returnTo) {
|
|
950
|
+
loginUrl.searchParams.set("returnTo", details.returnTo);
|
|
951
|
+
}
|
|
952
|
+
return Response.redirect(loginUrl.toString(), 302);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return new Response(
|
|
956
|
+
JSON.stringify({
|
|
957
|
+
error: result.error.code.toLowerCase(),
|
|
958
|
+
error_description: result.error.message
|
|
959
|
+
}),
|
|
960
|
+
{
|
|
961
|
+
status: 400,
|
|
962
|
+
headers: { "Content-Type": "application/json" }
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
return Response.redirect(result.data.redirectUri, 302);
|
|
967
|
+
},
|
|
968
|
+
/** Token: 200 with Cache-Control: no-store or error */
|
|
969
|
+
tokenResponse: (result) => {
|
|
970
|
+
if (!result.success) {
|
|
971
|
+
const status = result.error.code === "INVALID_CLIENT" ? 401 : 400;
|
|
972
|
+
return new Response(
|
|
973
|
+
JSON.stringify({
|
|
974
|
+
error: result.error.code.toLowerCase(),
|
|
975
|
+
error_description: result.error.message
|
|
976
|
+
}),
|
|
977
|
+
{
|
|
978
|
+
status,
|
|
979
|
+
headers: {
|
|
980
|
+
"Content-Type": "application/json",
|
|
981
|
+
"Cache-Control": "no-store",
|
|
982
|
+
Pragma: "no-cache",
|
|
983
|
+
...corsHeaders
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
return new Response(JSON.stringify(result.data), {
|
|
989
|
+
status: 200,
|
|
990
|
+
headers: {
|
|
991
|
+
"Content-Type": "application/json",
|
|
992
|
+
"Cache-Control": "no-store",
|
|
993
|
+
Pragma: "no-cache",
|
|
994
|
+
...corsHeaders
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
},
|
|
998
|
+
/** Auth failure in JSON-RPC format for MCP resource servers */
|
|
999
|
+
unauthorizedResponse: (error) => buildUnauthorizedResponse(ctx, error)
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export { McpAuthorizeRequestSchema, McpClientRegistrationSchema, McpTokenRequestSchema, buildUnauthorizedResponse, computeS256Challenge, createMcpModule, createMcpResponseHelpers, extractBasicAuth, extractBearerToken, generateSecureToken, getAuthorizationServerMetadata, getProtectedResourceMetadata, handleAuthorize, handleTokenExchange, parseRequestBody, registerClient, validateAccessToken, verifyS256, withMcpAuth };
|
|
1004
|
+
//# sourceMappingURL=index.js.map
|
|
1005
|
+
//# sourceMappingURL=index.js.map
|