Closes checklist items F-01..F-06, D-01..D-28, S-01..S-10, C-01..C-07, B-01..B-07, R-01..R-02, O-03. Security (28 deletions + 10 auth gates): - Delete 28 unauthenticated debug/cursor/firebase/test routes - Gate ai/chat, ai/conversation, context/summarize, work-completed with withTenantProject/withAuth - Add HMAC-SHA256 signature verification to webhooks/coolify - Switch all admin secret comparisons to timingSafeStringEq Foundations (lib/server/*): - api-handler.ts: withAuth, withTenantProject, withWorkspace, withAdminSecret, withRateLimit - logger.ts: structured request-scoped logging with turnId - audit-log.ts: writeAuditLog helper + audit_log table - rate-limit.ts: Postgres sliding window rate limiter - coolify-webhook.ts: verifyCoolifySignature - timing-safe.ts: timingSafeStringEq Chat hardening (chat/route.ts): - MAX_TOOL_ROUNDS 15 → 8 (C-01) - Loop detection: hard-break at 3 identical fingerprints (was 5) (C-02) - Add 6-consecutive-tool-call hard-break (C-02) - Mode: respond first, act second prompt block (C-03) - SSE heartbeat every 25s via setInterval (C-04) - Per-tool 45s timeout via Promise.race (C-05) - turnId per-turn UUID for log correlation (C-06) - Recovery fires when roundsSinceText >= 4 (C-07) - SSE plan event on plan_task_add/edit (B-05) Beta features: - invites table + GET/POST /api/invites (P4.8) - invites/[token] validate + redeem (P4.8) - fs_project_dev_servers table + lib/server/dev-server-state.ts (P6.B1) - fs_project_secrets table + CRUD routes (P6.D2) - lib/integrations/brief-extract.ts (P3.7) Documentation: - app/api/ROUTES.md: full route map with auth + tenant
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
/**
|
|
* POST /api/context/summarize
|
|
* Body: { content: string, title?: string }
|
|
*
|
|
* Generates a short summary via Gemini. Closes S-04: now requires a
|
|
* signed-in user (rate-limit per user, not per-IP) so we don't burn Gemini
|
|
* quota on anonymous traffic.
|
|
*/
|
|
import { NextResponse } from "next/server";
|
|
import { withAuth, withRateLimit } from "@/lib/server/api-handler";
|
|
import { log } from "@/lib/server/logger";
|
|
|
|
const MODEL = process.env.GEMINI_MODEL || "gemini-3.1-pro-preview";
|
|
const API_KEY = process.env.GOOGLE_API_KEY || "";
|
|
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`;
|
|
|
|
export const POST = withRateLimit(
|
|
withAuth(async (request, _ctx, { user }) => {
|
|
try {
|
|
const { content, title } = (await request.json()) as {
|
|
content?: string;
|
|
title?: string;
|
|
};
|
|
|
|
if (!content || typeof content !== "string") {
|
|
return NextResponse.json(
|
|
{ error: "content is required" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const maxContentLength = 30000;
|
|
const truncatedContent =
|
|
content.length > maxContentLength
|
|
? content.substring(0, maxContentLength) + "..."
|
|
: content;
|
|
|
|
const prompt = `Read this document titled "${title ?? "(untitled)"}" and provide a concise 1-2 sentence summary that captures the main topic and key points. Be specific and actionable.
|
|
|
|
Document content:
|
|
${truncatedContent}
|
|
|
|
Summary:`;
|
|
|
|
const response = await fetch(`${GEMINI_URL}?key=${API_KEY}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
generationConfig: { temperature: 0.3 },
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
log.warn("context/summarize gemini error", {
|
|
route: "api.context.summarize",
|
|
user: user.email,
|
|
status: response.status,
|
|
body: text.slice(0, 500),
|
|
});
|
|
return NextResponse.json(
|
|
{
|
|
error: `Gemini API error (${response.status})`,
|
|
details: text.slice(0, 500),
|
|
},
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const summary =
|
|
result.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ||
|
|
"Summary unavailable";
|
|
return NextResponse.json({ summary });
|
|
} catch (err) {
|
|
log.error("context/summarize failed", {
|
|
route: "api.context.summarize",
|
|
err: err instanceof Error ? err.message : String(err),
|
|
});
|
|
return NextResponse.json(
|
|
{
|
|
error: "Failed to generate summary",
|
|
details: err instanceof Error ? err.message : String(err),
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}),
|
|
{
|
|
// 20 summaries / min / user — much higher than chat because they're cheap.
|
|
limit: 20,
|
|
windowMs: 60_000,
|
|
keyFn: (_req, extra) => {
|
|
const userEmail = (extra as { user?: { email?: string } })?.user?.email;
|
|
return `context-summarize:${userEmail ?? "anon"}`;
|
|
},
|
|
},
|
|
);
|