Files
vibn-agent-runner/vibn-frontend/lib/server/dev-server-state.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

141 lines
4.3 KiB
TypeScript

/**
* Persistent dev-server configuration store.
* Closes BETA_LAUNCH_PLAN P6.B1.
*
* When `dev_server_start` succeeds, the MCP tool should call
* `upsertDevServerConfig` so the project page can auto-resume the
* server on next mount without requiring the user to re-type the
* command (see P6.B2 for the auto-resume hook).
*
* Schema:
* fs_project_dev_servers
* project_id UUID PK → fs_projects.id
* command TEXT NOT NULL e.g. "cd myapp && npm run dev"
* port INT NOT NULL e.g. 3000
* framework TEXT e.g. "nextjs", "vite", "express"
* preview_url TEXT last known *.preview.vibnai.com URL
* last_started_at TIMESTAMPTZ
* status TEXT CHECK IN ('running','stopped','crashed')
* updated_at TIMESTAMPTZ DEFAULT NOW()
*/
import { query } from "@/lib/db-postgres";
import { log } from "@/lib/server/logger";
let tableReady = false;
async function ensureTable() {
if (tableReady) return;
await query(`
CREATE TABLE IF NOT EXISTS fs_project_dev_servers (
project_id TEXT PRIMARY KEY,
command TEXT NOT NULL,
port INT NOT NULL,
framework TEXT,
preview_url TEXT,
last_started_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'stopped'
CHECK (status IN ('running', 'stopped', 'crashed')),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
tableReady = true;
}
export interface DevServerConfig {
projectId: string;
command: string;
port: number;
framework?: string;
previewUrl?: string;
status: "running" | "stopped" | "crashed";
}
/** Called by the MCP dev_server_start handler after a successful start. */
export async function upsertDevServerConfig(
cfg: DevServerConfig,
): Promise<void> {
try {
await ensureTable();
await query(
`INSERT INTO fs_project_dev_servers
(project_id, command, port, framework, preview_url, last_started_at, status, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW(), $6, NOW())
ON CONFLICT (project_id) DO UPDATE SET
command = EXCLUDED.command,
port = EXCLUDED.port,
framework = COALESCE(EXCLUDED.framework, fs_project_dev_servers.framework),
preview_url = COALESCE(EXCLUDED.preview_url, fs_project_dev_servers.preview_url),
last_started_at = NOW(),
status = EXCLUDED.status,
updated_at = NOW()`,
[
cfg.projectId,
cfg.command,
cfg.port,
cfg.framework ?? null,
cfg.previewUrl ?? null,
cfg.status,
],
);
} catch (err) {
log.warn("dev-server-state: upsert failed (non-fatal)", {
projectId: cfg.projectId,
err: err instanceof Error ? err.message : String(err),
});
}
}
/** Update just the status (e.g. on stop / crash). */
export async function setDevServerStatus(
projectId: string,
status: "running" | "stopped" | "crashed",
): Promise<void> {
try {
await ensureTable();
await query(
`UPDATE fs_project_dev_servers
SET status = $2, updated_at = NOW()
WHERE project_id = $1`,
[projectId, status],
);
} catch (err) {
log.warn("dev-server-state: status update failed (non-fatal)", {
projectId,
err: err instanceof Error ? err.message : String(err),
});
}
}
/** Returns the last-known dev server config for a project, or null. */
export async function getDevServerConfig(
projectId: string,
): Promise<DevServerConfig | null> {
try {
await ensureTable();
const rows = await query<{
project_id: string;
command: string;
port: number;
framework: string | null;
preview_url: string | null;
status: string;
}>(
`SELECT project_id, command, port, framework, preview_url, status
FROM fs_project_dev_servers WHERE project_id = $1`,
[projectId],
);
if (!rows[0]) return null;
const r = rows[0];
return {
projectId: r.project_id,
command: r.command,
port: r.port,
framework: r.framework ?? undefined,
previewUrl: r.preview_url ?? undefined,
status: r.status as "running" | "stopped" | "crashed",
};
} catch {
return null;
}
}