diff --git a/src/index.ts b/src/index.ts index 2f9187f..1f8da0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,8 +28,9 @@ import { createOAuthProvider, handleAuthorize, handleGitHubCallback, - type OAuthEnv, - type GitHubUserProps, + readGitHubProps, + readOAuthHelpers, + writeMcpProps, } from "./oauth.js"; import { handleScheduled } from "./poller.js"; import { handleWebhook } from "./webhook.js"; @@ -127,36 +128,30 @@ const innerHandler: ExportedHandler = { // -- MCP endpoint (OAuth-protected, ctx.props set by OAuthProvider) -- if (url.pathname.startsWith("/mcp")) { - const props = (ctx as unknown as { props: GitHubUserProps }).props; + const props = readGitHubProps(ctx); if (!props?.githubUserId) { return new Response("Unauthorized", { status: 401 }); } // Rewrite ctx.props to McpProps shape expected by RagMcpAgentV2. // Pass the GitHub access token so the agent can make API calls. - (ctx as unknown as { props: { githubUserId: number; githubLogin: string; accessToken: string } }).props = { + writeMcpProps(ctx, { githubUserId: props.githubUserId, githubLogin: props.githubLogin, accessToken: props.githubAccessToken, - }; + }); return mcpHandler.fetch(request, env, ctx); } // -- OAuth authorize (redirect to GitHub) -- if (url.pathname === "/oauth/authorize") { - const oauthHelpers = ( - env as unknown as { OAUTH_PROVIDER: Parameters[2] } - ).OAUTH_PROVIDER; - return handleAuthorize(request, env, oauthHelpers); + return handleAuthorize(request, env, readOAuthHelpers(env)); } // -- OAuth callback (GitHub redirects back here) -- if (url.pathname === "/oauth/callback") { - const oauthHelpers = ( - env as unknown as { OAUTH_PROVIDER: Parameters[2] } - ).OAUTH_PROVIDER; - return handleGitHubCallback(request, env, oauthHelpers); + return handleGitHubCallback(request, env, readOAuthHelpers(env)); } return new Response("Not found", { status: 404 }); @@ -170,13 +165,11 @@ const innerHandler: ExportedHandler = { // OAuthProvider wraps the inner handler, adding OAuth endpoints // and protecting /mcp route with access token validation. // Note: OAuthProvider only wraps fetch. We re-export scheduled separately. -const oauthWrapped = createOAuthProvider( - innerHandler as unknown as ExportedHandler>, -); +const oauthWrapped = createOAuthProvider(innerHandler); export default { fetch: (req: Request, env: Env, ctx: ExecutionContext) => - oauthWrapped.fetch(req, env as unknown as OAuthEnv & Record, ctx), + oauthWrapped.fetch(req, env, ctx), async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise { await handleScheduled(controller, env, ctx); }, diff --git a/src/oauth.ts b/src/oauth.ts index 22af8a1..215bf7c 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -44,6 +44,45 @@ export interface OAuthEnv { GITHUB_CLIENT_SECRET: string; } +/** + * Props consumed by RagMcpAgentV2 on the /mcp route, after the inner handler + * rewrites ctx.props from GitHubUserProps. Distinct from GitHubUserProps so + * the MCP agent does not see GitHub refresh tokens it has no use for. + */ +export interface McpProps { + githubUserId: number; + githubLogin: string; + accessToken: string; +} + +/** + * Read GitHubUserProps from ctx. OAuthProvider sets ctx.props after access + * token validation on protected routes, but the workers-types ExecutionContext + * declaration does not include `props`. Centralize the cast here so call + * sites stay type-safe. + */ +export function readGitHubProps(ctx: ExecutionContext): GitHubUserProps | undefined { + return (ctx as unknown as { props?: GitHubUserProps }).props; +} + +/** + * Replace ctx.props with the McpProps shape RagMcpAgentV2 expects. + * Mirrors readGitHubProps on the write side. + */ +export function writeMcpProps(ctx: ExecutionContext, props: McpProps): void { + (ctx as unknown as { props: McpProps }).props = props; +} + +/** + * Read OAUTH_PROVIDER helper that OAuthProvider injects into env on default + * routes. The library injects OAuthHelpers under a known key but its env + * generic does not declare it, so call sites would otherwise need an ad-hoc + * cast each time. + */ +export function readOAuthHelpers(env: OAuthEnv): OAuthHelpers { + return (env as unknown as OAuthEnv & { OAUTH_PROVIDER: OAuthHelpers }).OAUTH_PROVIDER; +} + /** * Handle the /oauth/authorize endpoint. * @@ -169,26 +208,43 @@ export async function handleGitHubCallback( return Response.redirect(redirectTo, 302); } +/** + * Worker handler shape returned by createOAuthProvider. Hides the library's + * `OAuthEnv & Record` index-signature requirement so + * downstream wiring can stay in terms of the caller's own Env type. + */ +export interface OAuthWrappedHandler { + fetch: (request: Request, env: TEnv, ctx: ExecutionContext) => Promise; +} + /** * Create the OAuthProvider instance. * + * Generic over TEnv so callers wire in their own Worker Env type without + * casting at the call site. All `Record` index-signature + * gymnastics live here, at the single boundary between the caller's Env and + * the OAuthProvider library's generic constraint. + * * The provider wraps the existing Worker default handler, adding OAuth * endpoints and protecting API routes. Non-OAuth routes pass through * to the defaultHandler. * * @param defaultHandler - The existing Worker fetch handler to wrap */ -export function createOAuthProvider( - defaultHandler: ExportedHandler>, -): OAuthProvider> { - return new OAuthProvider>({ +export function createOAuthProvider( + defaultHandler: ExportedHandler, +): OAuthWrappedHandler { + type LibEnv = OAuthEnv & Record; + const libHandler = defaultHandler as unknown as ExportedHandler; + + const provider = new OAuthProvider({ // API routes protected by OAuth access tokens apiRoute: ["/mcp"], // The existing Worker handler serves as both apiHandler and defaultHandler - apiHandler: defaultHandler as ExportedHandler> & - Pick>>, "fetch">, - defaultHandler, + apiHandler: libHandler as ExportedHandler & + Pick>, "fetch">, + defaultHandler: libHandler, // OAuth endpoints authorizeEndpoint: "/oauth/authorize", @@ -212,4 +268,9 @@ export function createOAuthProvider( } }, }); + + return { + fetch: (request, env, ctx) => + provider.fetch(request, env as unknown as LibEnv, ctx), + }; } diff --git a/src/types.ts b/src/types.ts index 940739c..1bb20a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -211,19 +211,22 @@ export interface VectorMetadata { assignee_1?: string; } -/** Env bindings for the Worker */ -export interface Env { +import type { OAuthEnv } from "./oauth.js"; + +/** + * Env bindings for the Worker. + * + * Extends OAuthEnv so OAUTH_KV / GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET are + * inherited; this makes Env structurally compatible with the OAuthProvider + * boundary in oauth.ts without an ad-hoc cast at the wiring site. + */ +export interface Env extends OAuthEnv { MCP_OBJECT: DurableObjectNamespace; ISSUE_STORE: DurableObjectNamespace; - OAUTH_KV: KVNamespace; VECTORIZE: Vectorize; /** D1 database for full-text search (sparse side of hybrid retrieval, BM25 via FTS5) */ DB_FTS: D1Database; AI: Ai; - /** GitHub App OAuth client ID (set via `wrangler secret put`) */ - GITHUB_CLIENT_ID: string; - /** GitHub App OAuth client secret (set via `wrangler secret put`) */ - GITHUB_CLIENT_SECRET: string; /** GitHub personal access token or installation token for API access (set via `wrangler secret put`) */ GITHUB_TOKEN: string; /** Comma-separated list of repos to poll, e.g. "owner/repo1,owner/repo2" */