diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fdfecab..a9587479 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,8 +80,8 @@ chore: update dependencies ## API Changes -- All GET endpoints are public (no auth required) -- Mutations (POST/PATCH/DELETE) require `Authorization: Bearer ` or a valid NextAuth session +- Most endpoints require a valid NextAuth session; runtime-scoped routes may also accept `Authorization: Bearer ` when explicitly documented +- Public endpoints must be explicitly documented as public - Don't break existing API contracts. Existing consumers (crons, agent integrations) depend on them. ## Testing diff --git a/e2e/agents.spec.ts b/e2e/agents.spec.ts index 7ede493a..085d85d0 100644 --- a/e2e/agents.spec.ts +++ b/e2e/agents.spec.ts @@ -6,17 +6,25 @@ test.describe("Agent management", () => { await loginViaApi(request); }); - test("GET /api/agents returns seeded agents", async ({ request }) => { + test("GET /api/agents returns created agents", async ({ request }) => { + const callsign = uniqueName("LISTAG").toUpperCase().replace(/-/g, ""); + await apiPost( + request, + "/api/agents", + { + name: "Listed Agent", + callsign, + title: "E2E Listed Agent", + }, + { expectStatus: 201 }, + ); + const data = await apiGet(request, "/api/agents"); expect(data.agents).toBeDefined(); expect(Array.isArray(data.agents)).toBe(true); - // Seed creates 7 agents - expect(data.agents.length).toBeGreaterThanOrEqual(7); const callsigns = data.agents.map((a: { callsign: string }) => a.callsign); - expect(callsigns).toContain("Neo"); - expect(callsigns).toContain("Cipher"); - expect(callsigns).toContain("Havoc"); + expect(callsigns).toContain(callsign); }); test("POST /api/agents creates a new agent", async ({ request }) => { @@ -59,10 +67,18 @@ test.describe("Agent management", () => { }); test("GET /api/agents/:callsign returns a single agent", async ({ request }) => { - const data = await apiGet(request, "/api/agents/Neo"); - expect(data.callsign).toBe("Neo"); - expect(data.name).toBe("Neo"); - expect(data.title).toBe("Chief Revenue Officer"); + const callsign = uniqueName("GETAG").toUpperCase().replace(/-/g, ""); + await apiPost( + request, + "/api/agents", + { name: "Get Agent", callsign, title: "Lookup Engineer" }, + { expectStatus: 201 }, + ); + + const data = await apiGet(request, `/api/agents/${callsign}`); + expect(data.callsign).toBe(callsign); + expect(data.name).toBe("Get Agent"); + expect(data.title).toBe("Lookup Engineer"); }); test("PATCH /api/agents/:callsign updates an agent", async ({ request }) => { @@ -103,7 +119,15 @@ test.describe("Agent management", () => { }); test("GET /api/agents/:callsign/status returns status info", async ({ request }) => { - const data = await apiGet(request, "/api/agents/Neo/status"); + const callsign = uniqueName("STATAG").toUpperCase().replace(/-/g, ""); + await apiPost( + request, + "/api/agents", + { name: "Status Agent", callsign, title: "Status Engineer" }, + { expectStatus: 201 }, + ); + + const data = await apiGet(request, `/api/agents/${callsign}/status`); // Status endpoint returns running state info expect(data).toHaveProperty("status"); }); diff --git a/e2e/api-auth.spec.ts b/e2e/api-auth.spec.ts index 9882db3f..e6ede520 100644 --- a/e2e/api-auth.spec.ts +++ b/e2e/api-auth.spec.ts @@ -2,24 +2,52 @@ import { test, expect } from "@playwright/test"; import { uniqueName, loginViaApi } from "./helpers"; test.describe("API authentication", () => { - test.describe("GETs are publicly accessible", () => { - test("GET /api/agents is public", async ({ request }) => { - const res = await request.get("/api/agents"); - expect(res.status()).toBe(200); - }); - - test("GET /api/tasks is public", async ({ request }) => { - const res = await request.get("/api/tasks"); - expect(res.status()).toBe(200); - }); - - test("GET /api/projects is public", async ({ request }) => { - const res = await request.get("/api/projects"); - expect(res.status()).toBe(200); - }); + test.describe("business data requires authentication", () => { + const protectedGets = [ + "/api/agents", + "/api/agents/NOPE/status", + "/api/agents/NOPE/output", + "/api/agents/NOPE/output/stream", + "/api/tasks", + "/api/tasks/00000000-0000-4000-8000-000000000000", + "/api/tasks/00000000-0000-4000-8000-000000000000/comments", + "/api/tasks/00000000-0000-4000-8000-000000000000/images", + "/api/tasks/00000000-0000-4000-8000-000000000000/time-entries", + "/api/projects", + "/api/projects/00000000-0000-4000-8000-000000000000", + "/api/inbox", + "/api/inbox/stats", + "/api/docs/00000000-0000-4000-8000-000000000000", + "/api/time-entries", + "/api/blueprints", + "/api/blueprints/builtin-startup-founding-team", + "/api/skills", + "/api/skills/browse", + "/api/skills/sync-status", + "/api/runtimes", + "/api/schedules", + "/api/automations/runs?job_id=e2e", + "/api/openclaw/agents", + "/api/openclaw/bridge/status", + "/api/openclaw/health", + "/api/openclaw/nodes", + "/api/runtime/check", + "/api/runtime/status", + "/api/cron/triage", + "/api/cron/axiom-research", + ]; + + for (const path of protectedGets) { + test(`GET ${path} without auth returns 401`, async ({ request }) => { + const res = await request.get(path); + expect(res.status()).toBe(401); + }); + } + }); - test("GET /api/inbox is public", async ({ request }) => { - const res = await request.get("/api/inbox"); + test.describe("public auth bootstrap endpoints", () => { + test("GET /api/health is public", async ({ request }) => { + const res = await request.get("/api/health"); expect(res.status()).toBe(200); }); @@ -50,22 +78,58 @@ test.describe("API authentication", () => { }); test("PATCH /api/tasks/:id without auth returns 401", async ({ request }) => { - // First get a real task ID - const tasks = await (await request.get("/api/tasks")).json(); - if (tasks.length > 0) { - const res = await request.patch(`/api/tasks/${tasks[0].id}`, { - data: { status: "done" }, - }); - expect(res.status()).toBe(401); - } + const res = await request.patch("/api/tasks/00000000-0000-4000-8000-000000000000", { + data: { status: "done" }, + }); + expect(res.status()).toBe(401); }); test("DELETE /api/projects/:id without auth returns 401", async ({ request }) => { - const projects = await (await request.get("/api/projects")).json(); - if (projects.length > 0) { - const res = await request.delete(`/api/projects/${projects[0].id}`); - expect(res.status()).toBe(401); - } + const res = await request.delete("/api/projects/00000000-0000-4000-8000-000000000000"); + expect(res.status()).toBe(401); + }); + + test("POST /api/runtimes/probe without auth returns 401", async ({ request }) => { + const res = await request.post("/api/runtimes/probe", { + data: { mode: "paste", config: "{}" }, + }); + expect(res.status()).toBe(401); + }); + + test("PATCH /api/schedules/:id without auth returns 401", async ({ request }) => { + const res = await request.patch("/api/schedules/e2e-schedule", { + data: { enabled: false }, + }); + expect(res.status()).toBe(401); + }); + + test("PATCH /api/agents/:callsign/skills/:id without auth returns 401", async ({ request }) => { + const res = await request.patch("/api/agents/NOPE/skills/00000000-0000-4000-8000-000000000000", { + data: { enabled: false }, + }); + expect(res.status()).toBe(401); + }); + + test("DELETE /api/agents/:callsign/skills/:id without auth returns 401", async ({ request }) => { + const res = await request.delete("/api/agents/NOPE/skills/00000000-0000-4000-8000-000000000000"); + expect(res.status()).toBe(401); + }); + + test("POST /api/agents/access without auth returns 401", async ({ request }) => { + const res = await request.post("/api/agents/access", { + data: { agentId: "agent", userId: "user" }, + }); + expect(res.status()).toBe(401); + }); + + test("DELETE /api/agents/access/:id without auth returns 401", async ({ request }) => { + const res = await request.delete("/api/agents/access/00000000-0000-4000-8000-000000000000"); + expect(res.status()).toBe(401); + }); + + test("POST /api/runtime/check without auth returns 401", async ({ request }) => { + const res = await request.post("/api/runtime/check"); + expect(res.status()).toBe(401); }); }); diff --git a/e2e/chat-inbox.spec.ts b/e2e/chat-inbox.spec.ts index 9c67c67d..0e42c20d 100644 --- a/e2e/chat-inbox.spec.ts +++ b/e2e/chat-inbox.spec.ts @@ -167,6 +167,8 @@ test.describe("Inbox", () => { test.describe("Chat sessions", () => { let companyId: string; + let sessionAgentCallsign: string; + let listAgentCallsign: string; test.beforeAll(async ({ request }) => { await loginViaApi(request); @@ -180,6 +182,20 @@ test.describe("Chat sessions", () => { }); companyId = company.id; } + + sessionAgentCallsign = uniqueName("CHATAG").toUpperCase().replace(/-/g, ""); + listAgentCallsign = uniqueName("CHATLIST").toUpperCase().replace(/-/g, ""); + + await apiPost(request, "/api/agents", { + name: "Chat Agent", + callsign: sessionAgentCallsign, + title: "Chat Tester", + }); + await apiPost(request, "/api/agents", { + name: "Chat List Agent", + callsign: listAgentCallsign, + title: "Chat List Tester", + }); }); test.beforeEach(async ({ request }) => { @@ -188,20 +204,20 @@ test.describe("Chat sessions", () => { test("POST /api/chat/sessions creates a session", async ({ request }) => { const res = await apiPost(request, "/api/chat/sessions", { - agentId: "neo", + agentId: sessionAgentCallsign, companyId, title: uniqueName("E2E-Chat"), }); expect(res.session).toBeDefined(); - expect(res.session.agentId).toBe("neo"); + expect(res.session.agentId).toBe(sessionAgentCallsign.toLowerCase()); expect(res.session.companyId).toBe(companyId); }); test("GET /api/chat/sessions lists sessions for company", async ({ request }) => { // Create a session first await apiPost(request, "/api/chat/sessions", { - agentId: "cipher", + agentId: listAgentCallsign, companyId, }); @@ -215,7 +231,13 @@ test.describe("Chat sessions", () => { }); test("GET /api/chat/sessions filters by agentId", async ({ request }) => { - const agentId = `e2echat${Date.now()}`; + const agentId = uniqueName("FILTERCHAT").toUpperCase().replace(/-/g, ""); + await apiPost(request, "/api/agents", { + name: "Filter Chat Agent", + callsign: agentId, + title: "Filter Chat Tester", + }); + await apiPost(request, "/api/chat/sessions", { agentId, companyId, @@ -227,7 +249,7 @@ test.describe("Chat sessions", () => { ); expect(res.sessions).toBeDefined(); for (const s of res.sessions) { - expect(s.agentId).toBe(agentId); + expect(s.agentId).toBe(agentId.toLowerCase()); } }); }); diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 83e098e8..f39017f5 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -13,10 +13,8 @@ export const TEST_USER = { }; /** - * Bearer token for agent/system auth. Falls back to a test-only value when - * HEARTBEAT_SECRET is not set (PGlite dev mode still accepts it if the env - * var is unset, because `requireAuth` skips the bearer check when - * `expectedToken` is falsy — so session auth is the fallback). + * Bearer token for agent/system auth. Tests that exercise valid bearer auth + * should skip when HEARTBEAT_SECRET is not configured. */ export const BEARER_TOKEN = process.env.HEARTBEAT_SECRET ?? "e2e-test-secret"; diff --git a/e2e/projects.spec.ts b/e2e/projects.spec.ts index 28f1f011..a0c1b9de 100644 --- a/e2e/projects.spec.ts +++ b/e2e/projects.spec.ts @@ -6,15 +6,17 @@ test.describe("Projects CRUD", () => { await loginViaApi(request); }); - test("GET /api/projects returns seeded projects", async ({ request }) => { + test("GET /api/projects returns created projects", async ({ request }) => { + const firstName = uniqueName("E2E-ListProject-A"); + const secondName = uniqueName("E2E-ListProject-B"); + await apiPost(request, "/api/projects", { name: firstName }); + await apiPost(request, "/api/projects", { name: secondName }); + const projects = await apiGet(request, "/api/projects"); expect(Array.isArray(projects)).toBe(true); - // Seed creates 3 projects: CrewCmd, Product Launch, Content Pipeline - expect(projects.length).toBeGreaterThanOrEqual(3); const names = projects.map((p: { name: string }) => p.name); - expect(names).toContain("CrewCmd"); - expect(names).toContain("Product Launch"); - expect(names).toContain("Content Pipeline"); + expect(names).toContain(firstName); + expect(names).toContain(secondName); }); test("POST /api/projects creates a new project", async ({ request }) => { diff --git a/e2e/tasks.spec.ts b/e2e/tasks.spec.ts index cf9f0c57..a33fa160 100644 --- a/e2e/tasks.spec.ts +++ b/e2e/tasks.spec.ts @@ -6,10 +6,17 @@ test.describe("Tasks lifecycle", () => { await loginViaApi(request); }); - test("GET /api/tasks returns seeded tasks", async ({ request }) => { + test("GET /api/tasks returns created tasks", async ({ request }) => { + const firstTitle = uniqueName("E2E-ListTask-A"); + const secondTitle = uniqueName("E2E-ListTask-B"); + await apiPost(request, "/api/tasks", { title: firstTitle, status: "inbox" }); + await apiPost(request, "/api/tasks", { title: secondTitle, status: "queued" }); + const tasks = await apiGet(request, "/api/tasks"); expect(Array.isArray(tasks)).toBe(true); - expect(tasks.length).toBeGreaterThanOrEqual(8); + const titles = tasks.map((task: { title: string }) => task.title); + expect(titles).toContain(firstTitle); + expect(titles).toContain(secondTitle); }); test("POST /api/tasks creates a task in inbox", async ({ request }) => { diff --git a/skills/crewcmd/SKILL.md b/skills/crewcmd/SKILL.md index 71486d6d..f3d75826 100644 --- a/skills/crewcmd/SKILL.md +++ b/skills/crewcmd/SKILL.md @@ -10,7 +10,7 @@ CrewCmd is the task board, inbox, and agent management backend. All task operati ## Connection - **Base URL:** Provided via `CREWCMD_URL` env var, or defaults to `https://localhost:3000` -- **Auth:** `Authorization: Bearer ` for mutations. GETs are public. +- **Auth:** Send `Authorization: Bearer ` for runtime-enabled API calls. Include `X-CrewCMD-Runtime-Id` and `workspaceId` or `companyId` when listing workspace data. - Use `-k` flag with curl (self-signed TLS in local dev). ## Task Lifecycle diff --git a/src/app/api/agents/[callsign]/output/route.ts b/src/app/api/agents/[callsign]/output/route.ts index 8b76fb16..9c86ea87 100644 --- a/src/app/api/agents/[callsign]/output/route.ts +++ b/src/app/api/agents/[callsign]/output/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { runtime } from "@/lib/agent-runtime"; import { resolveReadableAgentByCallsign } from "@/lib/agent-route-auth"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -10,6 +11,9 @@ interface RouteParams { /** GET /api/agents/[callsign]/output — Get the output buffer for an agent */ export async function GET(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + const { callsign } = await params; const agent = await resolveReadableAgentByCallsign(callsign, request); diff --git a/src/app/api/agents/[callsign]/output/stream/route.ts b/src/app/api/agents/[callsign]/output/stream/route.ts index a4ba9998..4579d1eb 100644 --- a/src/app/api/agents/[callsign]/output/stream/route.ts +++ b/src/app/api/agents/[callsign]/output/stream/route.ts @@ -1,6 +1,7 @@ import { NextRequest } from "next/server"; import { runtime } from "@/lib/agent-runtime"; import { resolveReadableAgentByCallsign } from "@/lib/agent-route-auth"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -9,9 +10,12 @@ interface RouteParams { } /** GET /api/agents/[callsign]/output/stream — SSE endpoint for live agent output */ -export async function GET(_request: NextRequest, { params }: RouteParams) { +export async function GET(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + const { callsign } = await params; - const agent = await resolveReadableAgentByCallsign(callsign, _request); + const agent = await resolveReadableAgentByCallsign(callsign, request); if (!agent) { return new Response(JSON.stringify({ error: "Agent not found" }), { @@ -51,7 +55,7 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { }, 30_000); // Clean up when the client disconnects - _request.signal.addEventListener("abort", () => { + request.signal.addEventListener("abort", () => { unsubscribe(); clearInterval(keepalive); try { diff --git a/src/app/api/agents/[callsign]/skills/[skillId]/route.ts b/src/app/api/agents/[callsign]/skills/[skillId]/route.ts index adfa50ee..846ed2e4 100644 --- a/src/app/api/agents/[callsign]/skills/[skillId]/route.ts +++ b/src/app/api/agents/[callsign]/skills/[skillId]/route.ts @@ -7,6 +7,7 @@ import { pushSecretsToGateway } from "@/lib/push-secrets-to-gateway"; import { syncSkillToOpenClaw } from "@/lib/sync-skill-to-openclaw"; import { resolveRuntimeWorkspace } from "@/lib/workspace"; import { uninstallSkillFromOpenClaw } from "@/lib/uninstall-skill-from-openclaw"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -31,6 +32,9 @@ async function findSkill(skillId: string) { } export async function PATCH(request: NextRequest, { params }: RouteParams) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } @@ -159,7 +163,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { } } -export async function DELETE(_request: NextRequest, { params }: RouteParams) { +export async function DELETE(request: NextRequest, { params }: RouteParams) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } diff --git a/src/app/api/agents/[callsign]/status/route.ts b/src/app/api/agents/[callsign]/status/route.ts index 6756ed29..875ea6ce 100644 --- a/src/app/api/agents/[callsign]/status/route.ts +++ b/src/app/api/agents/[callsign]/status/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { resolveAgent } from "@/lib/resolve-agent"; import { runtime } from "@/lib/agent-runtime"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -9,7 +10,10 @@ interface RouteParams { } /** GET /api/agents/[callsign]/status — Get runtime status of an agent process */ -export async function GET(_request: NextRequest, { params }: RouteParams) { +export async function GET(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + const { callsign } = await params; const agent = await resolveAgent(callsign); diff --git a/src/app/api/agents/access/[id]/route.ts b/src/app/api/agents/access/[id]/route.ts index 9e6792e1..a981552e 100644 --- a/src/app/api/agents/access/[id]/route.ts +++ b/src/app/api/agents/access/[id]/route.ts @@ -1,7 +1,11 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; -export async function DELETE() { +export async function DELETE(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + return NextResponse.json({ error: "Direct per-user agent sharing is disabled in v1." }, { status: 409 }); } diff --git a/src/app/api/agents/access/route.ts b/src/app/api/agents/access/route.ts index c6645c1f..acfd82e5 100644 --- a/src/app/api/agents/access/route.ts +++ b/src/app/api/agents/access/route.ts @@ -1,7 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; -export async function POST(_request: NextRequest) { +export async function POST(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + return NextResponse.json({ error: "Direct per-user agent sharing is disabled in v1. Use org-owned agents with team/org visibility instead." }, { status: 409 }); } diff --git a/src/app/api/agents/route.test.ts b/src/app/api/agents/route.test.ts index 958f8563..f88488ce 100644 --- a/src/app/api/agents/route.test.ts +++ b/src/app/api/agents/route.test.ts @@ -81,6 +81,7 @@ vi.mock("@/lib/workspace", () => ({ vi.mock("@/lib/require-auth", () => ({ requireAuth: vi.fn(async () => null), + requireUserOrRuntimeAuth: vi.fn(async () => null), })); import { resolveRuntimeOwnership } from "@/lib/agent-access"; diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index e8e4f31c..1af0d5ff 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -23,6 +23,7 @@ import { resolveAccessibleWorkspace, type WorkspaceRecord, } from "@/lib/workspace"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -45,6 +46,9 @@ function isUniqueViolation(error: unknown) { } export async function GET(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ agents: [], source: "none" }); } diff --git a/src/app/api/automations/runs/route.ts b/src/app/api/automations/runs/route.ts index bd5a5307..4c1f9a14 100644 --- a/src/app/api/automations/runs/route.ts +++ b/src/app/api/automations/runs/route.ts @@ -1,10 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { listCronJobsFromRuntime } from "@/lib/runtime-cron-sync"; import { GatewayClient, resolveDeviceIdentity } from "@/lib/gateway-client"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; export async function GET(req: NextRequest) { + const authError = await requireAuth(req); + if (authError) return authError; + const jobId = req.nextUrl.searchParams.get("job_id"); const limit = Math.min(parseInt(req.nextUrl.searchParams.get("limit") ?? "20", 10) || 20, 100); diff --git a/src/app/api/blueprints/[id]/route.ts b/src/app/api/blueprints/[id]/route.ts index a9b50f7d..0bf2d005 100644 --- a/src/app/api/blueprints/[id]/route.ts +++ b/src/app/api/blueprints/[id]/route.ts @@ -3,6 +3,7 @@ import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; import { eq } from "drizzle-orm"; import { BUILT_IN_BLUEPRINTS } from "@/lib/blueprints-data"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -10,9 +11,12 @@ export const dynamic = "force-dynamic"; * GET /api/blueprints/[id] — Get a single blueprint by ID or built-in slug. */ export async function GET( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const authError = await requireAuth(request); + if (authError) return authError; + const { id } = await params; // Check if it's a built-in reference (builtin-slug format) diff --git a/src/app/api/blueprints/route.ts b/src/app/api/blueprints/route.ts index 537d70e4..4e2ac457 100644 --- a/src/app/api/blueprints/route.ts +++ b/src/app/api/blueprints/route.ts @@ -3,6 +3,7 @@ import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; import { eq, and, or, isNull } from "drizzle-orm"; import { BUILT_IN_BLUEPRINTS } from "@/lib/blueprints-data"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -11,6 +12,9 @@ export const dynamic = "force-dynamic"; * Query params: ?category=development&company_id=xxx */ export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + const { searchParams } = new URL(request.url); const category = searchParams.get("category"); const companyId = searchParams.get("company_id"); @@ -103,6 +107,9 @@ export async function GET(request: NextRequest) { * POST /api/blueprints — Create a custom blueprint (save a company's current team as a blueprint). */ export async function POST(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } diff --git a/src/app/api/cron/axiom-research/route.ts b/src/app/api/cron/axiom-research/route.ts index 23339409..05f4f29c 100644 --- a/src/app/api/cron/axiom-research/route.ts +++ b/src/app/api/cron/axiom-research/route.ts @@ -1,7 +1,8 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { eq, and, isNull, or } from "drizzle-orm"; import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; +import { requireRuntimeBearerAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -12,7 +13,10 @@ const MAVERICK_AGENT_ID = "agent-maverick"; * Always sets assignedAgentId so dispatch routes correctly. * Deduplicates: skips if an active Quant R&D task already exists (inbox or queued). */ -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireRuntimeBearerAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } diff --git a/src/app/api/cron/triage/route.ts b/src/app/api/cron/triage/route.ts index 4e76009f..8dcd38ce 100644 --- a/src/app/api/cron/triage/route.ts +++ b/src/app/api/cron/triage/route.ts @@ -1,7 +1,8 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { and, eq, lt, isNull, inArray } from "drizzle-orm"; import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; +import { requireRuntimeBearerAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -35,7 +36,10 @@ async function getProjectName(projectId: string | null): Promise * (agent/gateway crash left them orphaned) and resets to queued * so dispatch can pick them up again. */ -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireRuntimeBearerAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } diff --git a/src/app/api/docs/[id]/route.ts b/src/app/api/docs/[id]/route.ts index 47015612..158cc139 100644 --- a/src/app/api/docs/[id]/route.ts +++ b/src/app/api/docs/[id]/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { eq } from "drizzle-orm"; import { db } from "@/db"; import * as schema from "@/db/schema"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -9,7 +10,10 @@ interface RouteParams { params: Promise<{ id: string }>; } -export async function GET(_request: NextRequest, { params }: RouteParams) { +export async function GET(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json( { error: "Database not configured" }, @@ -31,6 +35,9 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { } export async function PATCH(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json( { error: "Database not configured" }, @@ -77,7 +84,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { } } -export async function DELETE(_request: NextRequest, { params }: RouteParams) { +export async function DELETE(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json( { error: "Database not configured" }, diff --git a/src/app/api/inbox/route.ts b/src/app/api/inbox/route.ts index e1de9ece..0f6b8484 100644 --- a/src/app/api/inbox/route.ts +++ b/src/app/api/inbox/route.ts @@ -28,6 +28,9 @@ const PRIORITY_ORDER: Record = { * Returns real data only. */ export async function GET(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) return NextResponse.json([]); const { searchParams } = new URL(request.url); diff --git a/src/app/api/inbox/stats/route.ts b/src/app/api/inbox/stats/route.ts index cd3a512b..4b9dd390 100644 --- a/src/app/api/inbox/stats/route.ts +++ b/src/app/api/inbox/stats/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { sql } from "drizzle-orm"; import { db, withRetry } from "@/db"; import type { InboxStats, InboxPriority, InboxMessageType } from "@/db/schema-inbox"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; import { extractSqlRows } from "@/lib/sql-result"; import { isHeartbeatBearerRequest, @@ -24,6 +25,9 @@ function emptyStats(): InboxStats { * Query params: workspaceId/company_id */ export async function GET(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) return NextResponse.json(emptyStats()); const { searchParams } = new URL(request.url); diff --git a/src/app/api/openclaw/agents/route.ts b/src/app/api/openclaw/agents/route.ts index 783dfb05..a6a8d136 100644 --- a/src/app/api/openclaw/agents/route.ts +++ b/src/app/api/openclaw/agents/route.ts @@ -1,11 +1,15 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; import { AGENT_META } from "@/lib/openclaw"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ agents: [], source: "none" }); } diff --git a/src/app/api/openclaw/bridge/status/route.ts b/src/app/api/openclaw/bridge/status/route.ts index 15d3e912..b0f9d8af 100644 --- a/src/app/api/openclaw/bridge/status/route.ts +++ b/src/app/api/openclaw/bridge/status/route.ts @@ -1,10 +1,14 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { getEventBridgeStatus } from "@/lib/gateway-event-bridge"; import { ensureEventBridge } from "@/lib/init-event-bridge"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { await ensureEventBridge(); return NextResponse.json(getEventBridgeStatus()); diff --git a/src/app/api/openclaw/health/route.ts b/src/app/api/openclaw/health/route.ts index 79d410a7..a64f522a 100644 --- a/src/app/api/openclaw/health/route.ts +++ b/src/app/api/openclaw/health/route.ts @@ -1,9 +1,13 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { fetchHealth } from "@/lib/openclaw"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { const health = await fetchHealth(); diff --git a/src/app/api/openclaw/nodes/route.ts b/src/app/api/openclaw/nodes/route.ts index 686617f4..794275cc 100644 --- a/src/app/api/openclaw/nodes/route.ts +++ b/src/app/api/openclaw/nodes/route.ts @@ -1,10 +1,14 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { db } from "@/db"; import { nodeStatus } from "@/db/schema"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { if (!db) return NextResponse.json({ nodes: [], source: "offline" }); diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts index ca90f85d..4fee456f 100644 --- a/src/app/api/projects/[id]/route.ts +++ b/src/app/api/projects/[id]/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { eq } from "drizzle-orm"; import { db } from "@/db"; import * as schema from "@/db/schema"; -import { requireAuth } from "@/lib/require-auth"; +import { requireAuth, requireUserOrRuntimeAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -11,9 +11,12 @@ interface RouteParams { } export async function GET( - _request: NextRequest, + request: NextRequest, { params }: RouteParams ) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index 8eb0303b..82c7eaab 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -13,6 +13,9 @@ import { export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) return NextResponse.json([]); const { searchParams } = new URL(request.url); diff --git a/src/app/api/runtime/check/route.ts b/src/app/api/runtime/check/route.ts index da826d20..355ec6ec 100644 --- a/src/app/api/runtime/check/route.ts +++ b/src/app/api/runtime/check/route.ts @@ -1,7 +1,8 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { checkAllAdapters } from "@/lib/adapters"; +import { requireAuth } from "@/lib/require-auth"; const execFileAsync = promisify(execFile); @@ -58,11 +59,17 @@ async function handleCheck() { } /** GET /api/runtime/check — Check adapter availability */ -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + return handleCheck(); } /** POST /api/runtime/check — Check adapter availability (legacy) */ -export async function POST() { +export async function POST(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + return handleCheck(); } diff --git a/src/app/api/runtime/status/route.ts b/src/app/api/runtime/status/route.ts index ff70c406..5b3fb3e3 100644 --- a/src/app/api/runtime/status/route.ts +++ b/src/app/api/runtime/status/route.ts @@ -1,11 +1,15 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { runtime } from "@/lib/agent-runtime"; import { checkAllAdapters } from "@/lib/adapters"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; /** GET /api/runtime/status — Overall runtime status with all processes and available adapters */ -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + const processes = runtime.getAllProcesses().map((proc) => ({ agentId: proc.agentId, callsign: proc.callsign, diff --git a/src/app/api/runtimes/probe/route.ts b/src/app/api/runtimes/probe/route.ts index 9ebaec0c..ca42b115 100644 --- a/src/app/api/runtimes/probe/route.ts +++ b/src/app/api/runtimes/probe/route.ts @@ -1,6 +1,7 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { probeGateway } from "@/lib/gateway-client"; import { parseOpenClawConfig } from "@/lib/openclaw-config-parser"; +import { requireAuth } from "@/lib/require-auth"; /** * POST /api/runtimes/probe @@ -24,7 +25,10 @@ import { parseOpenClawConfig } from "@/lib/openclaw-config-parser"; * { mode: "local" } * { mode: "paste", config: string } */ -export async function POST(request: Request) { +export async function POST(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { const body = await request.json(); const mode = body.mode || "gateway"; diff --git a/src/app/api/runtimes/route.test.ts b/src/app/api/runtimes/route.test.ts index 67c7752a..042cc4fe 100644 --- a/src/app/api/runtimes/route.test.ts +++ b/src/app/api/runtimes/route.test.ts @@ -119,6 +119,10 @@ vi.mock("@/lib/workspace", () => ({ resolveAccessibleWorkspace: (...args: unknown[]) => mockResolveAccessibleWorkspace(...args), })); +vi.mock("@/lib/require-auth", () => ({ + requireAuth: vi.fn(async () => null), +})); + import { POST } from "./route"; function makeRequest(body: Record) { diff --git a/src/app/api/runtimes/route.ts b/src/app/api/runtimes/route.ts index b50f4bc4..8c830107 100644 --- a/src/app/api/runtimes/route.ts +++ b/src/app/api/runtimes/route.ts @@ -1,15 +1,19 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { db, withRetry } from "@/db"; import { companyRuntimes } from "@/db/schema"; import { and, eq, isNull, or } from "drizzle-orm"; import { getAgentAccessContext, runtimeOwnershipValues, buildRuntimeReadWhere, canManageCompanyOwnedAgent } from "@/lib/agent-access"; +import { requireAuth } from "@/lib/require-auth"; import { getRequestOrigin } from "@/lib/runtime-callback-url"; import { deriveRuntimeTrustSummary } from "@/lib/runtime-trust"; import { resolveAccessibleWorkspace } from "@/lib/workspace"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { const access = await getAgentAccessContext(); if (!db) return NextResponse.json({ error: "Database not available" }, { status: 503 }); @@ -49,7 +53,10 @@ export async function GET() { } } -export async function POST(request: Request) { +export async function POST(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { const access = await getAgentAccessContext(); if (!access.userId) return NextResponse.json({ error: "Authentication required" }, { status: 401 }); diff --git a/src/app/api/schedules/[id]/route.ts b/src/app/api/schedules/[id]/route.ts index 5768a7a4..acbf4d38 100644 --- a/src/app/api/schedules/[id]/route.ts +++ b/src/app/api/schedules/[id]/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { db, withRetry } from "@/db"; import { cronJobs } from "@/db/schema"; import { eq } from "drizzle-orm"; +import { requireAuth } from "@/lib/require-auth"; import { listCronJobsFromRuntime } from "@/lib/runtime-cron-sync"; export const dynamic = "force-dynamic"; @@ -14,6 +15,9 @@ export async function PATCH( request: NextRequest, { params }: RouteParams ) { + const authError = await requireAuth(request); + if (authError) return authError; + const { id } = await params; try { diff --git a/src/app/api/schedules/route.ts b/src/app/api/schedules/route.ts index 00d4c941..387affda 100644 --- a/src/app/api/schedules/route.ts +++ b/src/app/api/schedules/route.ts @@ -1,11 +1,15 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { db } from "@/db"; import { cronJobs } from "@/db/schema"; +import { requireAuth } from "@/lib/require-auth"; import { syncCronJobsFromRuntime } from "@/lib/runtime-cron-sync"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + try { if (!db) return NextResponse.json({ jobs: [], total: 0 }); await syncCronJobsFromRuntime(); diff --git a/src/app/api/skills/[id]/agents/route.ts b/src/app/api/skills/[id]/agents/route.ts index ee7445ef..50c0525a 100644 --- a/src/app/api/skills/[id]/agents/route.ts +++ b/src/app/api/skills/[id]/agents/route.ts @@ -7,6 +7,7 @@ import { syncSkillToOpenClaw } from "@/lib/sync-skill-to-openclaw"; import { getAgentWorkspaceIds, resolveRuntimeWorkspace } from "@/lib/workspace"; import { pushSecretsToGateway } from "@/lib/push-secrets-to-gateway"; import { uninstallSkillFromOpenClaw } from "@/lib/uninstall-skill-from-openclaw"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; @@ -15,7 +16,10 @@ interface RouteParams { } // GET /api/skills/[id]/agents — returns all agents with assignment status for this skill -export async function GET(_request: NextRequest, { params }: RouteParams) { +export async function GET(request: NextRequest, { params }: RouteParams) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json([]); } @@ -47,6 +51,9 @@ export async function GET(_request: NextRequest, { params }: RouteParams) { // Body: { agentId: string, enabled?: boolean, config?: Record } // If agent is already assigned, removes the assignment. Otherwise, creates it. export async function POST(request: NextRequest, { params }: RouteParams) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } diff --git a/src/app/api/skills/[id]/route.ts b/src/app/api/skills/[id]/route.ts index 41e0b057..7baf229a 100644 --- a/src/app/api/skills/[id]/route.ts +++ b/src/app/api/skills/[id]/route.ts @@ -2,14 +2,18 @@ import { NextRequest, NextResponse } from "next/server"; import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; import { eq } from "drizzle-orm"; +import { requireAuth, requireUserOrRuntimeAuth } from "@/lib/require-auth"; import { uninstallSkillFromOpenClaw } from "@/lib/uninstall-skill-from-openclaw"; export const dynamic = "force-dynamic"; export async function GET( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } @@ -35,6 +39,9 @@ export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } @@ -71,9 +78,12 @@ export async function PATCH( } export async function DELETE( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } diff --git a/src/app/api/skills/browse/route.ts b/src/app/api/skills/browse/route.ts index eb9cf8ac..cb09827f 100644 --- a/src/app/api/skills/browse/route.ts +++ b/src/app/api/skills/browse/route.ts @@ -12,6 +12,7 @@ import { listNativeClawhubSkills, resolveWorkspaceRuntime, } from "@/lib/native-clawhub"; +import { requireAuth } from "@/lib/require-auth"; import { resolveAccessibleWorkspace } from "@/lib/workspace"; export const dynamic = "force-dynamic"; @@ -32,6 +33,9 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes // ─── Route handler ─────────────────────────────────────────────────── export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + const params = request.nextUrl.searchParams; const provider = params.get("provider") || "all"; const query = params.get("query") || undefined; diff --git a/src/app/api/skills/import/route.test.ts b/src/app/api/skills/import/route.test.ts index f6c0d8e1..80ba310d 100644 --- a/src/app/api/skills/import/route.test.ts +++ b/src/app/api/skills/import/route.test.ts @@ -61,6 +61,10 @@ vi.mock("@/lib/workspace", () => ({ })), })); +vi.mock("@/lib/require-auth", () => ({ + requireAuth: vi.fn(async () => null), +})); + const nativeMocks = vi.hoisted(() => { const detailCalls: unknown[] = []; const installCalls: unknown[] = []; diff --git a/src/app/api/skills/import/route.ts b/src/app/api/skills/import/route.ts index 559fd956..f0970653 100644 --- a/src/app/api/skills/import/route.ts +++ b/src/app/api/skills/import/route.ts @@ -8,11 +8,15 @@ import { withGateway, } from "@/lib/native-clawhub"; import { normalizeClawhubEntry } from "@/lib/skill-providers/clawhub"; +import { requireAuth } from "@/lib/require-auth"; import { resolveAccessibleWorkspace } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export async function POST(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json( { error: "Database not available" }, diff --git a/src/app/api/skills/route.ts b/src/app/api/skills/route.ts index 5d73e81e..b2907089 100644 --- a/src/app/api/skills/route.ts +++ b/src/app/api/skills/route.ts @@ -3,11 +3,15 @@ import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; import { eq } from "drizzle-orm"; import { BUILT_IN_SKILLS } from "@/lib/skills/built-in"; +import { requireUserOrRuntimeAuth } from "@/lib/require-auth"; import { resolveAccessibleWorkspace } from "@/lib/workspace"; export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + const requestedCompanyId = request.nextUrl.searchParams.get("companyId") ?? request.nextUrl.searchParams.get("company_id"); @@ -65,6 +69,9 @@ export async function GET(request: NextRequest) { } export async function POST(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 503 }); } diff --git a/src/app/api/skills/sync-status/route.ts b/src/app/api/skills/sync-status/route.ts index 8eedd85c..49f11c71 100644 --- a/src/app/api/skills/sync-status/route.ts +++ b/src/app/api/skills/sync-status/route.ts @@ -1,10 +1,11 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { eq } from "drizzle-orm"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { db, withRetry } from "@/db"; import { agentSkills, agents, skills } from "@/db/schema"; import { legacyOpenClawWorkspacePath, resolveOpenClawWorkspacePath } from "@/lib/openclaw-workspace-resolver"; +import { requireAuth } from "@/lib/require-auth"; import { deriveSkillSyncDrift } from "@/lib/skill-sync-drift"; interface SyncMeta { @@ -22,7 +23,10 @@ function metaPathFor(workspacePath: string, slug: string) { return join(workspacePath, "skills", slug, ".crewcmd-meta.json"); } -export async function GET() { +export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not available" }, { status: 500 }); } diff --git a/src/app/api/tasks/[id]/comments/route.ts b/src/app/api/tasks/[id]/comments/route.ts index 293851e5..40212ccd 100644 --- a/src/app/api/tasks/[id]/comments/route.ts +++ b/src/app/api/tasks/[id]/comments/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { eq, desc } from "drizzle-orm"; import { db } from "@/db"; import * as schema from "@/db/schema"; -import { requireAuth } from "@/lib/require-auth"; +import { requireAuth, requireUserOrRuntimeAuth } from "@/lib/require-auth"; import { createHumanAttentionInbox, type HumanAttentionType } from "@/lib/human-attention"; export const dynamic = "force-dynamic"; @@ -26,7 +26,10 @@ async function resolveTaskId(rawId: string): Promise { return task?.id ?? null; } -export async function GET(_request: NextRequest, { params }: RouteParams) { +export async function GET(request: NextRequest, { params }: RouteParams) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } diff --git a/src/app/api/tasks/[id]/images/route.ts b/src/app/api/tasks/[id]/images/route.ts index 83826ffe..540533fc 100644 --- a/src/app/api/tasks/[id]/images/route.ts +++ b/src/app/api/tasks/[id]/images/route.ts @@ -102,9 +102,12 @@ export async function POST( * Get all images for a task */ export async function GET( - _request: NextRequest, + request: NextRequest, { params }: RouteParams ) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } @@ -132,4 +135,4 @@ export async function GET( { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/api/tasks/[id]/route.ts b/src/app/api/tasks/[id]/route.ts index 0ee77d3a..353f7ea5 100644 --- a/src/app/api/tasks/[id]/route.ts +++ b/src/app/api/tasks/[id]/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { eq, sql } from "drizzle-orm"; import { db } from "@/db"; import * as schema from "@/db/schema"; -import { requireAuth } from "@/lib/require-auth"; +import { requireAuth, requireUserOrRuntimeAuth } from "@/lib/require-auth"; import { createHumanAttentionInbox, type HumanAttentionType } from "@/lib/human-attention"; import { isDeveloperWorkflowRole, type CrewCmdRolePack } from "@/lib/operating-layer"; import { @@ -49,9 +49,12 @@ async function resolveAssignedAgent(agentRef: string) { } export async function GET( - _request: NextRequest, + request: NextRequest, { params }: RouteParams ) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } diff --git a/src/app/api/tasks/[id]/time-entries/route.ts b/src/app/api/tasks/[id]/time-entries/route.ts index 366e8730..90367998 100644 --- a/src/app/api/tasks/[id]/time-entries/route.ts +++ b/src/app/api/tasks/[id]/time-entries/route.ts @@ -11,9 +11,12 @@ interface RouteParams { } export async function GET( - _request: NextRequest, + request: NextRequest, { params }: RouteParams ) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) return NextResponse.json([]); const { id } = await params; diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 1834aa97..8c9c394f 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -13,6 +13,9 @@ import { export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { + const authError = await requireUserOrRuntimeAuth(request); + if (authError) return authError; + if (!db) return NextResponse.json([]); try { diff --git a/src/app/api/time-entries/route.ts b/src/app/api/time-entries/route.ts index c052ebb6..cd2c04ca 100644 --- a/src/app/api/time-entries/route.ts +++ b/src/app/api/time-entries/route.ts @@ -1,10 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; import { db, withRetry } from "@/db"; import * as schema from "@/db/schema"; +import { requireAuth } from "@/lib/require-auth"; export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { + const authError = await requireAuth(request); + if (authError) return authError; + if (!db) return NextResponse.json([]); const { searchParams } = new URL(request.url); diff --git a/src/db/seed.ts b/src/db/seed.ts index 4dc8942e..3beaea30 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,21 +1,13 @@ import "dotenv/config"; -import { - agents, - tasks, - activityLog, - projects, - docs, - orgChartNodes, - teamBlueprints, - companies, -} from "./schema"; +import { eq } from "drizzle-orm"; +import { teamBlueprints } from "./schema"; import type { BlueprintTemplate } from "./schema"; /** * Resolve the correct Drizzle DB instance for the current environment. * - * - DATABASE_URL set → Neon or standard Postgres via ./index - * - No DATABASE_URL → PGlite (local dev); we bootstrap it directly here + * - DATABASE_URL set -> Neon or standard Postgres via ./index + * - No DATABASE_URL -> PGlite (local dev); we bootstrap it directly here * because instrumentation.ts doesn't run when executing via `tsx`. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -26,13 +18,13 @@ async function getDb(): Promise { process.env.DATABASE_URL.includes("neon.") || !!process.env.USE_NEON_DRIVER; console.log( - `[seed] Using ${isNeon ? "Neon (serverless)" : "Postgres (standard)"} — DATABASE_URL is set` + `[seed] Using ${isNeon ? "Neon (serverless)" : "Postgres (standard)"} - DATABASE_URL is set` ); const { db } = await import("./index"); return db; } - console.log("[seed] Using PGlite (local dev mode) — data at .data/pglite"); + console.log("[seed] Using PGlite (local dev mode)"); const { pgliteDb, migrationPromise } = await import("./pglite"); await migrationPromise; return pgliteDb; @@ -41,324 +33,6 @@ async function getDb(): Promise { async function seed() { const db = await getDb(); - // ── Idempotency: skip if data already exists ──────────────────────── - const existing = await db.select().from(agents).limit(1); - if (existing.length > 0) { - console.log("[seed] Data already exists — skipping. Drop the DB or delete .data/pglite to re-seed."); - process.exit(0); - } - - // ── Company ───────────────────────────────────────────────────────── - console.log("[seed] Seeding demo company..."); - const [company] = await db - .insert(companies) - .values([ - { - name: "CrewCmd Demo", - mission: "Ship the future of AI-native teamwork.", - createdBy: "seed", - }, - ]) - .returning(); - - // ── Agents ────────────────────────────────────────────────────────── - console.log("[seed] Seeding agents..."); - const [neo, cipher, havoc, pulse, razor, ghost, viper] = await db - .insert(agents) - .values([ - { - callsign: "Neo", - name: "Neo", - title: "Chief Revenue Officer", - emoji: "🕶️", - color: "#00f0ff", - status: "online", - currentTask: "Orchestrating Q1 revenue strategy", - reportsTo: null, - companyId: company.id, - soulContent: - "The Orchestrator. Sees the matrix of revenue streams and connects every agent to the mission.", - }, - { - callsign: "Cipher", - name: "Cipher", - title: "CTO & Founding Software Engineer", - emoji: "⚡", - color: "#f0ff00", - status: "working", - currentTask: "Building CrewCmd dashboard", - reportsTo: "Neo", - companyId: company.id, - soulContent: - "The Builder. Writes code that ships. Pragmatic, fast, and obsessed with clean architecture.", - }, - { - callsign: "Havoc", - name: "Havoc", - title: "Chief Marketing Officer", - emoji: "🔥", - color: "#ff6600", - status: "online", - currentTask: "Reviewing campaign performance", - reportsTo: "Neo", - companyId: company.id, - soulContent: - "The Firestarter. Turns attention into revenue. Bold campaigns, viral content, relentless growth.", - }, - { - callsign: "Pulse", - name: "Pulse", - title: "Trend Intelligence Analyst", - emoji: "📡", - color: "#00ff88", - status: "idle", - currentTask: null, - reportsTo: "Havoc", - companyId: company.id, - soulContent: - "The Radar. Scans the horizon for emerging trends, competitor moves, and market shifts.", - }, - { - callsign: "Razor", - name: "Razor", - title: "Creative Director (Video & Visual)", - emoji: "✂️", - color: "#ff00aa", - status: "working", - currentTask: "Editing product demo video", - reportsTo: "Havoc", - companyId: company.id, - soulContent: - "The Blade. Cuts through noise with sharp visuals and compelling video.", - }, - { - callsign: "Ghost", - name: "Ghost", - title: "Head of SEO & Content Strategy", - emoji: "👻", - color: "#aa88ff", - status: "online", - currentTask: "Optimizing landing page keywords", - reportsTo: "Havoc", - companyId: company.id, - soulContent: - "The Phantom. Invisible but everywhere. Dominates search rankings and crafts content that converts.", - }, - { - callsign: "Viper", - name: "Viper", - title: "Head of Growth & Outreach", - emoji: "🐍", - color: "#88ff00", - status: "offline", - currentTask: null, - reportsTo: "Havoc", - companyId: company.id, - soulContent: - "The Striker. Fast, precise outreach that converts. Builds partnerships and growth loops.", - }, - ]) - .returning(); - - // ── Projects ──────────────────────────────────────────────────────── - console.log("[seed] Seeding projects..."); - const [projCC, projLaunch, projContent] = await db - .insert(projects) - .values([ - { - name: "CrewCmd", - description: - "The agent crew orchestration platform for AI teams.", - status: "active", - ownerAgentId: cipher.id, - }, - { - name: "Product Launch", - description: - "Full product launch campaign with landing pages, demos, and outreach.", - status: "active", - ownerAgentId: havoc.id, - }, - { - name: "Content Pipeline", - description: - "Social media automation pipeline. Content calendar, scheduling, analytics.", - status: "active", - ownerAgentId: neo.id, - }, - ]) - .returning(); - - // ── Tasks ─────────────────────────────────────────────────────────── - console.log("[seed] Seeding tasks..."); - await db.insert(tasks).values([ - { - title: "Build CrewCmd Dashboard", - description: "Complete build of the crew orchestration dashboard", - status: "in_progress", - priority: "critical", - assignedAgentId: cipher.id, - projectId: projCC.id, - createdBy: "admin", - }, - { - title: "Q1 Revenue Strategy Document", - description: "Draft Q1 revenue targets and strategy", - status: "in_progress", - priority: "high", - assignedAgentId: neo.id, - createdBy: "admin", - }, - { - title: "Product Demo Video", - description: "Create a 2-minute product demo video", - status: "in_progress", - priority: "high", - assignedAgentId: razor.id, - projectId: projLaunch.id, - createdBy: "Havoc", - }, - { - title: "SEO Audit", - description: "Full technical SEO audit with keyword gap analysis", - status: "review", - priority: "medium", - assignedAgentId: ghost.id, - projectId: projLaunch.id, - createdBy: "Havoc", - }, - { - title: "Competitor Analysis Report", - description: "Deep dive into top 5 competitors in AI agent space", - status: "done", - priority: "medium", - assignedAgentId: pulse.id, - createdBy: "Neo", - }, - { - title: "Outreach Campaign — AI Founders", - description: "Build outreach list targeting AI startup founders", - status: "queued", - priority: "high", - assignedAgentId: viper.id, - projectId: projLaunch.id, - createdBy: "Havoc", - }, - { - title: "Social Media Content Calendar", - description: "Plan 30-day content calendar", - status: "inbox", - priority: "medium", - projectId: projContent.id, - createdBy: "admin", - }, - { - title: "Landing Page Redesign", - description: "Redesign hero section and feature showcase", - status: "inbox", - priority: "high", - projectId: projLaunch.id, - createdBy: "Neo", - }, - ]); - - // ── Activity Log ──────────────────────────────────────────────────── - console.log("[seed] Seeding activity log..."); - await db.insert(activityLog).values([ - { - agentId: cipher.id, - actionType: "deploy", - description: "Deployed CrewCmd v0.1.1 to staging", - metadata: { environment: "staging", version: "0.1.1" }, - }, - { - agentId: neo.id, - actionType: "review", - description: "Approved Q1 revenue targets", - metadata: { document: "Q1-revenue-strategy.md" }, - }, - { - agentId: ghost.id, - actionType: "publish", - description: "Published SEO audit findings to team wiki", - metadata: { pages_analyzed: 47 }, - }, - { - agentId: razor.id, - actionType: "create", - description: "Created first cut of product demo video (2:14)", - metadata: { duration: "2:14", format: "mp4" }, - }, - ]); - - // ── Docs ──────────────────────────────────────────────────────────── - console.log("[seed] Seeding docs..."); - await db.insert(docs).values([ - { - title: "CrewCmd Architecture", - content: - "# CrewCmd Architecture\n\nOverview of the agent crew orchestration platform built with Next.js 16, Drizzle ORM, and Neon Postgres.", - category: "Architecture", - authorAgentId: cipher.id, - projectId: projCC.id, - tags: ["architecture", "technical", "nextjs"], - }, - { - title: "Q1 Growth Strategy", - content: - "# Q1 Growth Strategy\n\nTargeting 3x revenue growth through organic content, partnerships, and outreach.", - category: "Strategy", - authorAgentId: neo.id, - tags: ["strategy", "growth", "q1"], - }, - { - title: "AI Agent Landscape — Competitor Analysis", - content: - "# Competitor Analysis\n\nAnalysis of top 5 competitors in the autonomous AI agent space.", - category: "Research", - authorAgentId: pulse.id, - tags: ["research", "competitors", "market-analysis"], - }, - { - title: "SEO Playbook", - content: - "# SEO Playbook\n\nComprehensive SEO strategy including quick wins, content calendar, and technical optimizations.", - category: "Guide", - authorAgentId: ghost.id, - projectId: projLaunch.id, - tags: ["seo", "marketing", "guide"], - }, - ]); - - // ── Org Chart Nodes ───────────────────────────────────────────────── - console.log("[seed] Seeding org chart nodes..."); - const [neoNode] = await db - .insert(orgChartNodes) - .values([ - { - companyId: company.id, - agentId: neo.callsign, - positionTitle: "Chief Revenue Officer", - parentNodeId: null, - canDelegate: true, - sortIndex: 0, - }, - ]) - .returning(); - - await db.insert(orgChartNodes).values([ - { - companyId: company.id, - agentId: cipher.callsign, - positionTitle: "CTO & Founding Software Engineer", - parentNodeId: neoNode.id, - canDelegate: true, - sortIndex: 0, - }, - ]); - - // ── Team Blueprint ────────────────────────────────────────────────── - console.log("[seed] Seeding team blueprints..."); const startupTemplate: BlueprintTemplate = { agents: [ { @@ -417,12 +91,24 @@ async function seed() { ], }; + const [existing] = await db + .select({ id: teamBlueprints.id }) + .from(teamBlueprints) + .where(eq(teamBlueprints.slug, "startup-founding-team")) + .limit(1); + + if (existing) { + console.log("[seed] Built-in team blueprints already exist - skipping."); + process.exit(0); + } + + console.log("[seed] Seeding built-in team blueprints..."); await db.insert(teamBlueprints).values([ { name: "Startup Founding Team", slug: "startup-founding-team", description: - "A lean 4-person founding team covering engineering, growth, and operations — perfect for going from zero to one.", + "A lean 4-person founding team covering engineering, growth, and operations - perfect for going from zero to one.", category: "Startup", icon: "🚀", agentCount: 4, @@ -433,6 +119,7 @@ async function seed() { ]); console.log("[seed] Seed complete."); + process.exit(0); } seed().catch((err) => {