From a0486e9a27d4fef096a2581bcc50f5718ef00e7c Mon Sep 17 00:00:00 2001 From: Aadesh Kheria Date: Fri, 10 Apr 2026 17:42:26 -0700 Subject: [PATCH] security fixes --- .../internal/mcp-review/add-manual/route.ts | 10 +-- .../internal/mcp-review/delete/route.ts | 10 +-- .../mcp-review/mark-reviewed/route.ts | 10 +-- .../mcp-review/update-correction/route.ts | 10 +-- apps/backend/src/lib/ai/tools/docs.ts | 2 +- apps/internal-tool/.env | 15 ++-- apps/internal-tool/.env.development | 8 +-- apps/internal-tool/src/app/app-client.tsx | 66 +++++++++--------- .../src/app/handler/[...stack]/page.tsx | 11 +++ apps/internal-tool/src/app/questions/page.tsx | 3 - .../src/components/CallLogDetail.tsx | 24 ++++++- .../internal-tool/src/hooks/useSpacetimeDB.ts | 8 ++- apps/internal-tool/src/lib/mcp-review-api.ts | 68 ++++++++++++------- apps/internal-tool/src/stack.ts | 39 ++++++++--- 14 files changed, 177 insertions(+), 107 deletions(-) create mode 100644 apps/internal-tool/src/app/handler/[...stack]/page.tsx diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts index 94f773bac..807d812f7 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts @@ -1,7 +1,7 @@ import { getConnection } from "@/lib/ai/mcp-logger"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ @@ -28,9 +28,11 @@ export const POST = createSmartRouteHandler({ }).defined(), }), handler: async ({ body }, fullReq) => { - const metadata = fullReq.auth?.user?.client_read_only_metadata; - if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { - throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + if (getNodeEnvironment() !== "development") { + const metadata = fullReq.auth?.user?.client_read_only_metadata; + if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { + throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + } } const conn = await getConnection(); diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts index d02f83303..c545bf5f6 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts @@ -1,7 +1,7 @@ import { getConnection } from "@/lib/ai/mcp-logger"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ @@ -25,9 +25,11 @@ export const POST = createSmartRouteHandler({ }).defined(), }), handler: async ({ body }, fullReq) => { - const metadata = fullReq.auth?.user?.client_read_only_metadata; - if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { - throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + if (getNodeEnvironment() !== "development") { + const metadata = fullReq.auth?.user?.client_read_only_metadata; + if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { + throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + } } const conn = await getConnection(); diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts index cffdbe037..27abd4529 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts @@ -1,7 +1,7 @@ import { getConnection } from "@/lib/ai/mcp-logger"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ @@ -26,9 +26,11 @@ export const POST = createSmartRouteHandler({ }).defined(), }), handler: async ({ body }, fullReq) => { - const metadata = fullReq.auth?.user?.client_read_only_metadata; - if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { - throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + if (getNodeEnvironment() !== "development") { + const metadata = fullReq.auth?.user?.client_read_only_metadata; + if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { + throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + } } const conn = await getConnection(); diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts index 672c3760a..19ff74b0a 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts @@ -1,7 +1,7 @@ import { getConnection } from "@/lib/ai/mcp-logger"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ @@ -29,9 +29,11 @@ export const POST = createSmartRouteHandler({ }).defined(), }), handler: async ({ body }, fullReq) => { - const metadata = fullReq.auth?.user?.client_read_only_metadata; - if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { - throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + if (getNodeEnvironment() !== "development") { + const metadata = fullReq.auth?.user?.client_read_only_metadata; + if (!(metadata && typeof metadata === "object" && "approved" in metadata && metadata.approved === true)) { + throw new StatusError(StatusError.Forbidden, "You are not approved to perform MCP review operations."); + } } const conn = await getConnection(); diff --git a/apps/backend/src/lib/ai/tools/docs.ts b/apps/backend/src/lib/ai/tools/docs.ts index 91f45ea10..2c4b29ecf 100644 --- a/apps/backend/src/lib/ai/tools/docs.ts +++ b/apps/backend/src/lib/ai/tools/docs.ts @@ -15,7 +15,7 @@ function getDocsToolsBaseUrl(): string { } if (getNodeEnvironment() === "development") { const portPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); - return `http://localhost:${portPrefix}04`; + return `http://localhost:${portPrefix}26`; } return "https://mcp.stack-auth.com"; } diff --git a/apps/internal-tool/.env b/apps/internal-tool/.env index 4bf80bb2f..439b9ffa8 100644 --- a/apps/internal-tool/.env +++ b/apps/internal-tool/.env @@ -1,10 +1,9 @@ # Stack Auth -NEXT_PUBLIC_STACK_API_URL= -NEXT_PUBLIC_STACK_PROJECT_ID=internal -NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= -NEXT_PUBLIC_STACK_HOSTED_COMPONENTS_URL= -NEXT_PUBLIC_STACK_INTERNAL_TOOL_URL= -NEXT_PUBLIC_STACK_DASHBOARD_URL= +NEXT_PUBLIC_STACK_API_URL=REPLACE_ME +NEXT_PUBLIC_STACK_PROJECT_ID=REPLACE_ME +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=REPLACE_ME +STACK_SECRET_SERVER_KEY=REPLACE_ME +NEXT_PUBLIC_STACK_DASHBOARD_URL=REPLACE_ME # SpacetimeDB -NEXT_PUBLIC_SPACETIMEDB_HOST= -NEXT_PUBLIC_SPACETIMEDB_DB_NAME= +NEXT_PUBLIC_SPACETIMEDB_HOST=REPLACE_ME +NEXT_PUBLIC_SPACETIMEDB_DB_NAME=REPLACE_ME diff --git a/apps/internal-tool/.env.development b/apps/internal-tool/.env.development index 185e8e12a..f04eca35e 100644 --- a/apps/internal-tool/.env.development +++ b/apps/internal-tool/.env.development @@ -1,8 +1,6 @@ -NEXT_PUBLIC_STACK_API_URL=http://localhost:8102 +NEXT_PUBLIC_STACK_API_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02 NEXT_PUBLIC_STACK_PROJECT_ID=internal NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only -NEXT_PUBLIC_STACK_HOSTED_COMPONENTS_URL=http://internal.localhost:8109 -NEXT_PUBLIC_STACK_INTERNAL_TOOL_URL=http://localhost:8141 -NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101 -NEXT_PUBLIC_SPACETIMEDB_HOST=ws://localhost:8139 +NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01 +NEXT_PUBLIC_SPACETIMEDB_HOST=ws://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}39 NEXT_PUBLIC_SPACETIMEDB_DB_NAME=stack-auth-llm diff --git a/apps/internal-tool/src/app/app-client.tsx b/apps/internal-tool/src/app/app-client.tsx index 7b694ebe9..d0593698c 100644 --- a/apps/internal-tool/src/app/app-client.tsx +++ b/apps/internal-tool/src/app/app-client.tsx @@ -1,19 +1,19 @@ import { useUser } from "@stackframe/react"; -import { useState } from "react"; import { clsx } from "clsx"; -import { CallLogList } from "../components/CallLogList"; -import { CallLogDetail } from "../components/CallLogDetail"; +import { useState } from "react"; import { AddManualQa } from "../components/AddManualQa"; -import { KnowledgeBase } from "../components/KnowledgeBase"; import { Analytics } from "../components/Analytics"; +import { CallLogDetail } from "../components/CallLogDetail"; +import { CallLogList } from "../components/CallLogList"; +import { KnowledgeBase } from "../components/KnowledgeBase"; import { useMcpCallLogs } from "../hooks/useSpacetimeDB"; -import { mcpReviewApi } from "../lib/mcp-review-api"; +import { makeMcpReviewApi } from "../lib/mcp-review-api"; import type { McpCallLogRow } from "../types"; type Tab = "calls" | "knowledge" | "analytics"; export default function App() { - const user = useUser(); + const user = useUser({ or: process.env.NODE_ENV === "development" ? "redirect" : "return-null" }); const [selectedRow, setSelectedRow] = useState(null); const [showAddQa, setShowAddQa] = useState(false); const [tab, setTab] = useState("calls"); @@ -52,9 +52,6 @@ export default function App() {

