/** * GET /api/workspaces/[slug]/storage/buckets — describe the workspace's * provisioned GCS state (default bucket name, SA email, HMAC accessId, * provision status). Does NOT return the HMAC secret. * * POST /api/workspaces/[slug]/storage/buckets — idempotently provisions * the per-workspace GCS substrate: * 1. dedicated GCP service account (vibn-ws-{slug}@…) * 2. SA JSON keyfile (encrypted at rest) * 3. default bucket vibn-ws-{slug}-{6char} in northamerica-northeast1 * 4. roles/storage.objectAdmin binding for the SA on that bucket * 5. HMAC key on the SA so app code can use AWS S3 SDKs * Safe to re-run; each step short-circuits when already complete. * * Auth: session OR `Bearer vibn_sk_...`. Same workspace-scope rules as * every other /api/workspaces/[slug]/* endpoint. * * P5.3 — vertical slice. The full storage.* tool family (presign, * list_objects, delete_object, set_lifecycle) lands once this * provisioning step is verified end-to-end. */ import { NextResponse } from 'next/server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; import { ensureWorkspaceGcsProvisioned, getWorkspaceGcsState, } from '@/lib/workspace-gcs'; export async function GET( request: Request, { params }: { params: Promise<{ slug: string }> }, ) { const { slug } = await params; const principal = await requireWorkspacePrincipal(request, { targetSlug: slug }); if (principal instanceof NextResponse) return principal; const ws = await getWorkspaceGcsState(principal.workspace.id); if (!ws) { return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }); } return NextResponse.json({ workspace: { slug: ws.slug }, storage: { status: ws.gcp_provision_status ?? 'pending', error: ws.gcp_provision_error ?? null, serviceAccountEmail: ws.gcp_service_account_email ?? null, defaultBucketName: ws.gcs_default_bucket_name ?? null, hmacAccessId: ws.gcs_hmac_access_id ?? null, location: 'northamerica-northeast1', }, }); } export async function POST( request: Request, { params }: { params: Promise<{ slug: string }> }, ) { const { slug } = await params; const principal = await requireWorkspacePrincipal(request, { targetSlug: slug }); if (principal instanceof NextResponse) return principal; try { const result = await ensureWorkspaceGcsProvisioned(principal.workspace); return NextResponse.json( { workspace: { slug: principal.workspace.slug }, storage: { status: result.status, serviceAccountEmail: result.serviceAccountEmail, bucket: result.bucket, hmacAccessId: result.hmac.accessId, location: result.bucket.location, }, }, { status: 200 }, ); } catch (err) { const message = err instanceof Error ? err.message : String(err); // Schema-not-applied detection: makes the failure mode obvious in // dev before the operator runs scripts/migrate-workspace-gcs.sql. if (/column .* does not exist/i.test(message)) { return NextResponse.json( { error: 'GCS columns missing on vibn_workspaces. Run scripts/migrate-workspace-gcs.sql.', details: message, }, { status: 503 }, ); } return NextResponse.json( { error: 'GCS provisioning failed', details: message }, { status: 502 }, ); } }