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
141 lines
4.3 KiB
TypeScript
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;
|
|
}
|
|
}
|