feat(api): comprehensive QA hardening — security gates, chat improvements, beta scaffolds

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
This commit is contained in:
2026-05-17 19:17:22 -07:00
parent 955aeed6ce
commit 6b8862ef2b
86 changed files with 6772 additions and 2817 deletions

View File

@@ -9,10 +9,20 @@
import { useEffect, useState } from "react";
export interface Anatomy {
project: { id: string; name: string; gitea?: string; coolifyProjectUuid?: string };
project: {
id: string;
name: string;
gitea?: string;
coolifyProjectUuid?: string;
};
codebasesReason?: "no_repo" | "empty_repo";
product: {
codebases: Array<{ id: string; label: string; path: string; hint?: string }>;
codebases: Array<{
id: string;
label: string;
path: string;
hint?: string;
}>;
images: Array<{
uuid: string;
name: string;
@@ -104,7 +114,10 @@ export interface UseAnatomyOptions {
pollMs?: number;
}
export function useAnatomy(projectId: string, options: UseAnatomyOptions = {}): UseAnatomyResult {
export function useAnatomy(
projectId: string,
options: UseAnatomyOptions = {},
): UseAnatomyResult {
const { pollMs } = options;
const [anatomy, setAnatomy] = useState<Anatomy | null>(null);
const [loading, setLoading] = useState(true);
@@ -131,22 +144,30 @@ export function useAnatomy(projectId: string, options: UseAnatomyOptions = {}):
fetch(`/api/projects/${projectId}/anatomy`, {
credentials: "include",
signal: controller.signal,
cache: "no-store",
})
.then(async r => {
.then(async (r) => {
let body: unknown = {};
try { body = await r.json(); } catch { /* keep {} */ }
try {
body = await r.json();
} catch {
/* keep {} */
}
if (!r.ok) {
const msg = (body as { error?: string }).error || `HTTP ${r.status} ${r.statusText}`.trim();
const msg =
(body as { error?: string }).error ||
`HTTP ${r.status} ${r.statusText}`.trim();
throw new Error(msg);
}
return body as Anatomy;
})
.then(data => {
.then((data) => {
if (!cancelled) setAnatomy(data);
})
.catch(err => {
.catch((err) => {
if (cancelled) return;
if (err?.name === "AbortError") setError("Request timed out after 10s.");
if (err?.name === "AbortError")
setError("Request timed out after 10s.");
else setError(err?.message || "Failed to load project anatomy");
})
.finally(() => {
@@ -161,5 +182,5 @@ export function useAnatomy(projectId: string, options: UseAnatomyOptions = {}):
};
}, [projectId, tick]);
return { anatomy, loading, error, reload: () => setTick(t => t + 1) };
return { anatomy, loading, error, reload: () => setTick((t) => t + 1) };
}