mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-10 03:28:14 +00:00
Fix MCP auth timeouts and return retryable 504 on auth stalls.
Add shared FETCH_TIMEOUT_MS, timeout both API-key and OAuth auth paths, disable SDK retries for a true 30s bound, and distinguish auth timeouts from invalid credentials so clients get 504 instead of 401.
This commit is contained in:
parent
3d5579a006
commit
790f989d2f
4 changed files with 81 additions and 36 deletions
|
|
@ -4,6 +4,8 @@
|
|||
* This validates OAuth tokens and API keys by calling the main Supermemory API,
|
||||
*/
|
||||
|
||||
import { FETCH_TIMEOUT_MS } from "./constants"
|
||||
|
||||
export interface AuthUser {
|
||||
userId: string
|
||||
apiKey: string
|
||||
|
|
@ -11,6 +13,18 @@ export interface AuthUser {
|
|||
name?: string
|
||||
}
|
||||
|
||||
export type AuthValidationResult =
|
||||
| { status: "success"; user: AuthUser }
|
||||
| { status: "invalid" }
|
||||
| { status: "timeout" }
|
||||
|
||||
function isFetchTimeout(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
(error.name === "AbortError" || error.name === "TimeoutError")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is an API key (starts with "sm_")
|
||||
*/
|
||||
|
|
@ -25,13 +39,14 @@ export function isApiKey(token: string): boolean {
|
|||
export async function validateApiKey(
|
||||
apiKey: string,
|
||||
apiUrl: string,
|
||||
): Promise<AuthUser | null> {
|
||||
): Promise<AuthValidationResult> {
|
||||
try {
|
||||
const sessionResponse = await fetch(`${apiUrl}/v3/session`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!sessionResponse.ok) {
|
||||
|
|
@ -56,7 +71,7 @@ export async function validateApiKey(
|
|||
} else {
|
||||
console.error("API key validation failed:", status, responseText)
|
||||
}
|
||||
return null
|
||||
return { status: "invalid" }
|
||||
}
|
||||
|
||||
const sessionData = (await sessionResponse.json()) as {
|
||||
|
|
@ -72,20 +87,27 @@ export async function validateApiKey(
|
|||
|
||||
if (!sessionData?.user?.id) {
|
||||
console.error("Missing user.id in session response:", sessionData)
|
||||
return null
|
||||
return { status: "invalid" }
|
||||
}
|
||||
|
||||
console.log("API key validated for user:", sessionData.user.id)
|
||||
|
||||
return {
|
||||
userId: sessionData.user.id,
|
||||
apiKey: apiKey,
|
||||
email: sessionData.user.email,
|
||||
name: sessionData.user.name,
|
||||
status: "success",
|
||||
user: {
|
||||
userId: sessionData.user.id,
|
||||
apiKey: apiKey,
|
||||
email: sessionData.user.email,
|
||||
name: sessionData.user.name,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
if (isFetchTimeout(error)) {
|
||||
console.error("API key validation timed out")
|
||||
return { status: "timeout" }
|
||||
}
|
||||
console.error("API key validation error:", error)
|
||||
return null
|
||||
return { status: "invalid" }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,14 +118,14 @@ export async function validateApiKey(
|
|||
export async function validateOAuthToken(
|
||||
token: string,
|
||||
apiUrl: string,
|
||||
): Promise<AuthUser | null> {
|
||||
): Promise<AuthValidationResult> {
|
||||
try {
|
||||
const sessionResponse = await fetch(`${apiUrl}/v3/mcp/session-with-key`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!sessionResponse.ok) {
|
||||
|
|
@ -128,7 +150,7 @@ export async function validateOAuthToken(
|
|||
} else {
|
||||
console.error("Token validation failed:", status, responseText)
|
||||
}
|
||||
return null
|
||||
return { status: "invalid" }
|
||||
}
|
||||
|
||||
const sessionData = (await sessionResponse.json()) as {
|
||||
|
|
@ -144,19 +166,26 @@ export async function validateOAuthToken(
|
|||
"Missing userId or apiKey in session response:",
|
||||
sessionData,
|
||||
)
|
||||
return null
|
||||
return { status: "invalid" }
|
||||
}
|
||||
|
||||
console.log("OAuth validated, got API key for user:", sessionData.userId)
|
||||
|
||||
return {
|
||||
userId: sessionData.userId,
|
||||
apiKey: sessionData.apiKey,
|
||||
email: sessionData.email,
|
||||
name: sessionData.name,
|
||||
status: "success",
|
||||
user: {
|
||||
userId: sessionData.userId,
|
||||
apiKey: sessionData.apiKey,
|
||||
email: sessionData.email,
|
||||
name: sessionData.name,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
if (isFetchTimeout(error)) {
|
||||
console.error("Token validation timed out")
|
||||
return { status: "timeout" }
|
||||
}
|
||||
console.error("Token validation error:", error)
|
||||
return null
|
||||
return { status: "invalid" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import Supermemory from "supermemory"
|
||||
import Supermemory, { APIConnectionTimeoutError } from "supermemory"
|
||||
|
||||
import { FETCH_TIMEOUT_MS } from "./constants"
|
||||
|
||||
const MAX_CHARS = 200000 // ~50k tokens (character-based limit)
|
||||
const DEFAULT_PROJECT_ID = "sm_project_default"
|
||||
const FETCH_TIMEOUT_MS = 30_000
|
||||
|
||||
interface MemoryRichFields {
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
|
@ -144,6 +145,7 @@ export class SupermemoryClient {
|
|||
apiKey: bearerToken,
|
||||
baseURL: apiUrl,
|
||||
timeout: FETCH_TIMEOUT_MS,
|
||||
maxRetries: 0,
|
||||
})
|
||||
this.containerTag = containerTag || DEFAULT_PROJECT_ID
|
||||
}
|
||||
|
|
@ -394,10 +396,10 @@ export class SupermemoryClient {
|
|||
}
|
||||
|
||||
private handleError(error: unknown): never {
|
||||
// Handle request timeout (AbortSignal.timeout or explicit abort)
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.name === "AbortError" || error.name === "TimeoutError")
|
||||
error instanceof APIConnectionTimeoutError ||
|
||||
(error instanceof Error &&
|
||||
(error.name === "AbortError" || error.name === "TimeoutError"))
|
||||
) {
|
||||
throw new Error("Request to Supermemory API timed out")
|
||||
}
|
||||
|
|
|
|||
1
apps/mcp/src/constants.ts
Normal file
1
apps/mcp/src/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const FETCH_TIMEOUT_MS = 30_000
|
||||
|
|
@ -2,6 +2,7 @@ import { cors } from "hono/cors"
|
|||
import { Hono, type Context } from "hono"
|
||||
import { SupermemoryMCP } from "./server"
|
||||
import { isApiKey, validateApiKey, validateOAuthToken } from "./auth"
|
||||
import { FETCH_TIMEOUT_MS } from "./constants"
|
||||
import { initPosthog } from "./posthog"
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||
|
||||
|
|
@ -89,7 +90,7 @@ app.get("/.well-known/oauth-authorization-server", async (c) => {
|
|||
// Fetch the authorization server metadata from the main API
|
||||
const response = await fetch(
|
||||
`${apiUrl}/.well-known/oauth-authorization-server`,
|
||||
{ signal: AbortSignal.timeout(30_000) },
|
||||
{ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) },
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -136,22 +137,32 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => {
|
|||
})
|
||||
}
|
||||
|
||||
let authUser: {
|
||||
userId: string
|
||||
apiKey: string
|
||||
email?: string
|
||||
name?: string
|
||||
} | null = null
|
||||
let authResult = isApiKey(token)
|
||||
? await validateApiKey(token, apiUrl)
|
||||
: await validateOAuthToken(token, apiUrl)
|
||||
|
||||
if (isApiKey(token)) {
|
||||
console.log("Authenticating with API key")
|
||||
authUser = await validateApiKey(token, apiUrl)
|
||||
} else {
|
||||
console.log("Authenticating with OAuth token")
|
||||
authUser = await validateOAuthToken(token, apiUrl)
|
||||
if (authResult.status === "timeout") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Authentication service timed out. Please try again.",
|
||||
},
|
||||
id: null,
|
||||
}),
|
||||
{
|
||||
status: 504,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Expose-Headers": "WWW-Authenticate",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (!authUser) {
|
||||
if (authResult.status !== "success") {
|
||||
const errorMessage = isApiKey(token)
|
||||
? "Unauthorized: Invalid or expired API key"
|
||||
: "Unauthorized: Invalid or expired token"
|
||||
|
|
@ -177,6 +188,8 @@ const handleMcpRequest = async (c: Context<{ Bindings: Bindings }>) => {
|
|||
)
|
||||
}
|
||||
|
||||
const authUser = authResult.user
|
||||
|
||||
// Create execution context with authenticated user props
|
||||
const ctx = {
|
||||
...c.executionCtx,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue