/** * Workspace autosave trigger. * * POST /api/admin/path-b/autosave * Headers: Authorization: Bearer * Body: { projectId: string, projectSlug: string } * * Pushes /workspace inside the project's dev container to a * `vibn-autosave/main` branch in Gitea. Throttled to once per 5 min * per project so we don't hammer Gitea on every chat turn. * * Two intended callers: * 1. Chat post-turn hook (best-effort fire-and-forget). * 2. Cron sweep every 5 min as a backstop. * * The autosave branch is force-pushed; never collides with `main`. * Treat this as a recovery point, not history — the user's real * commits go through the `ship` tool. */ import { NextResponse } from "next/server"; import { autosaveWorkspace } from "@/lib/dev-container"; import { query } from "@/lib/db-postgres"; import { getOrCreateProvisionedWorkspace } from "@/lib/workspaces"; import { timingSafeStringEq } from "@/lib/server/timing-safe"; export async function POST(request: Request) { const expected = process.env.NEXTAUTH_SECRET ?? ""; if (!expected) { return NextResponse.json( { error: "NEXTAUTH_SECRET not configured" }, { status: 503 }, ); } const auth = request.headers.get("authorization") ?? ""; const bearer = auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : ""; if (!bearer || !timingSafeStringEq(expected, bearer)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } let body: { projectId?: string; projectSlug?: string; sweep?: boolean }; try { body = await request.json(); } catch { return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); } // Single-project mode. if (body.projectId) { const projectId = String(body.projectId); const row = await query<{ slug: string; data: any; workspace: string }>( `SELECT slug, data, workspace FROM fs_projects WHERE id = $1 LIMIT 1`, [projectId], ); if (row.length === 0) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } const ws = await getOrCreateProvisionedWorkspace({ userId: row[0].data?.userId ?? "", email: row[0].data?.ownerEmail ?? "", displayName: row[0].workspace, }).catch(() => null); if (!ws) { return NextResponse.json( { error: "Workspace not provisioned" }, { status: 503 }, ); } const result = await autosaveWorkspace({ projectId, projectSlug: row[0].slug, workspace: ws, }); return NextResponse.json({ result }); } // Sweep mode: autosave every project with a running dev container. if (body.sweep) { const rows = await query<{ project_id: string; workspace: string }>( `SELECT project_id, workspace FROM fs_project_dev_containers WHERE state = 'running'`, [], ); const out: Array<{ projectId: string; ran: boolean; reason: string }> = []; for (const r of rows) { const proj = await query<{ slug: string; data: any }>( `SELECT slug, data FROM fs_projects WHERE id = $1 LIMIT 1`, [r.project_id], ); if (proj.length === 0) continue; const ws = await getOrCreateProvisionedWorkspace({ userId: proj[0].data?.userId ?? "", email: proj[0].data?.ownerEmail ?? "", displayName: r.workspace, }).catch(() => null); if (!ws) continue; const res = await autosaveWorkspace({ projectId: r.project_id, projectSlug: proj[0].slug, workspace: ws, }).catch((err) => ({ ran: false, reason: err instanceof Error ? err.message : String(err), })); out.push({ projectId: r.project_id, ran: res.ran, reason: res.reason }); } return NextResponse.json({ result: { swept: out.length, out } }); } return NextResponse.json( { error: "Provide either { projectId } or { sweep: true }" }, { status: 400 }, ); }