mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
security fix
This commit is contained in:
parent
ef77edc15f
commit
84dffa29f0
@ -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 },
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -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 },
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
question: __t.string(),
|
||||
answer: __t.string(),
|
||||
publish: __t.bool(),
|
||||
|
||||
@ -11,5 +11,6 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
correlationId: __t.string(),
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
correlationId: __t.string(),
|
||||
reviewedBy: __t.string(),
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
correlationId: __t.string(),
|
||||
correctedQuestion: __t.string(),
|
||||
correctedAnswer: __t.string(),
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -109,5 +109,5 @@ export function useMcpCallLogs() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { rows, connectionState, connection: connRef.current };
|
||||
return { rows, connectionState };
|
||||
}
|
||||
|
||||
44
apps/internal-tool/src/lib/mcp-review-api.ts
Normal file
44
apps/internal-tool/src/lib/mcp-review-api.ts
Normal 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),
|
||||
};
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
question: __t.string(),
|
||||
answer: __t.string(),
|
||||
publish: __t.bool(),
|
||||
|
||||
@ -11,5 +11,6 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
correlationId: __t.string(),
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
correlationId: __t.string(),
|
||||
reviewedBy: __t.string(),
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "spacetimedb";
|
||||
|
||||
export default {
|
||||
token: __t.string(),
|
||||
correlationId: __t.string(),
|
||||
correctedQuestion: __t.string(),
|
||||
correctedAnswer: __t.string(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user