You are signed in as {user.displayName ?? user.primaryEmail}, but your account is not approved.

-

- An admin needs to set approved: true in your client read-only metadata. -

); @@ -65,7 +62,16 @@ export default function App() { ? rows.find(r => r.id === selectedRow.id) ?? selectedRow : null; - const reviewedBy = user.displayName ?? user.primaryEmail ?? "unknown"; + const currentUser = user; + const reviewedBy = currentUser.displayName ?? currentUser.primaryEmail ?? "unknown"; + + async function getApi() { + const { accessToken, refreshToken } = await currentUser.getAuthJson(); + const authHeaders: Record = {}; + if (accessToken) authHeaders["x-stack-access-token"] = accessToken; + if (refreshToken) authHeaders["x-stack-refresh-token"] = refreshToken; + return makeMcpReviewApi(authHeaders); + } return (
@@ -127,12 +133,9 @@ export default function App() { setShowAddQa(false)} onSave={(question, answer, publish) => { - mcpReviewApi.addManual({ - question, - answer, - publish, - reviewedBy, - }).catch(() => { /* errors are surfaced by UI state */ }); + getApi() + .then(api => api.addManual({ question, answer, publish, reviewedBy })) + .catch(() => { /* errors are surfaced by UI state */ }); }} /> )} @@ -154,13 +157,14 @@ export default function App() { allRows={rows} onClose={() => setSelectedRow(null)} onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) => { - mcpReviewApi.updateCorrection({ - correlationId, - correctedQuestion, - correctedAnswer, - publish, - reviewedBy, - }).catch(() => { /* errors are surfaced by UI state */ }); + getApi() + .then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish, reviewedBy })) + .catch(() => { /* errors are surfaced by UI state */ }); + }} + onMarkReviewed={(correlationId) => { + getApi() + .then(api => api.markReviewed({ correlationId, reviewedBy })) + .catch(() => { /* errors are surfaced by UI state */ }); }} /> @@ -173,18 +177,14 @@ export default function App() { { - mcpReviewApi.updateCorrection({ - correlationId, - correctedQuestion: question, - correctedAnswer: answer, - publish, - reviewedBy, - }).catch(() => { /* errors are surfaced by UI state */ }); + getApi() + .then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish, reviewedBy })) + .catch(() => { /* errors are surfaced by UI state */ }); }} onDelete={(correlationId) => { - mcpReviewApi.delete({ - correlationId, - }).catch(() => { /* errors are surfaced by UI state */ }); + getApi() + .then(api => api.delete({ correlationId })) + .catch(() => { /* errors are surfaced by UI state */ }); }} /> diff --git a/apps/internal-tool/src/app/handler/[...stack]/page.tsx b/apps/internal-tool/src/app/handler/[...stack]/page.tsx new file mode 100644 index 000000000..d5b80af44 --- /dev/null +++ b/apps/internal-tool/src/app/handler/[...stack]/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { StackHandler } from "@stackframe/react"; +import { useEffect, useState } from "react"; + +export default function Handler() { + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + if (!mounted) return null; + return ; +} diff --git a/apps/internal-tool/src/app/questions/page.tsx b/apps/internal-tool/src/app/questions/page.tsx index 1ed1c5a7e..2fcadcc2d 100644 --- a/apps/internal-tool/src/app/questions/page.tsx +++ b/apps/internal-tool/src/app/questions/page.tsx @@ -61,9 +61,6 @@ export default function QuestionsPage() { {row.publishedAt && ( {format(toDate(row.publishedAt), "MMM d, yyyy")} )} - {row.humanReviewedBy && ( - Reviewed by {row.humanReviewedBy} - )}
))} diff --git a/apps/internal-tool/src/components/CallLogDetail.tsx b/apps/internal-tool/src/components/CallLogDetail.tsx index bf347bc7e..db8ac2d54 100644 --- a/apps/internal-tool/src/components/CallLogDetail.tsx +++ b/apps/internal-tool/src/components/CallLogDetail.tsx @@ -28,13 +28,15 @@ function CopyButton({ text }: { text: string }) { // ─── Main Component ──────────────────────────────────── -export function CallLogDetail({ row, allRows, onClose, onSaveCorrection }: { +export function CallLogDetail({ row, allRows, onClose, onSaveCorrection, onMarkReviewed }: { row: McpCallLogRow; allRows: McpCallLogRow[]; onClose: () => void; onSaveCorrection?: (correlationId: string, correctedQuestion: string, correctedAnswer: string, publish: boolean) => void; + onMarkReviewed?: (correlationId: string) => void; }) { const [showReplay, setShowReplay] = useState(false); + const isReviewed = row.humanReviewedAt != null; return (
@@ -44,8 +46,26 @@ export function CallLogDetail({ row, allRows, onClose, onSaveCorrection }: { {/* Header */}
-

Call Detail

+

Call Detail

+ {isReviewed && ( + + ✓ Reviewed{row.humanReviewedBy ? ` by ${row.humanReviewedBy}` : ""} + + )} +
+
+ {!isReviewed && onMarkReviewed && ( + + )}