From 0e6a94c94866ce0a946709ed17f106a59e253462 Mon Sep 17 00:00:00 2001 From: gonzaloaune Date: Wed, 22 Apr 2026 11:21:46 +0000 Subject: [PATCH 1/2] Generated with Hive: Remove app service block from dockerComposeContent and update integration test fixture --- .../api/workspaces-stakgraph.test.ts | 2 +- src/app/api/ask/quick/route.ts | 17 ++++++++++++ .../orgs/[githubLogin]/canvas/[ref]/route.ts | 27 +++++++++++++++---- .../api/orgs/[githubLogin]/canvas/route.ts | 27 +++++++++++++++---- src/utils/devContainerUtils.ts | 14 +--------- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/__tests__/integration/api/workspaces-stakgraph.test.ts b/src/__tests__/integration/api/workspaces-stakgraph.test.ts index 5151e6edb6..066dc8aa61 100644 --- a/src/__tests__/integration/api/workspaces-stakgraph.test.ts +++ b/src/__tests__/integration/api/workspaces-stakgraph.test.ts @@ -115,7 +115,7 @@ const DEFAULT_DOCKERFILE = "FROM ghcr.io/stakwork/staklink-js:v0.1.2\nRUN echo 'original'"; const UPDATED_DOCKERFILE = "FROM node:20-alpine\nRUN echo 'updated'"; const DEFAULT_DOCKER_COMPOSE = - "version: '3.8'\nservices:\n app:\n build: ."; + "version: '3.8'\nnetworks:\n app_network:\n driver: bridge\nservices: {}"; const UPDATED_DOCKER_COMPOSE = "version: '3.9'\nservices:\n app:\n build: .\n ports:\n - '3000:3000'"; const DEFAULT_DEVCONTAINER = JSON.stringify({ diff --git a/src/app/api/ask/quick/route.ts b/src/app/api/ask/quick/route.ts index ae86e7fbe1..0784ccf4ad 100644 --- a/src/app/api/ask/quick/route.ts +++ b/src/app/api/ask/quick/route.ts @@ -12,6 +12,7 @@ import { z } from "zod"; import { getWorkspaceChannelName, PUSHER_EVENTS, pusherServer } from "@/lib/pusher"; import { sanitizeAndCompleteToolCalls } from "@/lib/ai/message-sanitizer"; import { getMiddlewareContext, requireAuth } from "@/lib/middleware/utils"; +import { db } from "@/lib/db"; /** * Provenance data types @@ -144,6 +145,22 @@ export async function POST(request: NextRequest) { // agent to pick based on intent (document-an-integration vs // draw-a-diagram). if (orgId) { + // Verify the caller actually has a workspace in this org before + // exposing org-scoped canvas/connection tools. + const orgAccess = await db.workspace.findFirst({ + where: { + sourceControlOrgId: orgId, + deleted: false, + OR: [ + { ownerId: userOrResponse.id }, + { members: { some: { userId: userOrResponse.id, leftAt: null } } }, + ], + }, + select: { id: true }, + }); + if (!orgAccess) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } tools = { ...tools, ...buildConnectionTools(orgId, userOrResponse.id), diff --git a/src/app/api/orgs/[githubLogin]/canvas/[ref]/route.ts b/src/app/api/orgs/[githubLogin]/canvas/[ref]/route.ts index be4379744a..4bd353b7c4 100644 --- a/src/app/api/orgs/[githubLogin]/canvas/[ref]/route.ts +++ b/src/app/api/orgs/[githubLogin]/canvas/[ref]/route.ts @@ -18,9 +18,26 @@ function validateCanvasData(value: unknown): value is Record { return true; } -async function findOrg(githubLogin: string) { - return db.sourceControlOrg.findUnique({ - where: { githubLogin }, +/** + * Resolve the org by githubLogin while simultaneously verifying the caller + * has at least one workspace in it. Returns null when the org does not exist + * OR the user has no workspace there (both cases → 404 to avoid leaking + * whether the org exists at all). + */ +async function findOrgForUser(githubLogin: string, userId: string) { + return db.sourceControlOrg.findFirst({ + where: { + githubLogin, + workspaces: { + some: { + deleted: false, + OR: [ + { ownerId: userId }, + { members: { some: { userId, leftAt: null } } }, + ], + }, + }, + }, select: { id: true }, }); } @@ -41,7 +58,7 @@ export async function GET( } try { - const org = await findOrg(githubLogin); + const org = await findOrgForUser(githubLogin, userOrResponse.id); if (!org) { return NextResponse.json({ error: "Organization not found" }, { status: 404 }); } @@ -84,7 +101,7 @@ export async function PUT( return NextResponse.json({ error: "Invalid canvas data" }, { status: 400 }); } - const org = await findOrg(githubLogin); + const org = await findOrgForUser(githubLogin, userOrResponse.id); if (!org) { return NextResponse.json({ error: "Organization not found" }, { status: 404 }); } diff --git a/src/app/api/orgs/[githubLogin]/canvas/route.ts b/src/app/api/orgs/[githubLogin]/canvas/route.ts index bca78abcca..97e2632e4c 100644 --- a/src/app/api/orgs/[githubLogin]/canvas/route.ts +++ b/src/app/api/orgs/[githubLogin]/canvas/route.ts @@ -17,9 +17,26 @@ function validateCanvasData(value: unknown): value is Record { return true; } -async function findOrg(githubLogin: string) { - return db.sourceControlOrg.findUnique({ - where: { githubLogin }, +/** + * Resolve the org by githubLogin while simultaneously verifying the caller + * has at least one workspace in it. Returns null when the org does not exist + * OR the user has no workspace there (both cases → 404 to avoid leaking + * whether the org exists at all). + */ +async function findOrgForUser(githubLogin: string, userId: string) { + return db.sourceControlOrg.findFirst({ + where: { + githubLogin, + workspaces: { + some: { + deleted: false, + OR: [ + { ownerId: userId }, + { members: { some: { userId, leftAt: null } } }, + ], + }, + }, + }, select: { id: true }, }); } @@ -36,7 +53,7 @@ export async function GET( const { githubLogin } = await params; try { - const org = await findOrg(githubLogin); + const org = await findOrgForUser(githubLogin, userOrResponse.id); if (!org) { return NextResponse.json({ error: "Organization not found" }, { status: 404 }); } @@ -72,7 +89,7 @@ export async function PUT( return NextResponse.json({ error: "Invalid canvas data" }, { status: 400 }); } - const org = await findOrg(githubLogin); + const org = await findOrgForUser(githubLogin, userOrResponse.id); if (!org) { return NextResponse.json({ error: "Organization not found" }, { status: 404 }); } diff --git a/src/utils/devContainerUtils.ts b/src/utils/devContainerUtils.ts index 0e6c7c46c9..299900a671 100644 --- a/src/utils/devContainerUtils.ts +++ b/src/utils/devContainerUtils.ts @@ -334,19 +334,7 @@ export function dockerComposeContent() { networks: app_network: driver: bridge -services: - app: - build: - context: . - dockerfile: Dockerfile - volumes: - - ../..:/workspaces:cached - command: sleep infinity - networks: - - app_network - extra_hosts: - - "localhost:172.17.0.1" - - "host.docker.internal:host-gateway" +services: {} `; } From 3ed727f4242f8fbc1763dbf1d100775c98da5675 Mon Sep 17 00:00:00 2001 From: gonzaloaune Date: Sun, 26 Apr 2026 20:35:02 +0000 Subject: [PATCH 2/2] Generated with Hive: Fix CompactTasksList model selector test to avoid undefined error --- .../components/features/CompactTasksList.test.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/__tests__/unit/components/features/CompactTasksList.test.tsx b/src/__tests__/unit/components/features/CompactTasksList.test.tsx index a11e35927f..e6b3c54a9a 100644 --- a/src/__tests__/unit/components/features/CompactTasksList.test.tsx +++ b/src/__tests__/unit/components/features/CompactTasksList.test.tsx @@ -1611,18 +1611,12 @@ describe("CompactTasksList", () => { /> ); + // Wait specifically for the model select (data-value="") to appear after llm-models fetch resolves await waitFor(() => { - // Models should be loaded - find the model select by its empty value const selects = screen.getAllByTestId("select"); - expect(selects.length).toBeGreaterThanOrEqual(1); + const modelSelect = selects.find((s) => s.getAttribute("data-value") === ""); + expect(modelSelect).toBeDefined(); }); - - // Simulate onValueChange being called with a model value - // The Select mock renders a div with data-testid="select" and data-value - // We need to find the model select - it's the one with value="" (task.model is null) - const selects = screen.getAllByTestId("select"); - const modelSelect = selects.find((s) => s.getAttribute("data-value") === ""); - expect(modelSelect).toBeDefined(); }); test("shows existing model value in selector", async () => {