|
| 1 | +import { zValidator } from '@hono/zod-validator' |
| 2 | +import { Hono } from 'hono' |
| 3 | + |
| 4 | +import { |
| 5 | + AuthQuery, |
| 6 | + AuthRequestSchemaWithExtraParams, |
| 7 | + ValidServers, |
| 8 | +} from '@repo/mcp-common/src/cloudflare-oauth-handler' |
| 9 | +import { McpError } from '@repo/mcp-common/src/mcp-error' |
| 10 | + |
| 11 | +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type |
| 12 | +export const getApp = () => |
| 13 | + new Hono() |
| 14 | + /** |
| 15 | + * OAuth Callback Endpoint |
| 16 | + * |
| 17 | + * This route handles the callback from Cloudflare after user authentication. |
| 18 | + * It then proceeds to redirect the user to a valid server callback path (e.g /workers/observability/callback) |
| 19 | + */ |
| 20 | + .get('/oauth/callback', zValidator('query', AuthQuery), async (c) => { |
| 21 | + try { |
| 22 | + const { state, code, scope } = c.req.valid('query') |
| 23 | + const oauthReqInfo = AuthRequestSchemaWithExtraParams.parse(atob(state)) |
| 24 | + if (!oauthReqInfo.clientId) { |
| 25 | + throw new McpError('Invalid State', 400) |
| 26 | + } |
| 27 | + const params = new URLSearchParams({ |
| 28 | + code, |
| 29 | + state, |
| 30 | + scope, |
| 31 | + }) |
| 32 | + |
| 33 | + if (!ValidServers.safeParse(oauthReqInfo.serverPath).success) { |
| 34 | + throw new McpError(`Invalid server redirect ${oauthReqInfo.serverPath}`, 400) |
| 35 | + } |
| 36 | + |
| 37 | + const redirectUrl = new URL( |
| 38 | + `${new URL(c.req.url).origin}/${oauthReqInfo.serverPath}/oauth/callback?${params.toString()}` |
| 39 | + ) |
| 40 | + return Response.redirect(redirectUrl.toString(), 302) |
| 41 | + } catch (e) { |
| 42 | + console.error(e) |
| 43 | + if (e instanceof McpError) { |
| 44 | + return c.text(e.message, { status: e.code }) |
| 45 | + } |
| 46 | + return c.text('Internal Error', 500) |
| 47 | + } |
| 48 | + }) |
| 49 | + |
| 50 | +export default { |
| 51 | + fetch: getApp().fetch, |
| 52 | +} satisfies ExportedHandler |
0 commit comments