From 1e138d69d66a04dc8079b4ce79422372502edd59 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 27 Apr 2026 15:43:27 -0700 Subject: [PATCH] Auto-mint default MCP token on workspace creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ensureWorkspaceForUser() now calls mintWorkspaceApiKey('default') on first workspace creation - Legacy workspaces without a default key get one minted on first request - GET /api/workspaces/[slug]/keys/default reveals (or mints) the default token for session users - Chat panel fetches the token automatically on mount, caches it in localStorage - No manual setup needed — tool calling works immediately on first sign-in Made-with: Cursor --- .../workspaces/[slug]/keys/default/route.ts | 66 +++++++++++++++++++ components/vibn-chat/chat-panel.tsx | 19 ++++-- lib/workspaces.ts | 15 +++++ 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 app/api/workspaces/[slug]/keys/default/route.ts diff --git a/app/api/workspaces/[slug]/keys/default/route.ts b/app/api/workspaces/[slug]/keys/default/route.ts new file mode 100644 index 00000000..97f8e424 --- /dev/null +++ b/app/api/workspaces/[slug]/keys/default/route.ts @@ -0,0 +1,66 @@ +/** + * GET /api/workspaces/[slug]/keys/default + * + * Returns the plaintext token for the workspace's "default" API key, + * auto-minted at workspace creation. Used by the chat panel to bootstrap + * itself without any manual setup step. + * + * Session-only: never callable by an API-key principal (same restriction + * as the reveal endpoint, for the same reason). + * + * If no default key exists (legacy workspaces created before auto-mint), + * one is minted on the spot and returned. + */ +import { NextResponse } from 'next/server'; +import { + requireWorkspacePrincipal, + listWorkspaceApiKeys, + mintWorkspaceApiKey, + revealWorkspaceApiKey, +} from '@/lib/auth/workspace-auth'; + +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 (principal.source !== 'session') { + return NextResponse.json( + { error: 'Default key can only be retrieved from a signed-in session' }, + { status: 403 }, + ); + } + + const ws = principal.workspace; + const keys = await listWorkspaceApiKeys(ws.id); + let defaultKey = keys.find((k: any) => k.name === 'default' && !k.revoked_at); + + // Legacy workspace — mint the default key now + if (!defaultKey) { + const minted = await mintWorkspaceApiKey({ + workspaceId: ws.id, + name: 'default', + createdBy: principal.userId, + scopes: ['workspace:*'], + }); + return NextResponse.json({ token: minted.token, keyId: minted.id, minted: true }); + } + + // Reveal the stored plaintext + const token = await revealWorkspaceApiKey(ws.id, defaultKey.id); + if (!token) { + // Key predates encryption — rotate it + const minted = await mintWorkspaceApiKey({ + workspaceId: ws.id, + name: 'default', + createdBy: principal.userId, + scopes: ['workspace:*'], + }); + return NextResponse.json({ token: minted.token, keyId: minted.id, rotated: true }); + } + + return NextResponse.json({ token, keyId: defaultKey.id }); +} diff --git a/components/vibn-chat/chat-panel.tsx b/components/vibn-chat/chat-panel.tsx index a463c637..728d4aaa 100644 --- a/components/vibn-chat/chat-panel.tsx +++ b/components/vibn-chat/chat-panel.tsx @@ -163,11 +163,22 @@ export function ChatPanel() { document.documentElement.style.setProperty("--chat-panel-width", open ? "380px" : "0px"); }, [open]); - // Load MCP token from localStorage (set at /settings) + // Load MCP token — prefer localStorage cache, fetch from API if missing useEffect(() => { - const t = localStorage.getItem(`vibn-mcp-token-${workspace}`); - if (t) setMcpToken(t); - }, [workspace]); + if (!workspace || status !== "authenticated") return; + const cached = localStorage.getItem(`vibn-mcp-token-${workspace}`); + if (cached) { setMcpToken(cached); return; } + // Auto-fetch the workspace's default key (created at account setup) + fetch(`/api/workspaces/${workspace}/keys/default`) + .then((r) => r.ok ? r.json() : null) + .then((d) => { + if (d?.token) { + localStorage.setItem(`vibn-mcp-token-${workspace}`, d.token); + setMcpToken(d.token); + } + }) + .catch(() => {/* silent — panel works in read-only mode */}); + }, [workspace, status]); // Load threads const loadThreads = useCallback(async () => { diff --git a/lib/workspaces.ts b/lib/workspaces.ts index 6ac6744c..cea09604 100644 --- a/lib/workspaces.ts +++ b/lib/workspaces.ts @@ -25,6 +25,7 @@ import { COOLIFY_DEFAULT_SERVER_UUID, COOLIFY_DEFAULT_DESTINATION_UUID, } from '@/lib/coolify'; +import { mintWorkspaceApiKey } from '@/lib/auth/workspace-auth'; import { createOrg, getOrg, @@ -178,6 +179,20 @@ export async function ensureWorkspaceForUser(opts: { [workspace.id, opts.userId] ); + // Auto-mint a default API key so the chat panel and Cursor MCP + // integration work without any manual setup step. + try { + await mintWorkspaceApiKey({ + workspaceId: workspace.id, + name: 'default', + createdBy: opts.userId, + scopes: ['workspace:*'], + }); + } catch (err) { + // Non-fatal — user can mint manually in Settings if this fails. + console.error('[workspaces] auto-mint default key failed', workspace.slug, err); + } + return workspace; }