Unified · Split
| @@ -92,6 +92,7 @@ · 91 unmodified lines | |||
| 92 | "@streamdown/code": "^1.0.1", | 92 | "@streamdown/code": "^1.0.1", |
| 93 | "@tanstack/react-query": "^5.80.7", | 93 | "@tanstack/react-query": "^5.80.7", |
| 94 | "@workos-inc/authkit-nextjs": "^2.13.0", | 94 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 95 | "better-result": "^2.7.0", | ||
| 95 | "class-variance-authority": "^0.7.1", | 96 | "class-variance-authority": "^0.7.1", |
| 96 | "clsx": "^2.1.1", | 97 | "clsx": "^2.1.1", |
| 97 | "convex": "^1.31.7", | 98 | "convex": "^1.31.7", |
| @@ -17,6 +17,7 @@ · 16 unmodified lines | |||
| 17 | "@streamdown/code": "^1.0.1", | 17 | "@streamdown/code": "^1.0.1", |
| 18 | "@tanstack/react-query": "^5.80.7", | 18 | "@tanstack/react-query": "^5.80.7", |
| 19 | "@workos-inc/authkit-nextjs": "^2.13.0", | 19 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 20 | "better-result": "^2.7.0", | ||
| 20 | "class-variance-authority": "^0.7.1", | 21 | "class-variance-authority": "^0.7.1", |
| 21 | "clsx": "^2.1.1", | 22 | "clsx": "^2.1.1", |
| 22 | "convex": "^1.31.7", | 23 | "convex": "^1.31.7", |
| @@ -1,5 +1,6 @@ | |||
| 1 | import type { NextRequest } from "next/server"; | 1 | import type { NextRequest } from "next/server"; |
| 2 | import { NextResponse } from "next/server"; | 2 | import { NextResponse } from "next/server"; |
| 3 | import { Result } from "better-result"; | ||
| 3 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; | 4 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | import { | 5 | import { |
| 5 | buildPendingCookieName, | 6 | buildPendingCookieName, |
| @@ -34,6 +35,19 @@ function popupResultRedirect( · 28 unmodified lines | |||
| 34 | return response; | 35 | return response; |
| 35 | } | 36 | } |
| 36 | 37 | ||
| 38 | function resultErrorMessage(error: unknown, fallback: string): string { | ||
| 39 | const cause = typeof error === "object" && error && "cause" in error | ||
| 40 | ? (error as { cause?: unknown }).cause | ||
| 41 | : error; | ||
| 42 | if (cause instanceof Error && cause.message.trim()) { | ||
| 43 | return cause.message; | ||
| 44 | } | ||
| 45 | if (typeof cause === "string" && cause.trim()) { | ||
| 46 | return cause; | ||
| 47 | } | ||
| 48 | return fallback; | ||
| 49 | } | ||
| 50 | |||
| 37 | export async function GET(request: NextRequest) { | 51 | export async function GET(request: NextRequest) { |
| 38 | const code = request.nextUrl.searchParams.get("code")?.trim() ?? ""; | 52 | const code = request.nextUrl.searchParams.get("code")?.trim() ?? ""; |
| 39 | const state = request.nextUrl.searchParams.get("state")?.trim() ?? ""; | 53 | const state = request.nextUrl.searchParams.get("state")?.trim() ?? ""; |
| @@ -65,12 +79,11 @@ export async function GET(request: NextRequest) { · 25 unmodified lines | |||
| 65 | }); | 79 | }); |
| 66 | } | 80 | } |
| 67 | 81 | ||
| 68 | let sourceUrl: URL; | 82 | const sourceUrlResult = Result.try(() => new URL(pending.sourceUrl)); |
| 69 | try { | 83 | if (!sourceUrlResult.isOk()) { |
| 70 | sourceUrl = new URL(pending.sourceUrl); | ||
| 71 | } catch { | ||
| 72 | return popupResultRedirect(request, cookieName, { ok: false, error: "Invalid MCP source URL" }); | 84 | return popupResultRedirect(request, cookieName, { ok: false, error: "Invalid MCP source URL" }); |
| 73 | } | 85 | } |
| 86 | const sourceUrl = sourceUrlResult.value; | ||
| 74 | 87 | ||
| 75 | const provider = new McpPopupOAuthProvider({ | 88 | const provider = new McpPopupOAuthProvider({ |
| 76 | redirectUrl: pending.redirectUrl, | 89 | redirectUrl: pending.redirectUrl, |
| @@ -79,15 +92,14 @@ export async function GET(request: NextRequest) { · 2 unmodified lines | |||
| 79 | clientInformation: pending.clientInformation, | 92 | clientInformation: pending.clientInformation, |
| 80 | }); | 93 | }); |
| 81 | 94 | ||
| 82 | try { | 95 | const authResult = await Result.tryPromise(() => auth(provider, { |
| 83 | await auth(provider, { | ||
| 84 | serverUrl: sourceUrl, | 96 | serverUrl: sourceUrl, |
| 85 | authorizationCode: code, | 97 | authorizationCode: code, |
| 86 | }); | 98 | })); |
| 87 | } catch (finishError) { | 99 | if (!authResult.isOk()) { |
| 88 | return popupResultRedirect(request, cookieName, { | 100 | return popupResultRedirect(request, cookieName, { |
| 89 | ok: false, | 101 | ok: false, |
| 90 | error: finishError instanceof Error ? finishError.message : "Failed to finish OAuth", | 102 | error: resultErrorMessage(authResult.error, "Failed to finish OAuth"), |
| 91 | }); | 103 | }); |
| 92 | } | 104 | } |
| 93 | 105 | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | import type { NextRequest } from "next/server"; | 1 | import type { NextRequest } from "next/server"; |
| 2 | import { NextResponse } from "next/server"; | 2 | import { NextResponse } from "next/server"; |
| 3 | import { Result } from "better-result"; | ||
| 3 | import { discoverOAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; | 4 | import { discoverOAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | 5 | ||
| 5 | type DetectResponse = { | 6 | type DetectResponse = { |
| @@ -17,37 +18,49 @@ function noStoreJson(payload: DetectResponse, status = 200): NextResponse { · 11 unmodified lines | |||
| 17 | }); | 18 | }); |
| 18 | } | 19 | } |
| 19 | 20 | ||
| 21 | function resultErrorMessage(error: unknown, fallback: string): string { | ||
| 22 | const cause = typeof error === "object" && error && "cause" in error | ||
| 23 | ? (error as { cause?: unknown }).cause | ||
| 24 | : error; | ||
| 25 | if (cause instanceof Error && cause.message.trim()) { | ||
| 26 | return cause.message; | ||
| 27 | } | ||
| 28 | if (typeof cause === "string" && cause.trim()) { | ||
| 29 | return cause; | ||
| 30 | } | ||
| 31 | return fallback; | ||
| 32 | } | ||
| 33 | |||
| 20 | export async function GET(request: NextRequest) { | 34 | export async function GET(request: NextRequest) { |
| 21 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; | 35 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; |
| 22 | if (!sourceUrlRaw) { | 36 | if (!sourceUrlRaw) { |
| 23 | return noStoreJson({ oauth: false, authorizationServers: [], detail: "Missing sourceUrl" }, 400); | 37 | return noStoreJson({ oauth: false, authorizationServers: [], detail: "Missing sourceUrl" }, 400); |
| 24 | } | 38 | } |
| 25 | 39 | ||
| 26 | let sourceUrl: URL; | 40 | const sourceUrlResult = Result.try(() => new URL(sourceUrlRaw)); |
| 27 | try { | 41 | if (!sourceUrlResult.isOk()) { |
| 28 | sourceUrl = new URL(sourceUrlRaw); | ||
| 29 | } catch { | ||
| 30 | return noStoreJson({ oauth: false, authorizationServers: [], detail: "Invalid sourceUrl" }, 400); | 42 | return noStoreJson({ oauth: false, authorizationServers: [], detail: "Invalid sourceUrl" }, 400); |
| 31 | } | 43 | } |
| 44 | const sourceUrl = sourceUrlResult.value; | ||
| 32 | 45 | ||
| 33 | try { | 46 | const metadataResult = await Result.tryPromise(() => discoverOAuthProtectedResourceMetadata(sourceUrl)); |
| 34 | const metadata = await discoverOAuthProtectedResourceMetadata(sourceUrl); | 47 | if (!metadataResult.isOk()) { |
| 35 | const authorizationServers = Array.isArray((metadata as { authorization_servers?: unknown }).authorization_servers) | ||
| 36 | ? (metadata as { authorization_servers: unknown[] }).authorization_servers.filter( | ||
| 37 | (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, | ||
| 38 | ) | ||
| 39 | : []; | ||
| 40 | |||
| 41 | return noStoreJson({ | ||
| 42 | oauth: authorizationServers.length > 0, | ||
| 43 | authorizationServers, | ||
| 44 | }); | ||
| 45 | } catch (error) { | ||
| 46 | const message = error instanceof Error ? error.message : "OAuth metadata lookup failed"; | ||
| 47 | return noStoreJson({ | 48 | return noStoreJson({ |
| 48 | oauth: false, | 49 | oauth: false, |
| 49 | authorizationServers: [], | 50 | authorizationServers: [], |
| 50 | detail: message, | 51 | detail: resultErrorMessage(metadataResult.error, "OAuth metadata lookup failed"), |
| 51 | }); | 52 | }); |
| 52 | } | 53 | } |
| 54 | |||
| 55 | const metadata = metadataResult.value; | ||
| 56 | const authorizationServers = Array.isArray((metadata as { authorization_servers?: unknown }).authorization_servers) | ||
| 57 | ? (metadata as { authorization_servers: unknown[] }).authorization_servers.filter( | ||
| 58 | (entry): entry is string => typeof entry === "string" && entry.trim().length > 0, | ||
| 59 | ) | ||
| 60 | : []; | ||
| 61 | |||
| 62 | return noStoreJson({ | ||
| 63 | oauth: authorizationServers.length > 0, | ||
| 64 | authorizationServers, | ||
| 65 | }); | ||
| 53 | } | 66 | } |
| @@ -1,5 +1,6 @@ | |||
| 1 | import type { NextRequest } from "next/server"; | 1 | import type { NextRequest } from "next/server"; |
| 2 | import { NextResponse } from "next/server"; | 2 | import { NextResponse } from "next/server"; |
| 3 | import { Result } from "better-result"; | ||
| 3 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; | 4 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 4 | import { | 5 | import { |
| 5 | buildPendingCookieName, | 6 | buildPendingCookieName, |
| @@ -39,18 +40,30 @@ function badPopupResponse(request: NextRequest, message: string): NextResponse { · 33 unmodified lines | |||
| 39 | return popupResultRedirect(request, { ok: false, error: message }); | 40 | return popupResultRedirect(request, { ok: false, error: message }); |
| 40 | } | 41 | } |
| 41 | 42 | ||
| 43 | function resultErrorMessage(error: unknown, fallback: string): string { | ||
| 44 | const cause = typeof error === "object" && error && "cause" in error | ||
| 45 | ? (error as { cause?: unknown }).cause | ||
| 46 | : error; | ||
| 47 | if (cause instanceof Error && cause.message.trim()) { | ||
| 48 | return cause.message; | ||
| 49 | } | ||
| 50 | if (typeof cause === "string" && cause.trim()) { | ||
| 51 | return cause; | ||
| 52 | } | ||
| 53 | return fallback; | ||
| 54 | } | ||
| 55 | |||
| 42 | export async function GET(request: NextRequest) { | 56 | export async function GET(request: NextRequest) { |
| 43 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; | 57 | const sourceUrlRaw = request.nextUrl.searchParams.get("sourceUrl")?.trim() ?? ""; |
| 44 | if (!sourceUrlRaw) { | 58 | if (!sourceUrlRaw) { |
| 45 | return badPopupResponse(request, "Missing sourceUrl"); | 59 | return badPopupResponse(request, "Missing sourceUrl"); |
| 46 | } | 60 | } |
| 47 | 61 | ||
| 48 | let sourceUrl: URL; | 62 | const sourceUrlResult = Result.try(() => new URL(sourceUrlRaw)); |
| 49 | try { | 63 | if (!sourceUrlResult.isOk()) { |
| 50 | sourceUrl = new URL(sourceUrlRaw); | ||
| 51 | } catch { | ||
| 52 | return badPopupResponse(request, "Invalid sourceUrl"); | 64 | return badPopupResponse(request, "Invalid sourceUrl"); |
| 53 | } | 65 | } |
| 66 | const sourceUrl = sourceUrlResult.value; | ||
| 54 | 67 | ||
| 55 | const state = createOAuthState(); | 68 | const state = createOAuthState(); |
| 56 | const redirectUrl = `${getExternalOrigin(request)}/mcp/oauth/callback`; | 69 | const redirectUrl = `${getExternalOrigin(request)}/mcp/oauth/callback`; |
| @@ -59,15 +72,12 @@ export async function GET(request: NextRequest) { · 2 unmodified lines | |||
| 59 | state, | 72 | state, |
| 60 | }); | 73 | }); |
| 61 | 74 | ||
| 62 | let authResult: "AUTHORIZED" | "REDIRECT"; | 75 | const authResult = await Result.tryPromise(() => auth(provider, { serverUrl: sourceUrl })); |
| 63 | try { | 76 | if (!authResult.isOk()) { |
| 64 | authResult = await auth(provider, { serverUrl: sourceUrl }); | 77 | return badPopupResponse(request, resultErrorMessage(authResult.error, "Failed to start OAuth flow")); |
| 65 | } catch (error) { | ||
| 66 | const message = error instanceof Error ? error.message : "Failed to start OAuth flow"; | ||
| 67 | return badPopupResponse(request, message); | ||
| 68 | } | 78 | } |
| 69 | 79 | ||
| 70 | if (authResult === "AUTHORIZED") { | 80 | if (authResult.value === "AUTHORIZED") { |
| 71 | const tokens = provider.getTokens(); | 81 | const tokens = provider.getTokens(); |
| 72 | const accessToken = tokens?.access_token?.trim() ?? ""; | 82 | const accessToken = tokens?.access_token?.trim() ?? ""; |
| 73 | if (!accessToken) { | 83 | if (!accessToken) { |
| @@ -3,6 +3,7 @@ · 2 unmodified lines | |||
| 3 | import { useState, type ReactNode } from "react"; | 3 | import { useState, type ReactNode } from "react"; |
| 4 | import { Plus } from "lucide-react"; | 4 | import { Plus } from "lucide-react"; |
| 5 | import { useAction, useMutation, useQuery } from "convex/react"; | 5 | import { useAction, useMutation, useQuery } from "convex/react"; |
| 6 | import { Result } from "better-result"; | ||
| 6 | import { toast } from "sonner"; | 7 | import { toast } from "sonner"; |
| 7 | import { Button } from "@/components/ui/button"; | 8 | import { Button } from "@/components/ui/button"; |
| 8 | import { | 9 | import { |
| @@ -28,6 +29,19 @@ import { · 19 unmodified lines | |||
| 28 | } from "./use-add-source-form-state"; | 29 | } from "./use-add-source-form-state"; |
| 29 | import { saveSourceWithCredentials } from "./add-source-submit"; | 30 | import { saveSourceWithCredentials } from "./add-source-submit"; |
| 30 | 31 | ||
| 32 | function resultErrorMessage(error: unknown, fallback: string): string { | ||
| 33 | const cause = typeof error === "object" && error && "cause" in error | ||
| 34 | ? (error as { cause?: unknown }).cause | ||
| 35 | : error; | ||
| 36 | if (cause instanceof Error && cause.message.trim()) { | ||
| 37 | return cause.message; | ||
| 38 | } | ||
| 39 | if (typeof cause === "string" && cause.trim()) { | ||
| 40 | return cause; | ||
| 41 | } | ||
| 42 | return fallback; | ||
| 43 | } | ||
| 44 | |||
| 31 | export function AddSourceDialog({ | 45 | export function AddSourceDialog({ |
| 32 | existingSourceNames, | 46 | existingSourceNames, |
| 33 | onSourceAdded, | 47 | onSourceAdded, |
| @@ -87,8 +101,8 @@ export function AddSourceDialog({ · 53 unmodified lines | |||
| 87 | } | 101 | } |
| 88 | 102 | ||
| 89 | setSubmitting(true); | 103 | setSubmitting(true); |
| 90 | try { | 104 | const saveResult = await Result.tryPromise(() => |
| 91 | const result = await saveSourceWithCredentials({ | 105 | saveSourceWithCredentials({ |
| 92 | context, | 106 | context, |
| 93 | sourceToEdit, | 107 | sourceToEdit, |
| 94 | credentialsLoading, | 108 | credentialsLoading, |
| @@ -108,28 +122,34 @@ export function AddSourceDialog({ · 13 unmodified lines | |||
| 108 | hasCredentialInput: form.hasCredentialInput, | 122 | hasCredentialInput: form.hasCredentialInput, |
| 109 | buildSecretJson: form.buildSecretJson, | 123 | buildSecretJson: form.buildSecretJson, |
| 110 | }, | 124 | }, |
| 111 | }); | 125 | }) |
| 112 | 126 | ); | |
| 113 | onSourceAdded?.(result.source, { connected: result.connected }); | 127 | setSubmitting(false); |
| 114 | toast.success( | 128 | |
| 115 | sourceToEdit | 129 | if (!saveResult.isOk()) { |
| 116 | ? result.connected | 130 | toast.error(resultErrorMessage( |
| 117 | ? `Source "${form.name.trim()}" updated with credentials` | 131 | saveResult.error, |
| 118 | : `Source "${form.name.trim()}" updated` | 132 | sourceToEdit ? "Failed to update source" : "Failed to add source", |
| 119 | : result.connected | 133 | )); |
| 120 | ? `Source "${form.name.trim()}" added with credentials — loading tools…` | 134 | return; |
| 121 | : `Source "${form.name.trim()}" added — loading tools…`, | 135 | } |
| 122 | ); | 136 | |
| 123 | if (!sourceToEdit) { | 137 | const result = saveResult.value; |
| 124 | form.reserveSourceName(form.name.trim()); | 138 | onSourceAdded?.(result.source, { connected: result.connected }); |
| 125 | } | 139 | toast.success( |
| 126 | form.setView("catalog"); | 140 | sourceToEdit |
| 127 | setOpen(false); | 141 | ? result.connected |
| 128 | } catch (err) { | 142 | ? `Source "${form.name.trim()}" updated with credentials` |
| 129 | toast.error(err instanceof Error ? err.message : sourceToEdit ? "Failed to update source" : "Failed to add source"); | 143 | : `Source "${form.name.trim()}" updated` |
| 130 | } finally { | 144 | : result.connected |
| 131 | setSubmitting(false); | 145 | ? `Source "${form.name.trim()}" added with credentials — loading tools…` |
| 146 | : `Source "${form.name.trim()}" added — loading tools…`, | ||
| 147 | ); | ||
| 148 | if (!sourceToEdit) { | ||
| 149 | form.reserveSourceName(form.name.trim()); | ||
| 132 | } | 150 | } |
| 151 | form.setView("catalog"); | ||
| 152 | setOpen(false); | ||
| 133 | }; | 153 | }; |
| 134 | 154 | ||
| 135 | const handleMcpOAuthConnect = async () => { | 155 | const handleMcpOAuthConnect = async () => { |
| @@ -143,18 +163,19 @@ export function AddSourceDialog({ · 7 unmodified lines | |||
| 143 | } | 163 | } |
| 144 | 164 | ||
| 145 | setMcpOAuthBusy(true); | 165 | setMcpOAuthBusy(true); |
| 146 | try { | 166 | const oauthResult = await Result.tryPromise(() => startMcpOAuthPopup(endpoint)); |
| 147 | const result = await startMcpOAuthPopup(endpoint); | 167 | setMcpOAuthBusy(false); |
| 148 | if (form.authType !== "bearer") { | 168 | |
| 149 | form.handleAuthTypeChange("bearer"); | 169 | if (!oauthResult.isOk()) { |
| 150 | } | 170 | toast.error(resultErrorMessage(oauthResult.error, "Failed to connect OAuth")); |
| 151 | form.handleAuthFieldChange("tokenValue", result.accessToken); | 171 | return; |
| 152 | toast.success("OAuth linked successfully."); | 172 | } |
| 153 | } catch (error) { | 173 | |
| 154 | toast.error(error instanceof Error ? error.message : "Failed to connect OAuth"); | 174 | if (form.authType !== "bearer") { |
| 155 | } finally { | 175 | form.handleAuthTypeChange("bearer"); |
| 156 | setMcpOAuthBusy(false); | ||
| 157 | } | 176 | } |
| 177 | form.handleAuthFieldChange("tokenValue", oauthResult.value.accessToken); | ||
| 178 | toast.success("OAuth linked successfully."); | ||
| 158 | }; | 179 | }; |
| 159 | 180 | ||
| 160 | const dialogTitle = sourceToEdit ? "Edit Tool Source" : "Add Tool Source"; | 181 | const dialogTitle = sourceToEdit ? "Edit Tool Source" : "Add Tool Source"; |
| @@ -53,10 +53,8 @@ export async function startMcpOAuthPopup(sourceUrl: string): Promise<McpOAuthPop · 52 unmodified lines | |||
| 53 | const cleanup = () => { | 53 | const cleanup = () => { |
| 54 | window.removeEventListener("message", onMessage); | 54 | window.removeEventListener("message", onMessage); |
| 55 | window.clearInterval(closedPoll); | 55 | window.clearInterval(closedPoll); |
| 56 | try { | 56 | if (!popup.closed) { |
| 57 | popup.close(); | 57 | popup.close(); |
| 58 | } catch { | ||
| 59 | // ignore | ||
| 60 | } | 58 | } |
| 61 | }; | 59 | }; |
| 62 | 60 | ||
| @@ -1,4 +1,5 @@ | |||
| 1 | import { randomUUID } from "node:crypto"; | 1 | import { randomUUID } from "node:crypto"; |
| 2 | import { Result } from "better-result"; | ||
| 2 | import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; | 3 | import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; |
| 3 | import type { | 4 | import type { |
| 4 | OAuthClientInformationMixed, | 5 | OAuthClientInformationMixed, |
| @@ -40,27 +41,32 @@ export function encodePendingCookieValue(pending: McpOAuthPending): string { · 35 unmodified lines | |||
| 40 | } | 41 | } |
| 41 | 42 | ||
| 42 | export function decodePendingCookieValue(raw: string): McpOAuthPending | null { | 43 | export function decodePendingCookieValue(raw: string): McpOAuthPending | null { |
| 43 | try { | 44 | const decodedResult = Result.try(() => Buffer.from(raw, "base64url").toString("utf8")); |
| 44 | const decoded = Buffer.from(raw, "base64url").toString("utf8"); | 45 | if (!decodedResult.isOk()) { |
| 45 | const parsed = JSON.parse(decoded) as Partial<McpOAuthPending>; | 46 | return null; |
| 46 | if ( | 47 | } |
| 47 | typeof parsed.state !== "string" | ||
| 48 | || typeof parsed.sourceUrl !== "string" | ||
| 49 | || typeof parsed.redirectUrl !== "string" | ||
| 50 | ) { | ||
| 51 | return null; | ||
| 52 | } | ||
| 53 | 48 | ||
| 54 | return { | 49 | const parsedResult = Result.try(() => JSON.parse(decodedResult.value) as Partial<McpOAuthPending>); |
| 55 | state: parsed.state, | 50 | if (!parsedResult.isOk()) { |
| 56 | sourceUrl: parsed.sourceUrl, | ||
| 57 | redirectUrl: parsed.redirectUrl, | ||
| 58 | ...(typeof parsed.codeVerifier === "string" ? { codeVerifier: parsed.codeVerifier } : {}), | ||
| 59 | ...(parsed.clientInformation ? { clientInformation: parsed.clientInformation } : {}), | ||
| 60 | }; | ||
| 61 | } catch { | ||
| 62 | return null; | 51 | return null; |
| 63 | } | 52 | } |
| 53 | |||
| 54 | const parsed = parsedResult.value; | ||
| 55 | if ( | ||
| 56 | typeof parsed.state !== "string" | ||
| 57 | || typeof parsed.sourceUrl !== "string" | ||
| 58 | || typeof parsed.redirectUrl !== "string" | ||
| 59 | ) { | ||
| 60 | return null; | ||
| 61 | } | ||
| 62 | |||
| 63 | return { | ||
| 64 | state: parsed.state, | ||
| 65 | sourceUrl: parsed.sourceUrl, | ||
| 66 | redirectUrl: parsed.redirectUrl, | ||
| 67 | ...(typeof parsed.codeVerifier === "string" ? { codeVerifier: parsed.codeVerifier } : {}), | ||
| 68 | ...(parsed.clientInformation ? { clientInformation: parsed.clientInformation } : {}), | ||
| 69 | }; | ||
| 64 | } | 70 | } |
| 65 | 71 | ||
| 66 | export function encodePopupResultCookieValue(result: McpOAuthPopupResult): string { | 72 | export function encodePopupResultCookieValue(result: McpOAuthPopupResult): string { |
| @@ -68,25 +74,30 @@ export function encodePopupResultCookieValue(result: McpOAuthPopupResult): strin · 1 unmodified line | |||
| 68 | } | 74 | } |
| 69 | 75 | ||
| 70 | export function decodePopupResultCookieValue(raw: string): McpOAuthPopupResult | null { | 76 | export function decodePopupResultCookieValue(raw: string): McpOAuthPopupResult | null { |
| 71 | try { | 77 | const decodedResult = Result.try(() => Buffer.from(raw, "base64url").toString("utf8")); |
| 72 | const decoded = Buffer.from(raw, "base64url").toString("utf8"); | 78 | if (!decodedResult.isOk()) { |
| 73 | const parsed = JSON.parse(decoded) as Partial<McpOAuthPopupResult>; | 79 | return null; |
| 74 | if (typeof parsed.ok !== "boolean") { | 80 | } |
| 75 | return null; | ||
| 76 | } | ||
| 77 | 81 | ||
| 78 | return { | 82 | const parsedResult = Result.try(() => JSON.parse(decodedResult.value) as Partial<McpOAuthPopupResult>); |
| 79 | ok: parsed.ok, | 83 | if (!parsedResult.isOk()) { |
| 80 | ...(typeof parsed.sourceUrl === "string" ? { sourceUrl: parsed.sourceUrl } : {}), | ||
| 81 | ...(typeof parsed.accessToken === "string" ? { accessToken: parsed.accessToken } : {}), | ||
| 82 | ...(typeof parsed.refreshToken === "string" ? { refreshToken: parsed.refreshToken } : {}), | ||
| 83 | ...(typeof parsed.scope === "string" ? { scope: parsed.scope } : {}), | ||
| 84 | ...(typeof parsed.expiresIn === "number" ? { expiresIn: parsed.expiresIn } : {}), | ||
| 85 | ...(typeof parsed.error === "string" ? { error: parsed.error } : {}), | ||
| 86 | }; | ||
| 87 | } catch { | ||
| 88 | return null; | 84 | return null; |
| 89 | } | 85 | } |
| 86 | |||
| 87 | const parsed = parsedResult.value; | ||
| 88 | if (typeof parsed.ok !== "boolean") { | ||
| 89 | return null; | ||
| 90 | } | ||
| 91 | |||
| 92 | return { | ||
| 93 | ok: parsed.ok, | ||
| 94 | ...(typeof parsed.sourceUrl === "string" ? { sourceUrl: parsed.sourceUrl } : {}), | ||
| 95 | ...(typeof parsed.accessToken === "string" ? { accessToken: parsed.accessToken } : {}), | ||
| 96 | ...(typeof parsed.refreshToken === "string" ? { refreshToken: parsed.refreshToken } : {}), | ||
| 97 | ...(typeof parsed.scope === "string" ? { scope: parsed.scope } : {}), | ||
| 98 | ...(typeof parsed.expiresIn === "number" ? { expiresIn: parsed.expiresIn } : {}), | ||
| 99 | ...(typeof parsed.error === "string" ? { error: parsed.error } : {}), | ||
| 100 | }; | ||
| 90 | } | 101 | } |
| 91 | 102 | ||
| 92 | export class McpPopupOAuthProvider implements OAuthClientProvider { | 103 | export class McpPopupOAuthProvider implements OAuthClientProvider { |