@nocoo/base-mcp 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +315 -3
  2. package/dist/auth/index.d.ts +1 -0
  3. package/dist/auth/index.d.ts.map +1 -1
  4. package/dist/auth/index.js +2 -0
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/auth/oauth/constants.d.ts +7 -0
  7. package/dist/auth/oauth/constants.d.ts.map +1 -0
  8. package/dist/auth/oauth/constants.js +10 -0
  9. package/dist/auth/oauth/constants.js.map +1 -0
  10. package/dist/auth/oauth/handlers/authorize.d.ts +12 -0
  11. package/dist/auth/oauth/handlers/authorize.d.ts.map +1 -0
  12. package/dist/auth/oauth/handlers/authorize.js +89 -0
  13. package/dist/auth/oauth/handlers/authorize.js.map +1 -0
  14. package/dist/auth/oauth/handlers/callback.d.ts +13 -0
  15. package/dist/auth/oauth/handlers/callback.d.ts.map +1 -0
  16. package/dist/auth/oauth/handlers/callback.js +72 -0
  17. package/dist/auth/oauth/handlers/callback.js.map +1 -0
  18. package/dist/auth/oauth/handlers/index.d.ts +5 -0
  19. package/dist/auth/oauth/handlers/index.d.ts.map +1 -0
  20. package/dist/auth/oauth/handlers/index.js +8 -0
  21. package/dist/auth/oauth/handlers/index.js.map +1 -0
  22. package/dist/auth/oauth/handlers/register.d.ts +9 -0
  23. package/dist/auth/oauth/handlers/register.d.ts.map +1 -0
  24. package/dist/auth/oauth/handlers/register.js +62 -0
  25. package/dist/auth/oauth/handlers/register.js.map +1 -0
  26. package/dist/auth/oauth/handlers/token.d.ts +10 -0
  27. package/dist/auth/oauth/handlers/token.d.ts.map +1 -0
  28. package/dist/auth/oauth/handlers/token.js +139 -0
  29. package/dist/auth/oauth/handlers/token.js.map +1 -0
  30. package/dist/auth/oauth/index.d.ts +4 -0
  31. package/dist/auth/oauth/index.d.ts.map +1 -0
  32. package/dist/auth/oauth/index.js +8 -0
  33. package/dist/auth/oauth/index.js.map +1 -0
  34. package/dist/auth/oauth/testing/index.d.ts +2 -0
  35. package/dist/auth/oauth/testing/index.d.ts.map +1 -0
  36. package/dist/auth/oauth/testing/index.js +5 -0
  37. package/dist/auth/oauth/testing/index.js.map +1 -0
  38. package/dist/auth/oauth/testing/mock-stores.d.ts +36 -0
  39. package/dist/auth/oauth/testing/mock-stores.d.ts.map +1 -0
  40. package/dist/auth/oauth/testing/mock-stores.js +218 -0
  41. package/dist/auth/oauth/testing/mock-stores.js.map +1 -0
  42. package/dist/auth/oauth/types.d.ts +187 -0
  43. package/dist/auth/oauth/types.d.ts.map +1 -0
  44. package/dist/auth/oauth/types.js +5 -0
  45. package/dist/auth/oauth/types.js.map +1 -0
  46. package/package.json +12 -10
package/README.md CHANGED
@@ -20,7 +20,319 @@ Peer dependencies:
20
20
  pnpm add @modelcontextprotocol/sdk zod
