Files
vibn-frontend/vibn-frontend/app/api/work-completed/route.ts
mawkone 6b8862ef2b 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
2026-05-17 19:17:22 -07:00

66 lines
2.0 KiB
TypeScript

/**
* GET /api/work-completed?projectId=<uuid>&limit=<n>
*
* Returns the work_completed rows for a project the caller owns.
*
* Closes S-05: this route used to accept any projectId off the query string
* with no auth and silently fell back to `projectId = 1` on missing input.
*/
import { NextResponse } from "next/server";
import { query } from "@/lib/db-postgres";
import type { WorkCompleted } from "@/lib/types";
import { withTenantProject } from "@/lib/server/api-handler";
import { log } from "@/lib/server/logger";
export const GET = withTenantProject(
async (request) => {
const { searchParams } = new URL(request.url);
const projectId = searchParams.get("projectId");
if (!projectId) {
return NextResponse.json(
{ error: "projectId is required" },
{ status: 400 },
);
}
const limit = Math.min(
200,
Math.max(1, parseInt(searchParams.get("limit") ?? "20", 10) || 20),
);
try {
const workItems = await query<WorkCompleted>(
`SELECT
wc.*,
s.session_id,
s.primary_ai_model,
s.duration_minutes
FROM work_completed wc
LEFT JOIN sessions s ON wc.session_id = s.id
WHERE wc.project_id = $1
ORDER BY wc.completed_at DESC
LIMIT $2`,
[projectId, limit],
);
const parsedWork = workItems.map((item) => ({
...item,
files_modified:
typeof item.files_modified === "string"
? JSON.parse(item.files_modified)
: item.files_modified,
}));
return NextResponse.json(parsedWork);
} catch (err) {
log.error("work-completed: db error", {
route: "api.work-completed",
projectId,
err: err instanceof Error ? err.message : String(err),
});
return NextResponse.json(
{ error: "Failed to fetch work completed" },
{ status: 500 },
);
}
},
{ source: "search", paramName: "projectId" },
);