From 5a3a9670171c53311dee5b5fc33d03e4c45fb9d7 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 10 Feb 2026 13:46:11 -0800 Subject: [PATCH] fix(api): bake org context into API keys via tRPC wrapper API key creation now goes through a tRPC procedure that injects the user's activeOrganizationId into key metadata server-side. The MCP endpoint reads org from metadata instead of falling back to a members query. Also stops leaking the raw API key secret in the token field. --- .../src/app/api/agent/[transport]/route.ts | 24 ++++++++-------- .../ApiKeysSettings/ApiKeysSettings.tsx | 7 +++-- packages/trpc/src/root.ts | 2 ++ packages/trpc/src/router/api-key/api-key.ts | 28 +++++++++++++++++++ packages/trpc/src/router/api-key/index.ts | 1 + 5 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 packages/trpc/src/router/api-key/api-key.ts create mode 100644 packages/trpc/src/router/api-key/index.ts diff --git a/apps/api/src/app/api/agent/[transport]/route.ts b/apps/api/src/app/api/agent/[transport]/route.ts index d88c131a842..a1919bf5978 100644 --- a/apps/api/src/app/api/agent/[transport]/route.ts +++ b/apps/api/src/app/api/agent/[transport]/route.ts @@ -1,10 +1,7 @@ import { auth } from "@superset/auth/server"; -import { db } from "@superset/db/client"; -import { members } from "@superset/db/schema"; import { registerTools } from "@superset/mcp"; import type { McpContext } from "@superset/mcp/auth"; import { verifyAccessToken } from "better-auth/oauth2"; -import { desc, eq } from "drizzle-orm"; import { createMcpHandler, withMcpAuth } from "mcp-handler"; import { env } from "@/env"; @@ -40,24 +37,29 @@ async function verifyToken(req: Request, bearerToken?: string) { }); if (result.valid && result.key) { const userId = result.key.userId; - const membership = await db.query.members.findFirst({ - where: eq(members.userId, userId), - orderBy: desc(members.createdAt), - }); - if (!membership?.organizationId) { + if (!userId) { + console.error("[mcp/auth] API key missing userId"); + return undefined; + } + const metadata = + typeof result.key.metadata === "string" + ? JSON.parse(result.key.metadata) + : result.key.metadata; + const organizationId = metadata?.organizationId as string | undefined; + if (!organizationId) { console.error( - "[mcp/auth] API key user has no organization membership", + "[mcp/auth] API key missing organizationId in metadata", ); return undefined; } return { - token: bearerToken, + token: "api-key", clientId: "api-key", scopes: ["mcp:full"], extra: { mcpContext: { userId, - organizationId: membership.organizationId, + organizationId, } satisfies McpContext, }, }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx index 894a985db80..b8bbcd2ab63 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx @@ -31,6 +31,7 @@ import { HiOutlinePlus, HiOutlineTrash, } from "react-icons/hi2"; +import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { authClient } from "renderer/lib/auth-client"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { @@ -71,11 +72,11 @@ export function ApiKeysSettings({ visibleItems }: ApiKeysSettingsProps) { try { setIsGenerating(true); - const result = await authClient.apiKey.create({ + const result = await apiTrpcClient.apiKey.create.mutate({ name: newKeyName.trim(), }); - if (result.data?.key) { - setNewKeyValue(result.data.key); + if (result.key) { + setNewKeyValue(result.key); setShowGenerateDialog(false); setShowNewKeyDialog(true); setNewKeyName(""); diff --git a/packages/trpc/src/root.ts b/packages/trpc/src/root.ts index 7e333dee987..fdce635badb 100644 --- a/packages/trpc/src/root.ts +++ b/packages/trpc/src/root.ts @@ -3,6 +3,7 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { adminRouter } from "./router/admin"; import { agentRouter } from "./router/agent"; import { analyticsRouter } from "./router/analytics"; +import { apiKeyRouter } from "./router/api-key"; import { deviceRouter } from "./router/device"; import { integrationRouter } from "./router/integration"; import { organizationRouter } from "./router/organization"; @@ -14,6 +15,7 @@ import { createCallerFactory, createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ admin: adminRouter, agent: agentRouter, + apiKey: apiKeyRouter, analytics: analyticsRouter, device: deviceRouter, integration: integrationRouter, diff --git a/packages/trpc/src/router/api-key/api-key.ts b/packages/trpc/src/router/api-key/api-key.ts new file mode 100644 index 00000000000..2e30fcbb5e6 --- /dev/null +++ b/packages/trpc/src/router/api-key/api-key.ts @@ -0,0 +1,28 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; + +import { protectedProcedure } from "../../trpc"; + +export const apiKeyRouter = { + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + const organizationId = ctx.session.session.activeOrganizationId; + if (!organizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Active organization required to create an API key", + }); + } + + const result = await ctx.auth.api.createApiKey({ + headers: ctx.headers, + body: { + name: input.name, + metadata: { organizationId }, + }, + }); + + return { key: result.key }; + }), +}; diff --git a/packages/trpc/src/router/api-key/index.ts b/packages/trpc/src/router/api-key/index.ts new file mode 100644 index 00000000000..0fbc7e79675 --- /dev/null +++ b/packages/trpc/src/router/api-key/index.ts @@ -0,0 +1 @@ +export { apiKeyRouter } from "./api-key";