21
21
  ```
22
22
 
23
- ## Quick Start
23
+ ## Full Integration Guide
24
+
25
+ This guide covers the complete setup for integrating MCP with OAuth 2.1 authentication in a Next.js/Hono app.
26
+
27
+ ### Architecture Overview
28
+
29
+ ```
30
+ ┌─────────────────┐ ┌──────────────────────────────────────┐
31
+ │ MCP Client │ │ Your App │
32
+ │ (Claude Code, │ │ ┌─────────────────────────────────┐ │
33
+ │ Cursor, etc.) │────▶│ │ /.well-known/oauth-auth-server │ │ OAuth Discovery
34
+ │ │ │ └─────────────────────────────────┘ │
35
+ │ │ │ │
36
+ │ │────▶│ /api/mcp/register (Client Reg) │
37
+ │ │────▶│ /api/mcp/authorize (Auth Start) │
38
+ │ │────▶│ /api/mcp/callback (Auth Complete) │
39
+ │ │────▶│ /api/mcp/token (Token Exchange) │
40
+ │ │ │ │
41
+ │ │────▶│ /api/mcp (MCP Endpoint) │
42
+ └─────────────────┘ └──────────────────────────────────────┘
43
+ ```
44
+
45
+ ### Step 1: OAuth Discovery Endpoint
46
+
47
+ Create `/.well-known/oauth-authorization-server` to expose OAuth metadata.
48
+
49
+ **CRITICAL**: This endpoint must NOT be protected by authentication.
50
+
51
+ ```typescript
52
+ // src/app/.well-known/oauth-authorization-server/route.ts
53
+ import { getOAuthMetadata } from "@nocoo/base-mcp/auth";
54
+ import { NextResponse } from "next/server";
55
+
56
+ export function GET() {
57
+ // Use environment variable or derive from request
58
+ const issuer = process.env.AUTH_URL ?? "http://localhost:3000";
59
+ const metadata = getOAuthMetadata(issuer);
60
+
61
+ return NextResponse.json(metadata, {
62
+ headers: { "Cache-Control": "public, max-age=3600" },
63
+ });
64
+ }
65
+ ```
66
+
67
+ The metadata includes:
68
+ - `authorization_endpoint`: `/api/mcp/authorize`
69
+ - `token_endpoint`: `/api/mcp/token`
70
+ - `registration_endpoint`: `/api/mcp/register`
71
+
72
+ ### Step 2: Middleware Configuration
73
+
74
+ Ensure `/.well-known/` is publicly accessible without authentication:
75
+
76
+ ```typescript
77
+ // middleware.ts or proxy-logic.ts
78
+ const PUBLIC_PATHS = [
79
+ "/login",
80
+ "/terms",
81
+ "/privacy",
82
+ "/.well-known/", // OAuth metadata - must be public
83
+ ];
84
+
85
+ function isPublicPath(pathname: string): boolean {
86
+ return PUBLIC_PATHS.some(p => pathname === p || pathname.startsWith(p));
87
+ }
88
+ ```
89
+
90
+ ### Step 3: OAuth Endpoints
91
+
92
+ #### 3.1 Dynamic Client Registration (`/api/mcp/register`)
93
+
94
+ ```typescript
95
+ // src/app/api/mcp/register/route.ts
96
+ import { isLoopbackRedirectUri } from "@nocoo/base-mcp/auth";
97
+ import { createMcpClient } from "@/data/mcp-clients"; // Your data layer
98
+
99
+ export async function POST(request: Request) {
100
+ const body = await request.json();
101
+
102
+ // Validate redirect_uris (loopback only for security)
103
+ for (const uri of body.redirect_uris) {
104
+ if (!isLoopbackRedirectUri(uri)) {
105
+ return errorResponse("Only loopback redirect URIs are allowed");
106
+ }
107
+ }
108
+
109
+ const client = await createMcpClient(db, {
110
+ client_name: body.client_name,
111
+ redirect_uris: body.redirect_uris,
112
+ grant_types: body.grant_types ?? ["authorization_code"],
113
+ });
114
+
115
+ return jsonResponse({
116
+ client_id: client.client_id,
117
+ client_name: client.client_name,
118
+ redirect_uris: JSON.parse(client.redirect_uris),
119
+ grant_types: JSON.parse(client.grant_types),
120
+ token_endpoint_auth_method: "none",
121
+ }, 201);
122
+ }
123
+ ```
124
+
125
+ #### 3.2 Authorization (`/api/mcp/authorize`)
126
+
127
+ ```typescript
128
+ // src/app/api/mcp/authorize/route.ts
129
+ export async function GET(request: Request) {
130
+ const url = new URL(request.url);
131
+ const params = url.searchParams;
132
+
133
+ // Required OAuth params
134
+ const responseType = params.get("response_type"); // Must be "code"
135
+ const clientId = params.get("client_id");
136
+ const redirectUri = params.get("redirect_uri");
137
+ const codeChallenge = params.get("code_challenge");
138
+ const codeChallengeMethod = params.get("code_challenge_method"); // Must be "S256"
139
+ const state = params.get("state");
140
+
141
+ // Store auth session keyed by state
142
+ await createAuthSession(db, {
143
+ state,
144
+ client_id: clientId,
145
+ redirect_uri: redirectUri,
146
+ code_challenge: codeChallenge,
147
+ code_challenge_method: codeChallengeMethod,
148
+ scope,
149
+ expires_at: now + AUTH_CODE_TTL,
150
+ });
151
+
152
+ // Check if user already authenticated
153
+ const session = await auth();
154
+ if (session?.user?.email) {
155
+ return NextResponse.redirect(`/api/mcp/callback?state=${state}`);
156
+ }
157
+
158
+ // Redirect to login
159
+ return NextResponse.redirect(`/login?callbackUrl=/api/mcp/callback?state=${state}`);
160
+ }
161
+ ```
162
+
163
+ #### 3.3 Callback (`/api/mcp/callback`)
164
+
165
+ ```typescript
166
+ // src/app/api/mcp/callback/route.ts
167
+ export async function GET(request: Request) {
168
+ const state = new URL(request.url).searchParams.get("state");
169
+
170
+ // Verify user is authenticated
171
+ const session = await auth();
172
+ if (!session?.user?.email) {
173
+ return errorResponse("Authentication required", 401);
174
+ }
175
+
176
+ // Look up auth session
177
+ const authSession = await getAuthSessionByState(db, state);
178
+ if (!authSession) {
179
+ return errorResponse("Invalid or expired session");
180
+ }
181
+
182
+ // Generate authorization code
183
+ const code = randomBytes(32).toString("hex");
184
+ await upgradeAuthSession(db, state, code, session.user.email);
185
+
186
+ // Redirect to client's redirect_uri
187
+ const redirectUrl = new URL(authSession.redirect_uri);
188
+ redirectUrl.searchParams.set("code", code);
189
+ redirectUrl.searchParams.set("state", state);
190
+
191
+ return NextResponse.redirect(redirectUrl.toString());
192
+ }
193
+ ```
194
+
195
+ #### 3.4 Token Exchange (`/api/mcp/token`)
196
+
197
+ ```typescript
198
+ // src/app/api/mcp/token/route.ts
199
+ import { verifyPkceS256 } from "@nocoo/base-mcp/auth";
200
+
201
+ export async function POST(request: Request) {
202
+ const body = await request.formData();
203
+ const grantType = body.get("grant_type");
204
+
205
+ if (grantType === "authorization_code") {
206
+ const code = body.get("code");
207
+ const codeVerifier = body.get("code_verifier");
208
+
209
+ // Look up and validate auth code
210
+ const authCode = await getAuthCodeByCode(db, code);
211
+
212
+ // Verify PKCE
213
+ const pkceValid = await verifyPkceS256(codeVerifier, authCode.code_challenge);
214
+ if (!pkceValid) {
215
+ return oauthError("invalid_grant", "PKCE verification failed");
216
+ }
217
+
218
+ // Issue tokens
219
+ return jsonResponse({
220
+ access_token: accessToken,
221
+ token_type: "Bearer",
222
+ expires_in: ACCESS_TOKEN_TTL,
223
+ refresh_token: refreshToken,
224
+ scope,
225
+ });
226
+ }
227
+
228
+ if (grantType === "refresh_token") {
229
+ // Handle refresh token exchange
230
+ }
231
+ }
232
+ ```
233
+
234
+ ### Step 4: MCP Endpoint
235
+
236
+ ```typescript
237
+ // src/app/api/mcp/route.ts
238
+ import { validateMcpToken, validateOrigin } from "@nocoo/base-mcp/auth";
239
+
240
+ export async function POST(request: Request) {
241
+ const siteUrl = process.env.AUTH_URL ?? "http://localhost:3000";
242
+
243
+ // Step 1: Validate Origin (DNS rebinding protection)
244
+ const origin = request.headers.get("origin");
245
+ const originResult = validateOrigin(origin, siteUrl);
246
+ if (!originResult.valid) {
247
+ return errorResponse(originResult.error, originResult.status);
248
+ }
249
+
250
+ // Step 2: Validate Bearer token
251
+ const authResult = await validateMcpToken(
252
+ db,
253
+ request.headers.get("authorization"),
254
+ );
255
+ if (!authResult.valid) {
256
+ return errorResponse(authResult.error, authResult.status);
257
+ }
258
+
259
+ // Step 3: Create MCP server and handle request
260
+ const server = createMcpServer(db);
261
+ const transport = new WebStandardStreamableHTTPServerTransport({
262
+ enableJsonResponse: true, // Stateless mode
263
+ });
264
+
265
+ await server.connect(transport);
266
+ return transport.handleRequest(request);
267
+ }
268
+ ```
269
+
270
+ ### Step 5: MCP Configuration Page
271
+
272
+ Create a user-facing page to help configure MCP clients:
273
+
274
+ ```typescript
275
+ // src/app/mcp-tokens/mcp-tokens-client.tsx
276
+ "use client";
277
+
278
+ function getMcpUrl(): string {
279
+ if (typeof window === "undefined") {
280
+ return "https://your-app.com/api/mcp";
281
+ }
282
+ // Dynamic URL derivation - no hardcoding
283
+ const { protocol, hostname, port } = window.location;
284
+ let baseUrl = `${protocol}//${hostname}`;
285
+ if (port && port !== "80" && port !== "443") {
286
+ baseUrl += `:${port}`;
287
+ }
288
+ return `${baseUrl}/api/mcp`;
289
+ }
290
+
291
+ export function McpConfigPage() {
292
+ const mcpUrl = useMemo(() => getMcpUrl(), []);
293
+
294
+ const configs = {
295
+ "claude-code": `{
296
+ "mcpServers": {
297
+ "your-app": {
298
+ "type": "http",
299
+ "url": "${mcpUrl}"
300
+ }
301
+ }
302
+ }`,
303
+ "claude-desktop": `{
304
+ "mcpServers": {
305
+ "your-app": {
306
+ "command": "npx",
307
+ "args": ["-y", "mcp-remote", "${mcpUrl}"]
308
+ }
309
+ }
310
+ }`,
311
+ };
312
+
313
+ // Render configuration tabs...
314
+ }
315
+ ```
316
+
317
+ **Key principles:**
318
+ - Never hardcode domains - derive from `window.location`
319
+ - Use `type: "http"` for Claude Code (supports Streamable HTTP natively)
320
+ - Use `mcp-remote` bridge for Claude Desktop (requires stdio)
321
+
322
+ ### Summary Checklist
323
+
324
+ - [ ] `/.well-known/oauth-authorization-server` returns JSON without auth
325
+ - [ ] Middleware allows `/.well-known/` paths through without login
326
+ - [ ] `/api/mcp/register` validates loopback redirect URIs
327
+ - [ ] `/api/mcp/authorize` stores auth session and redirects to login
328
+ - [ ] `/api/mcp/callback` generates auth code after login
329
+ - [ ] `/api/mcp/token` verifies PKCE and issues tokens
330
+ - [ ] `/api/mcp` validates origin and bearer token
331
+ - [ ] MCP config page derives URL dynamically (no hardcoding)
332
+
333
+ ---
334
+
335
+ ## Quick Start (Entity-Driven CRUD)
24
336
 
25
337
  ```typescript
