import { NextRequest, NextResponse } from "next/server"; import { getSessionUser, canMutate } from "@/lib/session"; import { canUserSeeTenant } from "@/lib/visibility"; import { getTenant, patchTenantSpec } from "@/lib/k8s"; import { getPackageDef } from "@/lib/packages"; import { safeError } from "@/lib/errors"; const ALLOWED_WORKSPACE_FILES = ["SOUL.md", "AGENTS.md", "TOOLS.md"]; const MAX_WORKSPACE_FILE_SIZE = 10_000; export async function GET( _req: NextRequest, { params }: { params: Promise<{ name: string }> } ) { const user = await getSessionUser(); if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { name } = await params; try { const tenant = await getTenant(name); if (!tenant) return NextResponse.json({ error: "Not found" }, { status: 404 }); // Slice 6: visibility now includes assignment-table check for // user-role members. We return 404 (not 403) to avoid leaking // tenant existence — same as cross-org reads. if (!(await canUserSeeTenant(user, tenant))) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } return NextResponse.json(tenant); } catch (e: any) { return NextResponse.json( { error: safeError(e, "Failed to fetch tenant") }, { status: e.statusCode || 500 } ); } } export async function PATCH( req: NextRequest, { params }: { params: Promise<{ name: string }> } ) { const user = await getSessionUser(); if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!canMutate(user)) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const { name } = await params; const body = await req.json(); try { const existing = await getTenant(name); if (!existing) return NextResponse.json({ error: "Not found" }, { status: 404 }); if ( !user.isPlatform && existing.metadata.labels?.["pieced.ch/zitadel-org-id"] !== user.orgId ) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const specPatch: Record = {}; // ── Validate packages against catalog ── if (body.packages !== undefined) { if (!Array.isArray(body.packages) || body.packages.length > 10) { return NextResponse.json( { error: "Invalid packages: must be an array of at most 10 items" }, { status: 400 } ); } for (const pkg of body.packages) { if (typeof pkg !== "string" || !getPackageDef(pkg)) { return NextResponse.json( { error: `Unknown package: ${pkg}` }, { status: 400 } ); } } specPatch.packages = body.packages; } // ── Validate workspaceFiles ── if (body.workspaceFiles !== undefined) { if ( typeof body.workspaceFiles !== "object" || body.workspaceFiles === null || Array.isArray(body.workspaceFiles) ) { return NextResponse.json( { error: "Invalid workspaceFiles: must be an object" }, { status: 400 } ); } for (const [key, value] of Object.entries(body.workspaceFiles)) { if (!ALLOWED_WORKSPACE_FILES.includes(key)) { return NextResponse.json( { error: `Invalid workspace file: ${key}. Allowed: ${ALLOWED_WORKSPACE_FILES.join(", ")}`, }, { status: 400 } ); } if ( typeof value !== "string" || value.length > MAX_WORKSPACE_FILE_SIZE ) { return NextResponse.json( { error: `Workspace file ${key} must be a string of at most ${MAX_WORKSPACE_FILE_SIZE} characters`, }, { status: 400 } ); } } specPatch.workspaceFiles = body.workspaceFiles; } // ── Simple string fields ── if (body.displayName !== undefined) { if ( typeof body.displayName !== "string" || body.displayName.length < 1 || body.displayName.length > 100 ) { return NextResponse.json( { error: "displayName must be 1-100 characters" }, { status: 400 } ); } specPatch.displayName = body.displayName; } if (body.agentName !== undefined) { if ( typeof body.agentName !== "string" || body.agentName.length < 1 || body.agentName.length > 50 ) { return NextResponse.json( { error: "agentName must be 1-50 characters" }, { status: 400 } ); } specPatch.agentName = body.agentName; } // ── channelUsers (basic shape validation) ── if (body.channelUsers !== undefined) { if ( typeof body.channelUsers !== "object" || body.channelUsers === null || Array.isArray(body.channelUsers) ) { return NextResponse.json( { error: "Invalid channelUsers: must be an object" }, { status: 400 } ); } for (const [channel, users] of Object.entries(body.channelUsers)) { if (typeof channel !== "string" || channel.length > 50) { return NextResponse.json( { error: `Invalid channel name: ${channel}` }, { status: 400 } ); } if ( !Array.isArray(users) || (users as any[]).some( (u: any) => typeof u !== "string" || u.length > 100 ) ) { return NextResponse.json( { error: `Invalid user IDs for channel ${channel}` }, { status: 400 } ); } } specPatch.channelUsers = body.channelUsers; } const updated = await patchTenantSpec(name, specPatch); return NextResponse.json(updated); } catch (e: any) { return NextResponse.json( { error: safeError(e, "Failed to update tenant") }, { status: e.statusCode || 500 } ); } }