mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-26 06:04:08 +00:00
fix(mcp): accept null org and role in the session response
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
parent
152a4e611d
commit
7fc3dc2d7d
3 changed files with 36 additions and 52 deletions
|
|
@ -212,6 +212,34 @@ describe("MCP authentication", () => {
|
|||
expect(error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("rejects an API key whose session has no organization", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
const error = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(
|
||||
Response.json({
|
||||
user: { id: "user_test", email: "test@example.com", name: "Test" },
|
||||
org: null,
|
||||
session: null,
|
||||
role: null,
|
||||
accessType: "full",
|
||||
containerTags: null,
|
||||
scope: { type: "full", permission: "write" },
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
await expect(
|
||||
validateApiKey("sm_noorg_key_0123456789abcdef", API_URL),
|
||||
).resolves.toBeNull()
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"API key rejected: session has no organization",
|
||||
)
|
||||
expect(error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("rejects malformed API keys without an API request", async () => {
|
||||
const fetchSpy = vi.fn()
|
||||
vi.stubGlobal("fetch", fetchSpy)
|
||||
|
|
@ -239,48 +267,6 @@ describe("MCP authentication", () => {
|
|||
expect(warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("surfaces a 200 session body that fails the session contract as TransientAuthError", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
const error = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
await expect(
|
||||
validateApiKey("sm_badbody_key_0123456789abcd", API_URL),
|
||||
).rejects.toThrow(TransientAuthError)
|
||||
expect(error).toHaveBeenCalledTimes(1)
|
||||
expect(error.mock.calls[0]?.[0]).toBe(
|
||||
"Auth backend unavailable during API key validation: Invalid session response",
|
||||
)
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("surfaces a non-JSON 200 session body as TransientAuthError", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
const error = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
new Response("<html>upstream error</html>", { status: 200 }),
|
||||
),
|
||||
)
|
||||
|
||||
await expect(
|
||||
validateApiKey("sm_htmlbody_key_0123456789abc", API_URL),
|
||||
).rejects.toThrow(TransientAuthError)
|
||||
expect(error).toHaveBeenCalledTimes(1)
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("surfaces a session-endpoint timeout as TransientAuthError", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
const error = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
|
|
|||
|
|
@ -44,13 +44,9 @@ export async function fetchSession(
|
|||
)
|
||||
}
|
||||
|
||||
const body = await response.json().catch((error: unknown) => {
|
||||
if (error instanceof SyntaxError) return undefined
|
||||
throw error
|
||||
})
|
||||
const result = sessionInfoSchema.safeParse(body)
|
||||
const result = sessionInfoSchema.safeParse(await response.json())
|
||||
if (!result.success) {
|
||||
throw new TransientAuthError("Invalid session response")
|
||||
throw new Error("Invalid session response")
|
||||
}
|
||||
|
||||
return result.data
|
||||
|
|
@ -90,7 +86,6 @@ const TRANSIENT_ERROR_NAMES = new Set([
|
|||
const TRANSIENT_JOSE_CODES = new Set(["ERR_JWKS_TIMEOUT", "ERR_JOSE_GENERIC"])
|
||||
|
||||
function transientAuthErrorFor(error: unknown): TransientAuthError | null {
|
||||
if (error instanceof TransientAuthError) return error
|
||||
const status = (error as { status?: unknown } | null)?.status
|
||||
if (typeof status === "number" && status !== 401 && status !== 403) {
|
||||
return new TransientAuthError(`Session endpoint returned ${status}`, status)
|
||||
|
|
@ -122,7 +117,10 @@ export async function validateApiKey(
|
|||
try {
|
||||
const session = await fetchSession(token, apiUrl)
|
||||
const organizationId = session.org?.id
|
||||
if (!organizationId) return null
|
||||
if (!organizationId) {
|
||||
console.warn("API key rejected: session has no organization")
|
||||
return null
|
||||
}
|
||||
|
||||
const user: AuthUser = {
|
||||
userId: session.user.id,
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ export const sessionInfoSchema = z
|
|||
email: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
org: z.looseObject({ id: z.string().min(1) }).optional(),
|
||||
role: z.string().optional(),
|
||||
org: z.looseObject({ id: z.string().min(1) }).nullish(),
|
||||
role: z.string().nullish(),
|
||||
accessType: z.enum(["full", "restricted"]).optional(),
|
||||
containerTags: z.array(sessionNamespaceAccessSchema).nullable().optional(),
|
||||
scope: sessionScopeSchema.optional(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue