From 56e7c2fb5c7bd73ed3f77a08ded5d49af300999d Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 27 Apr 2026 16:04:38 -0700 Subject: [PATCH] Fix auto-token fetch: use keys?include_default_token=true instead of /keys/default Avoids the Next.js dynamic route conflict with [keyId]. Chat panel now correctly bootstraps MCP token on mount with no user action. Made-with: Cursor --- .../workspaces/[slug]/keys/default/route.ts | 66 ------------------- app/api/workspaces/[slug]/keys/route.ts | 35 +++++++++- components/vibn-chat/chat-panel.tsx | 8 +-- 3 files changed, 38 insertions(+), 71 deletions(-) delete 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 deleted file mode 100644 index 97f8e424..00000000 --- a/app/api/workspaces/[slug]/keys/default/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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/app/api/workspaces/[slug]/keys/route.ts b/app/api/workspaces/[slug]/keys/route.ts index 0683d617..9b25d425 100644 --- a/app/api/workspaces/[slug]/keys/route.ts +++ b/app/api/workspaces/[slug]/keys/route.ts @@ -13,7 +13,7 @@ import { NextResponse } from 'next/server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; -import { listWorkspaceApiKeys, mintWorkspaceApiKey } from '@/lib/auth/workspace-auth'; +import { listWorkspaceApiKeys, mintWorkspaceApiKey, revealWorkspaceApiKey } from '@/lib/auth/workspace-auth'; export async function GET( request: Request, @@ -24,6 +24,39 @@ export async function GET( if (principal instanceof NextResponse) return principal; const keys = await listWorkspaceApiKeys(principal.workspace.id); + + // ?include_default_token=true — used by the chat panel on mount to + // bootstrap tool-calling without any manual setup. Session-only. + const url = new URL(request.url); + if (url.searchParams.get('include_default_token') === 'true' && principal.source === 'session') { + let defaultKey = keys.find((k: any) => k.name === 'default' && !k.revoked_at); + + // Legacy workspace — mint the default key on demand + if (!defaultKey) { + const minted = await mintWorkspaceApiKey({ + workspaceId: principal.workspace.id, + name: 'default', + createdBy: principal.userId, + scopes: ['workspace:*'], + }); + return NextResponse.json({ keys, defaultToken: minted.token }); + } + + const token = await revealWorkspaceApiKey(principal.workspace.id, defaultKey.id); + if (!token) { + // Pre-encryption key — rotate it + const minted = await mintWorkspaceApiKey({ + workspaceId: principal.workspace.id, + name: 'default', + createdBy: principal.userId, + scopes: ['workspace:*'], + }); + return NextResponse.json({ keys, defaultToken: minted.token }); + } + + return NextResponse.json({ keys, defaultToken: token }); + } + return NextResponse.json({ keys }); } diff --git a/components/vibn-chat/chat-panel.tsx b/components/vibn-chat/chat-panel.tsx index 728d4aaa..9e187129 100644 --- a/components/vibn-chat/chat-panel.tsx +++ b/components/vibn-chat/chat-panel.tsx @@ -169,12 +169,12 @@ export function ChatPanel() { 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`) + fetch(`/api/workspaces/${workspace}/keys?include_default_token=true`) .then((r) => r.ok ? r.json() : null) .then((d) => { - if (d?.token) { - localStorage.setItem(`vibn-mcp-token-${workspace}`, d.token); - setMcpToken(d.token); + if (d?.defaultToken) { + localStorage.setItem(`vibn-mcp-token-${workspace}`, d.defaultToken); + setMcpToken(d.defaultToken); } }) .catch(() => {/* silent — panel works in read-only mode */});