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:
40
vibn-frontend/lib/server/coolify-webhook.ts
Normal file
40
vibn-frontend/lib/server/coolify-webhook.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Coolify webhook signature verification.
|
||||
*
|
||||
* Coolify (≥ 4.0.0-beta.300) signs every webhook with HMAC-SHA256 of the
|
||||
* raw body using the per-app `webhook_secret`. The signature is sent in
|
||||
* the `X-Coolify-Signature-256` header as `sha256=<hex>`.
|
||||
*
|
||||
* If the per-app secret is not set, Coolify sends the body unsigned. In
|
||||
* that case we reject the call: every prod deploy MUST set a secret.
|
||||
*
|
||||
* Mirrors the pattern in `lib/gitea.ts:verifyWebhookSignature`.
|
||||
*/
|
||||
|
||||
import { timingSafeStringEq } from "@/lib/server/timing-safe";
|
||||
|
||||
export async function verifyCoolifySignature(
|
||||
body: string,
|
||||
signatureHeader: string | null,
|
||||
secret: string,
|
||||
): Promise<boolean> {
|
||||
if (!secret) return false;
|
||||
if (!signatureHeader?.startsWith("sha256=")) return false;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const sigBytes = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
||||
const expected =
|
||||
"sha256=" +
|
||||
Array.from(new Uint8Array(sigBytes))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
|
||||
return timingSafeStringEq(expected, signatureHeader);
|
||||
}
|
||||
Reference in New Issue
Block a user