mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-27 06:15:15 +00:00
feat(mcp): validate API keys via /session (#1839)
## What The Supermemory API now serves the session endpoint unversioned at `/session` (supermemoryai/mono#3626). `sm_` API-key validation in the MCP worker calls `/session` instead of `/v3/session`. One-line code change. - `apps/mcp/src/server/auth/index.ts`: `fetchSession` requests `/session`. - `apps/mcp/src/server/auth/index.test.ts`: asserts the new URL and that a refused key makes exactly one request. - `apps/mcp/src/shared/types.ts`: comment updated. ## Deploy order This worker depends on the API serving `/session`. **Deploy mono#3626 first.** Until then, production answers `/session` with 404 for full keys and 403 for scoped keys (the scope allowlist is checked before routing), and every `sm_` key would be rejected. No fallback to `/v3/session` on purpose: the only "old server" in scope is that deploy window, and a failed-request fallback would blur 403/404 semantics for a case merge ordering already covers. ## Verification - `bun run check-types`, `vitest run src` (29/29). - Against the API with mono#3626 running: full key → `/session` 200 valid; scoped key → `/session` 200 valid; bad key → `/session` 401 rejected, single call. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
parent
f3001517d1
commit
f355dc8039
3 changed files with 9 additions and 7 deletions
|
|
@ -177,7 +177,7 @@ describe("MCP authentication", () => {
|
|||
scopes: [],
|
||||
})
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
`${API_URL}/v3/session`,
|
||||
`${API_URL}/session`,
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
}),
|
||||
|
|
@ -197,14 +197,16 @@ describe("MCP authentication", () => {
|
|||
it("rejects an API key the session endpoint refuses", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
const error = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(new Response(null, { status: 401 })),
|
||||
)
|
||||
const fetchSpy = vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Response(null, { status: 401 }))
|
||||
vi.stubGlobal("fetch", fetchSpy)
|
||||
|
||||
await expect(
|
||||
validateApiKey("sm_revoked_key_0123456789abcdef", API_URL),
|
||||
).resolves.toBeNull()
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||
expect(fetchSpy.mock.calls[0]?.[0]).toBe(`${API_URL}/session`)
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"API key rejected: Session request failed with status 401",
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export async function fetchSession(
|
|||
bearerToken: string,
|
||||
apiUrl: string,
|
||||
): Promise<SessionInfo> {
|
||||
const response = await fetch(`${apiUrl.replace(/\/+$/, "")}/v3/session`, {
|
||||
const response = await fetch(`${apiUrl.replace(/\/+$/, "")}/session`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${bearerToken}` },
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const namespaceAccessSchema = z.object({
|
|||
|
||||
export type NamespaceAccess = z.infer<typeof namespaceAccessSchema>
|
||||
|
||||
// /v3/session has no v5 replacement and still sends container-tag field names.
|
||||
// /session still sends container-tag field names.
|
||||
const sessionNamespaceAccessSchema = z
|
||||
.object({ containerTag: z.string(), permission: z.enum(["read", "write"]) })
|
||||
.transform(({ containerTag, permission }) => ({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue