security fix

This commit is contained in:
Aadesh Kheria 2026-04-10 12:55:22 -07:00
parent ef77edc15f
commit 84dffa29f0
17 changed files with 295 additions and 15 deletions

View File

@ -0,0 +1,56 @@
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 { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
export const POST = createSmartRouteHandler({
metadata: { hidden: true },
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: adaptSchema,
}).defined(),
body: yupObject({
question: yupString().defined(),
answer: yupString().defined(),
publish: yupBoolean().defined(),
reviewedBy: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().defined(),
}).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.");
}
const conn = await getConnection();
if (!conn) {
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
await conn.reducers.addManualQa({
token,
question: body.question,
answer: body.answer,
publish: body.publish,
reviewedBy: body.reviewedBy,
});
return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true },
};
},
});

View File

@ -0,0 +1,50 @@
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 { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
export const POST = createSmartRouteHandler({
metadata: { hidden: true },
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: adaptSchema,
}).defined(),
body: yupObject({
correlationId: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().defined(),
}).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.");
}
const conn = await getConnection();
if (!conn) {
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
await conn.reducers.deleteQaEntry({
token,
correlationId: body.correlationId,
});
return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true },
};
},
});

View File

@ -0,0 +1,52 @@
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 { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
export const POST = createSmartRouteHandler({
metadata: { hidden: true },
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: adaptSchema,
}).defined(),
body: yupObject({
correlationId: yupString().defined(),
reviewedBy: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().defined(),
}).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.");
}
const conn = await getConnection();
if (!conn) {
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
await conn.reducers.markHumanReviewed({
token,
correlationId: body.correlationId,
reviewedBy: body.reviewedBy,
});
return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true },
};
},
});

View File

@ -0,0 +1,58 @@
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 { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
export const POST = createSmartRouteHandler({
metadata: { hidden: true },
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: adaptSchema,
}).defined(),
body: yupObject({
correlationId: yupString().defined(),
correctedQuestion: yupString().defined(),
correctedAnswer: yupString().defined(),
publish: yupBoolean().defined(),
reviewedBy: yupString().defined(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().defined(),
}).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.");
}
const conn = await getConnection();
if (!conn) {
throw new StatusError(503, "SpacetimeDB unavailable");
}
const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me");
await conn.reducers.updateHumanCorrection({
token,
correlationId: body.correlationId,
correctedQuestion: body.correctedQuestion,
correctedAnswer: body.correctedAnswer,
publish: body.publish,
reviewedBy: body.reviewedBy,
});
return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true },
};
},
});

View File

@ -11,6 +11,7 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
question: __t.string(),
answer: __t.string(),
publish: __t.bool(),

View File

@ -11,5 +11,6 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
correlationId: __t.string(),
};

View File

@ -11,6 +11,7 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
correlationId: __t.string(),
reviewedBy: __t.string(),
};

View File

@ -11,6 +11,7 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
correlationId: __t.string(),
correctedQuestion: __t.string(),
correctedAnswer: __t.string(),

View File

@ -25,7 +25,6 @@ async function getVerifiedQaContextInner(): Promise<string> {
}
}
console.log(`[verified-qa] Found ${pairs.length} published Q&A pairs`);
if (pairs.length === 0) return "";
const formatted = pairs.map((p, i) =>

View File

@ -128,10 +128,14 @@ export const update_mcp_qa_review = spacetimedb.reducer(
export const mark_human_reviewed = spacetimedb.reducer(
{
token: t.string(),
correlationId: t.string(),
reviewedBy: t.string(),
},
(ctx, args) => {
if (args.token !== EXPECTED_LOG_TOKEN) {
throw new SenderError('Invalid log token');
}
for (const row of ctx.db.mcpCallLog.iter()) {
if (row.correlationId === args.correlationId) {
ctx.db.mcpCallLog.delete(row);
@ -149,6 +153,7 @@ export const mark_human_reviewed = spacetimedb.reducer(
export const update_human_correction = spacetimedb.reducer(
{
token: t.string(),
correlationId: t.string(),
correctedQuestion: t.string(),
correctedAnswer: t.string(),
@ -156,6 +161,9 @@ export const update_human_correction = spacetimedb.reducer(
reviewedBy: t.string(),
},
(ctx, args) => {
if (args.token !== EXPECTED_LOG_TOKEN) {
throw new SenderError('Invalid log token');
}
for (const row of ctx.db.mcpCallLog.iter()) {
if (row.correlationId === args.correlationId) {
ctx.db.mcpCallLog.delete(row);
@ -177,12 +185,16 @@ export const update_human_correction = spacetimedb.reducer(
export const add_manual_qa = spacetimedb.reducer(
{
token: t.string(),
question: t.string(),
answer: t.string(),
publish: t.bool(),
reviewedBy: t.string(),
},
(ctx, args) => {
if (args.token !== EXPECTED_LOG_TOKEN) {
throw new SenderError('Invalid log token');
}
ctx.db.mcpCallLog.insert({
id: 0n,
correlationId: ctx.newUuidV4().toString(),
@ -208,9 +220,13 @@ export const add_manual_qa = spacetimedb.reducer(
export const delete_qa_entry = spacetimedb.reducer(
{
token: t.string(),
correlationId: t.string(),
},
(ctx, args) => {
if (args.token !== EXPECTED_LOG_TOKEN) {
throw new SenderError('Invalid log token');
}
for (const row of ctx.db.mcpCallLog.iter()) {
if (row.correlationId === args.correlationId) {
ctx.db.mcpCallLog.delete(row);

View File

@ -7,6 +7,7 @@ import { AddManualQa } from "../components/AddManualQa";
import { KnowledgeBase } from "../components/KnowledgeBase";
import { Analytics } from "../components/Analytics";
import { useMcpCallLogs } from "../hooks/useSpacetimeDB";
import { mcpReviewApi } from "../lib/mcp-review-api";
import type { McpCallLogRow } from "../types";
type Tab = "calls" | "knowledge" | "analytics";
@ -16,7 +17,7 @@ export default function App() {
const [selectedRow, setSelectedRow] = useState<McpCallLogRow | null>(null);
const [showAddQa, setShowAddQa] = useState(false);
const [tab, setTab] = useState<Tab>("calls");
const { rows, connectionState, connection } = useMcpCallLogs();
const { rows, connectionState } = useMcpCallLogs();
if (!user) {
return (
@ -126,13 +127,12 @@ export default function App() {
<AddManualQa
onClose={() => setShowAddQa(false)}
onSave={(question, answer, publish) => {
if (!connection) return;
connection.reducers.addManualQa({
mcpReviewApi.addManual({
question,
answer,
publish,
reviewedBy,
}).catch(() => {});
}).catch(() => { /* errors are surfaced by UI state */ });
}}
/>
)}
@ -154,14 +154,13 @@ export default function App() {
allRows={rows}
onClose={() => setSelectedRow(null)}
onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) => {
if (!connection) return;
connection.reducers.updateHumanCorrection({
mcpReviewApi.updateCorrection({
correlationId,
correctedQuestion,
correctedAnswer,
publish,
reviewedBy,
}).catch(() => {});
}).catch(() => { /* errors are surfaced by UI state */ });
}}
/>
</aside>
@ -174,20 +173,18 @@ export default function App() {
<KnowledgeBase
rows={rows}
onSave={(correlationId, question, answer, publish) => {
if (!connection) return;
connection.reducers.updateHumanCorrection({
mcpReviewApi.updateCorrection({
correlationId,
correctedQuestion: question,
correctedAnswer: answer,
publish,
reviewedBy,
}).catch(() => {});
}).catch(() => { /* errors are surfaced by UI state */ });
}}
onDelete={(correlationId) => {
if (!connection) return;
connection.reducers.deleteQaEntry({
mcpReviewApi.delete({
correlationId,
}).catch(() => {});
}).catch(() => { /* errors are surfaced by UI state */ });
}}
/>
</main>

View File

@ -109,5 +109,5 @@ export function useMcpCallLogs() {
};
}, []);
return { rows, connectionState, connection: connRef.current };
return { rows, connectionState };
}

View File

@ -0,0 +1,44 @@
const API_URL = process.env.NEXT_PUBLIC_STACK_API_URL;
const PROJECT_ID = process.env.NEXT_PUBLIC_STACK_PROJECT_ID;
const PUBLISHABLE_CLIENT_KEY = process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY;
async function post(path: string, body: unknown): Promise<void> {
const res = await fetch(`${API_URL}/api/latest/internal/mcp-review/${path}`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-stack-access-type": "client",
"x-stack-project-id": PROJECT_ID ?? "",
"x-stack-publishable-client-key": PUBLISHABLE_CLIENT_KEY ?? "",
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`MCP review API error (${res.status}): ${text}`);
}
}
export const mcpReviewApi = {
markReviewed: (body: { correlationId: string; reviewedBy: string }) =>
post("mark-reviewed", body),
updateCorrection: (body: {
correlationId: string;
correctedQuestion: string;
correctedAnswer: string;
publish: boolean;
reviewedBy: string;
}) => post("update-correction", body),
addManual: (body: {
question: string;
answer: string;
publish: boolean;
reviewedBy: string;
}) => post("add-manual", body),
delete: (body: { correlationId: string }) =>
post("delete", body),
};

View File

@ -11,6 +11,7 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
question: __t.string(),
answer: __t.string(),
publish: __t.bool(),

View File

@ -11,5 +11,6 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
correlationId: __t.string(),
};

View File

@ -11,6 +11,7 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
correlationId: __t.string(),
reviewedBy: __t.string(),
};

View File

@ -11,6 +11,7 @@ import {
} from "spacetimedb";
export default {
token: __t.string(),
correlationId: __t.string(),
correctedQuestion: __t.string(),
correctedAnswer: __t.string(),