62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
|
|
/**
|
||
|
|
* POST /api/h/[hypothesisId]/red-team — enqueue a Schneier red_team_review
|
||
|
|
* investigation job for the given hypothesis. Returns { job_id, status_url }.
|
||
|
|
*
|
||
|
|
* Requires an authenticated user (any role). Audit captures the requester's
|
||
|
|
* email in triggered_by so the runtime can attribute the cost.
|
||
|
|
*/
|
||
|
|
import { NextResponse } from "next/server";
|
||
|
|
import { pgQuery } from "@/lib/retrieval/db";
|
||
|
|
import { createClient, isSupabaseConfigured } from "@/lib/supabase/server";
|
||
|
|
|
||
|
|
export const runtime = "nodejs";
|
||
|
|
export const dynamic = "force-dynamic";
|
||
|
|
|
||
|
|
export async function POST(
|
||
|
|
_request: Request,
|
||
|
|
ctx: { params: Promise<{ hypothesisId: string }> },
|
||
|
|
) {
|
||
|
|
const { hypothesisId } = await ctx.params;
|
||
|
|
if (!/^H-\d{4}$/.test(hypothesisId)) {
|
||
|
|
return NextResponse.json({ error: "bad_hypothesis_id" }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
let triggered_by = "user:anonymous";
|
||
|
|
if (isSupabaseConfigured()) {
|
||
|
|
const supabase = await createClient();
|
||
|
|
const { data: { user } } = await supabase.auth.getUser();
|
||
|
|
if (!user) {
|
||
|
|
return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
|
||
|
|
}
|
||
|
|
triggered_by = `user:${user.email ?? user.id}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify hypothesis exists.
|
||
|
|
const found = await pgQuery<{ hypothesis_id: string }>(
|
||
|
|
`SELECT hypothesis_id FROM public.hypotheses WHERE hypothesis_id = $1`,
|
||
|
|
[hypothesisId],
|
||
|
|
).catch(() => []);
|
||
|
|
if (found.length === 0) {
|
||
|
|
return NextResponse.json({ error: "not_found", hypothesis_id: hypothesisId }, { status: 404 });
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const rows = await pgQuery<{ job_id: string }>(
|
||
|
|
`INSERT INTO public.investigation_jobs (kind, payload, triggered_by, status)
|
||
|
|
VALUES ('red_team_review', $1::jsonb, $2, 'queued')
|
||
|
|
RETURNING job_id`,
|
||
|
|
[JSON.stringify({ hypothesis_id: hypothesisId }), triggered_by],
|
||
|
|
);
|
||
|
|
const job_id = rows[0]?.job_id;
|
||
|
|
if (!job_id) return NextResponse.json({ error: "insert_failed" }, { status: 500 });
|
||
|
|
return NextResponse.json({
|
||
|
|
job_id,
|
||
|
|
status: "queued",
|
||
|
|
status_url: `/jobs/${job_id}`,
|
||
|
|
eta_seconds: 30,
|
||
|
|
});
|
||
|
|
} catch (e) {
|
||
|
|
return NextResponse.json({ error: "db_unavailable", message: (e as Error).message }, { status: 503 });
|
||
|
|
}
|
||
|
|
}
|