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
27 lines
1.1 KiB
TypeScript
27 lines
1.1 KiB
TypeScript
/**
|
|
* Constant-time string comparison.
|
|
*
|
|
* Use this for every admin-secret / bearer-token / HMAC comparison. Naive
|
|
* `a === b` short-circuits on the first byte mismatch, leaking length
|
|
* information that an attacker can use to slow-search the secret.
|
|
*
|
|
* `crypto.timingSafeEqual` requires equal-length buffers and runs in
|
|
* constant time. We normalise to UTF-8 buffers, pad shorter to longer
|
|
* with zero bytes so length mismatch is also constant-time, and OR a
|
|
* length-mismatch flag at the end so different lengths can't return true.
|
|
*/
|
|
import { timingSafeEqual } from "crypto";
|
|
|
|
export function timingSafeStringEq(a: string, b: string): boolean {
|
|
const aBuf = Buffer.from(a, "utf8");
|
|
const bBuf = Buffer.from(b, "utf8");
|
|
const max = Math.max(aBuf.length, bBuf.length);
|
|
const aPadded = Buffer.alloc(max);
|
|
const bPadded = Buffer.alloc(max);
|
|
aBuf.copy(aPadded);
|
|
bBuf.copy(bPadded);
|
|
const equal = timingSafeEqual(aPadded, bPadded);
|
|
// Length mismatch defeats the compare even if padded prefixes happen to match.
|
|
return equal && aBuf.length === bBuf.length;
|
|
}
|