26
338
  import { createMcpServer, registerEntityTools, ok, error } from "@nocoo/base-mcp";
@@ -57,7 +369,7 @@ const ctx = { repos: createRepos(db) };
57
369
  registerEntityTools(server, productEntity, ctx);
58
370
 
59
371
  // 4. Handle HTTP requests (Hono/Cloudflare Worker example)
60
- app.post("/mcp", async (c) => {
372
+ app.post("/api/mcp", async (c) => {
61
373
  const transport = new WebStandardStreamableHTTPServerTransport({
62
374
  sessionIdGenerator: undefined, // Stateless
63
375
  enableJsonResponse: true,
@@ -67,7 +379,7 @@ app.post("/mcp", async (c) => {
67
379
  });
68
380
  ```
69
381
 
70
- ## API Reference
382
+ ---
71
383
 
72
384
  ### Server
73
385
 
@@ -2,4 +2,5 @@ export { verifyPkceS256, generateCodeVerifier, generateCodeChallenge, isLoopback
2
2
  export { getOAuthMetadata, type OAuthMetadata, type OAuthMetadataOptions, } from "./oauth-metadata.js";
3
3
  export { validateOrigin, isLoopbackHost, type OriginValidationResult, } from "./origin.js";
4
4
  export { generateToken, hashToken, tokenPreview, extractBearerToken, validateMcpToken, type TokenStore, type TokenValidationResult, type TokenValidationSuccess, type TokenValidationError, } from "./token.js";
5
+ export * from "./oauth/index.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,cAAc,EACd,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,cAAc,EACd,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,YAAY,CAAC;AAGpB,cAAc,kBAAkB,CAAC"}
@@ -3,4 +3,6 @@ export { verifyPkceS256, generateCodeVerifier, generateCodeChallenge, isLoopback
3
3
  export { getOAuthMetadata, } from "./oauth-metadata.js";
4
4
  export { validateOrigin, isLoopbackHost, } from "./origin.js";
5
5
  export { generateToken, hashToken, tokenPreview, extractBearerToken, validateMcpToken, } from "./token.js";
6
+ // OAuth handlers module
7
+ export * from "./oauth/index.js";
6
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,GAGjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,cAAc,GAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,GAKjB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,eAAe;AACf,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,gBAAgB,GAGjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,cAAc,GAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,GAKjB,MAAM,YAAY,CAAC;AAEpB,wBAAwB;AACxB,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** Authorization code TTL in seconds (10 minutes) */
2
+ export declare const AUTH_CODE_TTL: number;
3
+ /** Access token TTL in seconds (30 days) */
4
+ export declare const ACCESS_TOKEN_TTL: number;
5
+ /** Refresh token TTL in seconds (90 days) */
6
+ export declare const REFRESH_TOKEN_TTL: number;
7
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/auth/oauth/constants.ts"],"names":[],"mappings":"AAIA,qDAAqD;AACrD,eAAO,MAAM,aAAa,QAAU,CAAC;AAErC,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,6CAA6C;AAC7C,eAAO,MAAM,iBAAiB,QAAoB,CAAC"}
@@ -0,0 +1,10 @@
1
+ // ---------------------------------------------------------------------------
2
+ // OAuth Constants
3
+ // ---------------------------------------------------------------------------
4
+ /** Authorization code TTL in seconds (10 minutes) */
5
+ export const AUTH_CODE_TTL = 10 * 60;
6
+ /** Access token TTL in seconds (30 days) */
7
+ export const ACCESS_TOKEN_TTL = 30 * 24 * 60 * 60;
8
+ /** Refresh token TTL in seconds (90 days) */
9
+ export const REFRESH_TOKEN_TTL = 90 * 24 * 60 * 60;
10
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/auth/oauth/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,qDAAqD;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,GAAG,EAAE,CAAC;AAErC,4CAA4C;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAElD,6CAA6C;AAC7C,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { OAuthContext, AuthorizeInput, AuthorizeResult } from "../types.js";
2
+ /**
3
+ * Handle OAuth authorization request.
4
+ *
5
+ * This endpoint:
6
+ * 1. Validates all required parameters
7
+ * 2. Verifies client_id and redirect_uri
8
+ * 3. Stores the authorization session
9
+ * 4. Redirects to callback (if already logged in) or login page
10
+ */
11
+ export declare function handleAuthorize(ctx: OAuthContext, input: AuthorizeInput): Promise<AuthorizeResult>;
12
+ //# sourceMappingURL=authorize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/authorize.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGjF;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,YAAY,EACjB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,eAAe,CAAC,CA8F1B"}
@@ -0,0 +1,89 @@
1
+ // ---------------------------------------------------------------------------
2
+ // OAuth Authorize Handler — Authorization Endpoint
3
+ // ---------------------------------------------------------------------------
4
+ import { AUTH_CODE_TTL } from "../constants.js";
5
+ /**
6
+ * Handle OAuth authorization request.
7
+ *
8
+ * This endpoint:
9
+ * 1. Validates all required parameters
10
+ * 2. Verifies client_id and redirect_uri
11
+ * 3. Stores the authorization session
12
+ * 4. Redirects to callback (if already logged in) or login page
13
+ */
14
+ export async function handleAuthorize(ctx, input) {
15
+ const { response_type, client_id, redirect_uri, code_challenge, code_challenge_method, state, scope, } = input;
16
+ // Validate required params
17
+ if (!response_type ||
18
+ !client_id ||
19
+ !redirect_uri ||
20
+ !code_challenge ||
21
+ !code_challenge_method ||
22
+ !state) {
23
+ return {
24
+ action: "error",
25
+ error: "Missing required parameters: response_type, client_id, redirect_uri, code_challenge, code_challenge_method, state",
26
+ status: 400,
27
+ };
28
+ }
29
+ if (response_type !== "code") {
30
+ return {
31
+ action: "error",
32
+ error: "response_type must be 'code'",
33
+ status: 400,
34
+ };
35
+ }
36
+ if (code_challenge_method !== "S256") {
37
+ return {
38
+ action: "error",
39
+ error: "code_challenge_method must be 'S256'",
40
+ status: 400,
41
+ };
42
+ }
43
+ // Verify client exists
44
+ const client = await ctx.clients.findByClientId(client_id);
45
+ if (!client) {
46
+ return {
47
+ action: "error",
48
+ error: "Unknown client_id",
49
+ status: 401,
50
+ };
51
+ }
52
+ // Verify redirect_uri matches registered URIs
53
+ const registeredUris = JSON.parse(client.redirect_uris);
54
+ if (!registeredUris.includes(redirect_uri)) {
55
+ return {
56
+ action: "error",
57
+ error: "redirect_uri does not match any registered URIs",
58
+ status: 400,
59
+ };
60
+ }
61
+ // Store authorization session
62
+ const now = Math.floor(Date.now() / 1000);
63
+ await ctx.authCodes.createSession({
64
+ state,
65
+ client_id,
66
+ redirect_uri,
67
+ code_challenge,
68
+ code_challenge_method,
69
+ scope: scope ?? "mcp:full",
70
+ expires_at: now + AUTH_CODE_TTL,
71
+ });
72
+ // Build callback URL
73
+ const callbackUrl = `${ctx.issuer}/api/mcp/callback?state=${encodeURIComponent(state)}`;
74
+ // If user already authenticated, go straight to callback
75
+ const isAuthenticated = await ctx.auth.isAuthenticated();
76
+ if (isAuthenticated) {
77
+ return {
78
+ action: "redirect_to_callback",
79
+ url: callbackUrl,
80
+ };
81
+ }
82
+ // Otherwise, redirect to login
83
+ const loginUrl = `${ctx.issuer}/login?callbackUrl=${encodeURIComponent(callbackUrl)}`;
84
+ return {
85
+ action: "redirect_to_login",
86
+ loginUrl,
87
+ };
88
+ }
89
+ //# sourceMappingURL=authorize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/authorize.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mDAAmD;AACnD,8EAA8E;AAG9E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAiB,EACjB,KAAqB;IAErB,MAAM,EACJ,aAAa,EACb,SAAS,EACT,YAAY,EACZ,cAAc,EACd,qBAAqB,EACrB,KAAK,EACL,KAAK,GACN,GAAG,KAAK,CAAC;IAEV,2BAA2B;IAC3B,IACE,CAAC,aAAa;QACd,CAAC,SAAS;QACV,CAAC,YAAY;QACb,CAAC,cAAc;QACf,CAAC,qBAAqB;QACtB,CAAC,KAAK,EACN,CAAC;QACD,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EACH,mHAAmH;YACrH,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,IAAI,aAAa,KAAK,MAAM,EAAE,CAAC;QAC7B,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,8BAA8B;YACrC,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,IAAI,qBAAqB,KAAK,MAAM,EAAE,CAAC;QACrC,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,sCAAsC;YAC7C,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,uBAAuB;IACvB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IAC3D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,mBAAmB;YAC1B,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,8CAA8C;IAC9C,MAAM,cAAc,GAAa,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAClE,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QAC3C,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,iDAAiD;YACxD,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,8BAA8B;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC;QAChC,KAAK;QACL,SAAS;QACT,YAAY;QACZ,cAAc;QACd,qBAAqB;QACrB,KAAK,EAAE,KAAK,IAAI,UAAU;QAC1B,UAAU,EAAE,GAAG,GAAG,aAAa;KAChC,CAAC,CAAC;IAEH,qBAAqB;IACrB,MAAM,WAAW,GAAG,GAAG,GAAG,CAAC,MAAM,2BAA2B,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;IAExF,yDAAyD;IACzD,MAAM,eAAe,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;IACzD,IAAI,eAAe,EAAE,CAAC;QACpB,OAAO;YACL,MAAM,EAAE,sBAAsB;YAC9B,GAAG,EAAE,WAAW;SACjB,CAAC;IACJ,CAAC;IAED,+BAA+B;IAC/B,MAAM,QAAQ,GAAG,GAAG,GAAG,CAAC,MAAM,sBAAsB,kBAAkB,CAAC,WAAW,CAAC,EAAE,CAAC;IACtF,OAAO;QACL,MAAM,EAAE,mBAAmB;QAC3B,QAAQ;KACT,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { OAuthContext, CallbackInput, CallbackResult } from "../types.js";
2
+ /**
3
+ * Handle OAuth callback after user authentication.
4
+ *
5
+ * This endpoint:
6
+ * 1. Verifies user is authenticated
7
+ * 2. Verifies email is allowed
8
+ * 3. Generates authorization code
9
+ * 4. Upgrades the session with the code
10
+ * 5. Redirects to client's redirect_uri
11
+ */
12
+ export declare function handleCallback(ctx: OAuthContext, input: CallbackInput): Promise<CallbackResult>;
13
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/callback.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG/E;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,YAAY,EACjB,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,cAAc,CAAC,CAoEzB"}
@@ -0,0 +1,72 @@
1
+ // ---------------------------------------------------------------------------
2
+ // OAuth Callback Handler — Generate Authorization Code after Login
3
+ // ---------------------------------------------------------------------------
4
+ import { generateToken } from "../../token.js";
5
+ import { AUTH_CODE_TTL } from "../constants.js";
6
+ /**
7
+ * Handle OAuth callback after user authentication.
8
+ *
9
+ * This endpoint:
10
+ * 1. Verifies user is authenticated
11
+ * 2. Verifies email is allowed
12
+ * 3. Generates authorization code
13
+ * 4. Upgrades the session with the code
14
+ * 5. Redirects to client's redirect_uri
15
+ */
16
+ export async function handleCallback(ctx, input) {
17
+ const { state } = input;
18
+ if (!state) {
19
+ return {
20
+ action: "error",
21
+ error: "Missing state parameter",
22
+ status: 400,
23
+ };
24
+ }
25
+ // Verify user is authenticated
26
+ const email = await ctx.auth.getEmail();
27
+ if (!email) {
28
+ return {
29
+ action: "error",
30
+ error: "Authentication required",
31
+ status: 401,
32
+ };
33
+ }
34
+ // Verify email is allowed
35
+ if (!ctx.auth.isEmailAllowed(email)) {
36
+ return {
37
+ action: "error",
38
+ error: "Email not authorized",
39
+ status: 403,
40
+ };
41
+ }
42
+ // Look up authorization session by state
43
+ const session = await ctx.authCodes.findByState(state);
44
+ if (!session) {
45
+ return {
46
+ action: "error",
47
+ error: "Invalid or expired authorization session",
48
+ status: 400,
49
+ };
50
+ }
51
+ // Generate authorization code (64 hex chars = 32 bytes)
52
+ const code = generateToken(32);
53
+ // Upgrade session with code and user email
54
+ const now = Math.floor(Date.now() / 1000);
55
+ const upgraded = await ctx.authCodes.upgrade(state, code, email, now + AUTH_CODE_TTL);
56
+ if (!upgraded) {
57
+ return {
58
+ action: "error",
59
+ error: "Authorization session already used or expired",
60
+ status: 400,
61
+ };
62
+ }
63
+ // Redirect to client
64
+ const redirectUrl = new URL(session.redirect_uri);
65
+ redirectUrl.searchParams.set("code", code);
66
+ redirectUrl.searchParams.set("state", state);
67
+ return {
68
+ action: "redirect_to_client",
69
+ url: redirectUrl.toString(),
70
+ };
71
+ }
72
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/callback.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mEAAmE;AACnE,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAiB,EACjB,KAAoB;IAEpB,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IAExB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,yBAAyB;YAChC,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,+BAA+B;IAC/B,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,yBAAyB;YAChC,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,0BAA0B;IAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,sBAAsB;YAC7B,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,yCAAyC;IACzC,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACvD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,0CAA0C;YACjD,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,MAAM,IAAI,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IAE/B,2CAA2C;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,OAAO,CAC1C,KAAK,EACL,IAAI,EACJ,KAAK,EACL,GAAG,GAAG,aAAa,CACpB,CAAC;IACF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,+CAA+C;YACtD,MAAM,EAAE,GAAG;SACZ,CAAC;IACJ,CAAC;IAED,qBAAqB;IACrB,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAClD,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3C,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAE7C,OAAO;QACL,MAAM,EAAE,oBAAoB;QAC5B,GAAG,EAAE,WAAW,CAAC,QAAQ,EAAE;KAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { handleRegister } from "./register.js";
2
+ export { handleAuthorize } from "./authorize.js";
3
+ export { handleCallback } from "./callback.js";
4
+ export { handleToken } from "./token.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,8 @@
1
+ // ---------------------------------------------------------------------------
2
+ // OAuth Handlers Index — Export all handlers
3
+ // ---------------------------------------------------------------------------
4
+ export { handleRegister } from "./register.js";
5
+ export { handleAuthorize } from "./authorize.js";
6
+ export { handleCallback } from "./callback.js";
7
+ export { handleToken } from "./token.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6CAA6C;AAC7C,8EAA8E;AAE9E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { OAuthContext, RegisterInput, RegisterResult } from "../types.js";
2
+ /**
3
+ * Handle dynamic client registration.
4
+ *
5
+ * Per RFC 7591, clients can register themselves to obtain a client_id.
6
+ * Only loopback redirect URIs are allowed (native CLI apps).
7
+ */
8
+ export declare function handleRegister(ctx: OAuthContext, input: RegisterInput): Promise<RegisterResult>;
9
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../../src/auth/oauth/handlers/register.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE/E;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,YAAY,EACjB,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,cAAc,CAAC,CAqDzB"}