Files
vibn-frontend/app/api/workspaces/[slug]/gitea-credentials/route.ts
Mark Henderson b9511601bc feat(ai-access): per-workspace Gitea bot + tenant-safe Coolify proxy + MCP
Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.

Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
  columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
  createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
  a Writers team (write perms only) on the workspace's org, and stores
  its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
  bot PAT + clone URL template; session or vibn_sk_ bearer auth.

Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
  getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
  GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
  GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
  keys, exposing workspace.describe, gitea.credentials, projects.*,
  apps.* (list/get/deploy/deployments, envs.list/upsert/delete).

Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
  .cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
  Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
  one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
  fallback. Minted-key modal also shows the bootstrap one-liner.

Ops prerequisites:
  - Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
  - Run /api/admin/migrate to add the three bot columns.
  - GITEA_API_TOKEN must be a site-admin token (needed for admin/users
    + Sudo PAT mint); otherwise provision_status lands on 'partial'.

Made-with: Cursor
2026-04-21 10:49:17 -07:00

86 lines
2.7 KiB
TypeScript

/**
* GET /api/workspaces/[slug]/gitea-credentials
*
* Returns a ready-to-use git clone URL for the workspace's Gitea org,
* plus the bot username/token. This is the one endpoint an AI agent
* calls before doing any git work — it hides all the admin/org/bot
* bookkeeping behind a single bearer-auth request.
*
* Auth: NextAuth session (owner) OR `Bearer vibn_sk_...` scoped to
* this workspace. Never returns credentials for a different workspace.
*
* The plaintext PAT is decrypted on the server on every call — we
* never persist it in logs or client state.
*/
import { NextResponse } from 'next/server';
import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth';
import { getWorkspaceBotCredentials, ensureWorkspaceProvisioned } from '@/lib/workspaces';
const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
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;
// If the bot has never been provisioned, do it now. Idempotent.
let workspace = principal.workspace;
if (!workspace.gitea_bot_token_encrypted || !workspace.gitea_org) {
try {
workspace = await ensureWorkspaceProvisioned(workspace);
} catch (err) {
return NextResponse.json(
{
error: 'Provisioning failed',
details: err instanceof Error ? err.message : String(err),
},
{ status: 502 }
);
}
}
const creds = getWorkspaceBotCredentials(workspace);
if (!creds) {
return NextResponse.json(
{
error: 'Workspace has no Gitea bot yet',
provisionStatus: workspace.provision_status,
provisionError: workspace.provision_error,
hint:
'POST /api/workspaces/' +
slug +
'/provision to retry bot provisioning.',
},
{ status: 503 }
);
}
const apiBase = GITEA_API_URL.replace(/\/$/, '');
const host = new URL(apiBase).host;
return NextResponse.json({
workspace: { slug: workspace.slug, giteaOrg: creds.org },
bot: {
username: creds.username,
// Full plaintext PAT — treat like a password.
token: creds.token,
},
gitea: {
apiBase,
host,
// Templates for the agent. Substitute {{repo}} with the repo name.
cloneUrlTemplate: `https://${creds.username}:${creds.token}@${host}/${creds.org}/{{repo}}.git`,
sshRemoteTemplate: `git@${host}:${creds.org}/{{repo}}.git`,
webUrlTemplate: `${apiBase}/${creds.org}/{{repo}}`,
},
principal: {
source: principal.source,
apiKeyId: principal.apiKeyId ?? null,
},
});
}